Álvaro Valenzuela Valdes commited on
Commit
589d3b7
·
1 Parent(s): aeaf3e7

feat: Full multi-agent orchestration workflow with user-defined model selection (Gemma 4, Qwen 3 support)

Browse files
backend/app/services/llm.py CHANGED
@@ -139,62 +139,89 @@ def generate_mock_analysis(tender: Tender, company: CompanyProfile) -> AnalysisR
139
  audit_log=["Iniciando análisis de respaldo...", "Generando datos mock."]
140
  )
141
 
142
- def generate_analysis(tender: Tender, company: CompanyProfile, document_text: str | None = None) -> AnalysisResult:
143
- # Build a prompt that emphasizes the collaboration
144
- prompt = _build_analysis_prompt(tender, company, document_text)
145
-
146
- output = ""
147
- audit_messages = []
148
-
149
- # Strategy: Use Featherless (DeepSeek) for deep technical reasoning if available
150
- if settings.featherless_api_key:
151
- audit_messages.append("🧠 Multi-Model Consensus: DeepSeek-V3.2 (via Featherless) selected for Technical Reasoning.")
152
- output = call_featherless(prompt, model="deepseek-ai/DeepSeek-V3.2")
153
- if not output:
154
- audit_messages.append("⚠️ Featherless failed, falling back to Gemini.")
155
-
156
- # Fallback or Primary if no Featherless
157
- if not output:
158
- if not settings.gemini_api_key:
159
- analysis = generate_mock_analysis(tender, company)
160
- analysis.audit_log = ["⚠️ Error: No LLM keys configured (Gemini/Featherless).", "Fallback: Using mock analysis."]
161
- return analysis
 
 
 
 
 
 
 
 
 
 
162
 
163
- audit_messages.append("🧠 Using Gemini 2.5 Flash for unified agent analysis.")
164
- try:
165
- model = get_gemini_model()
166
- response = model.generate_content(prompt)
167
- output = response.text
168
- except Exception as e:
169
- audit_messages.append(f"❌ Gemini Error: {str(e)}")
170
-
171
- if not output:
172
- analysis = generate_mock_analysis(tender, company)
173
- analysis.audit_log = audit_messages + ["Fallback: System default analysis."]
174
- return analysis
175
-
176
- parse_result = _parse_gemini_response(output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
  if parse_result:
179
  try:
180
- if not parse_result.get("proposal_draft") or len(parse_result["proposal_draft"]) < 50:
181
  parse_result["proposal_draft"] = generate_proposal_draft(parse_result, company)
182
 
183
- if not parse_result.get("report_markdown"):
184
- parse_result["report_markdown"] = generate_markdown_report(parse_result)
185
-
186
  result = AnalysisResult(**parse_result)
187
- # Add our multimodal logs
188
  result.audit_log = audit_messages + (result.audit_log or [])
189
  return result
190
  except Exception as e:
191
- print(f"Error mapping to AnalysisResult: {e}")
192
- error_msg = f"🔍 Data Mapping Error: {str(e)[:50]}..."
193
- else:
194
- error_msg = "🧩 Format Error: Response was not valid JSON."
195
-
196
  analysis = generate_mock_analysis(tender, company)
197
- analysis.audit_log = audit_messages + [error_msg, "Fallback: Reverting to system default."]
198
  return analysis
199
 
200
  def generate_proposal_draft(analysis: dict, company: CompanyProfile) -> str:
 
139
  audit_log=["Iniciando análisis de respaldo...", "Generando datos mock."]
140
  )
141
 
142
+ def generate_analysis(tender: Tender, company: CompanyProfile, document_text: str | None = None, models: dict | None = None) -> AnalysisResult:
143
+ # Default model mapping
144
+ model_map = {
145
+ "Gemini 2.5 Flash": "gemini",
146
+ "DeepSeek-V3.2 (Featherless)": "deepseek-ai/DeepSeek-V3.2",
147
+ "Qwen-3-32B (Featherless)": "Qwen/Qwen3-32B",
148
+ "Gemma-4-31B (Featherless)": "google/gemma-4-31B-it",
149
+ "Llama-3.1-8B (Featherless)": "meta-llama/Meta-Llama-3.1-8B-Instruct"
150
+ }
151
+
152
+ # Get selected models or defaults
153
+ chosen = models or {
154
+ "legal": "Gemini 2.5 Flash",
155
+ "tech": "DeepSeek-V3.2 (Featherless)",
156
+ "risk": "Qwen-3-32B (Featherless)"
157
+ }
158
+
159
+ audit_messages = ["🚀 Launching Multi-Agent Orchestration Pipeline."]
160
+ agent_outputs = {}
161
+
162
+ # Define Agent roles for separate calls
163
+ agent_definitions = {
164
+ "legal": "Experto Legal & Cumplimiento: Evalúa bases administrativas, multas y garantías.",
165
+ "tech": "Ingeniero Técnico: Evalúa arquitectura, stack tecnológico y capacidad de ejecución.",
166
+ "risk": "Estratega Comercial: Evalúa rentabilidad, competencia y riesgos de mercado."
167
+ }
168
+
169
+ for agent_id, role_desc in agent_definitions.items():
170
+ model_name = chosen.get(agent_id, "Gemini 2.5 Flash")
171
+ model_id = model_map.get(model_name, "gemini")
172
 
173
+ audit_messages.append(f"🤖 Agent {agent_id.upper()} calling {model_name}...")
174
+
175
+ agent_prompt = f"""
176
+ Actúa como {role_desc}
177
+ Licitación: {tender.name} ({tender.code})
178
+ Empresa: {company.name}
179
+ Contexto Adicional: {document_text[:5000] if document_text else 'No adjunto.'}
180
+
181
+ PROPORCIONA TU ANÁLISIS ESPECÍFICO (Máx 200 palabras) EN ESPAÑOL.
182
+ """
183
+
184
+ if model_id == "gemini":
185
+ agent_outputs[agent_id] = call_gemini(agent_prompt)
186
+ else:
187
+ agent_outputs[agent_id] = call_featherless(agent_prompt, model=model_id)
188
+
189
+ # FINAL CONSENSUS AGENT (Synthesis)
190
+ audit_messages.append("⚖️ Final Consensus Agent synthesizing results...")
191
+ synthesis_prompt = f"""
192
+ Eres el AGENTE DE CONSENSO. Debes unificar estos 3 análisis en un único JSON de 'AnalysisResult':
193
+
194
+ 1. LEGAL: {agent_outputs.get('legal')}
195
+ 2. TECH: {agent_outputs.get('tech')}
196
+ 3. RISK: {agent_outputs.get('risk')}
197
+
198
+ Genera el JSON final siguiendo estas reglas:
199
+ - executive_summary: Resumen integrador en español.
200
+ - fit_score: Promedio de encaje (0-100).
201
+ - decision: 'Recommended', 'Review Carefully' o 'Not Recommended'.
202
+ - risks, key_requirements, compliance_gaps, action_plan.
203
+ - audit_log: Incluye los pasos tomados.
204
+
205
+ RESPONDE SOLO EL JSON.
206
+ """
207
+
208
+ final_json = call_gemini(synthesis_prompt)
209
+ parse_result = _parse_gemini_response(final_json)
210
 
211
  if parse_result:
212
  try:
213
+ if not parse_result.get("proposal_draft"):
214
  parse_result["proposal_draft"] = generate_proposal_draft(parse_result, company)
215
 
 
 
 
216
  result = AnalysisResult(**parse_result)
 
217
  result.audit_log = audit_messages + (result.audit_log or [])
218
  return result
219
  except Exception as e:
220
+ print(f"Synthesis Mapping Error: {e}")
221
+
222
+ # Final Fallback
 
 
223
  analysis = generate_mock_analysis(tender, company)
224
+ analysis.audit_log = audit_messages + ["⚠️ Synthesis failed, using emergency fallback."]
225
  return analysis
226
 
227
  def generate_proposal_draft(analysis: dict, company: CompanyProfile) -> str:
frontend/app/page.tsx CHANGED
@@ -128,9 +128,9 @@ export default function HomePage() {
128
  }
129
  };
130
 
131
- const handleRunAnalysis = async (documentText?: string) => {
132
  if (!selectedTender) return;
133
- const result = await analyzeTender(selectedTender, companyProfile, documentText);
134
  setAnalysisResult(result);
135
 
136
  try {
 
128
  }
129
  };
130
 
131
+ const handleRunAnalysis = async (documentText?: string, models?: Record<string, string>) => {
132
  if (!selectedTender) return;
133
+ const result = await analyzeTender(selectedTender, companyProfile, documentText, models);
134
  setAnalysisResult(result);
135
 
136
  try {
frontend/components/AgentAnalysis.tsx CHANGED
@@ -8,7 +8,7 @@ type Props = {
8
  tender: Tender | null;
9
  companyProfile: CompanyProfile;
10
  analysis: AnalysisResult | null;
11
- onAnalyze: (documentText?: string) => Promise<void>;
12
  };
13
 
14
  const agents = [
@@ -23,6 +23,13 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
23
  const [file, setFile] = useState<File | null>(null);
24
  const [isUploading, setIsUploading] = useState(false);
25
  const [documentText, setDocumentText] = useState<string | "">("");
 
 
 
 
 
 
 
26
 
27
  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
28
  if (event.target.files && event.target.files[0]) {
@@ -42,7 +49,7 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
42
  extractedText = uploadResult.text;
43
  setDocumentText(extractedText);
44
  }
45
- await onAnalyze(extractedText);
46
  } catch (error) {
47
  console.error("Error during analysis flow:", error);
48
  } finally {
@@ -151,16 +158,55 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
151
  </div>
152
  </div>
153
 
154
- {/* Agents Row (Visual feedback) */}
155
  <div className="grid gap-6 md:grid-cols-3">
156
  {agents.map((agent) => (
157
- <div key={agent.id} className={`glass-card rounded-3xl p-6 flex items-center gap-4 transition-all duration-700 ${isRunning ? 'ring-2 ring-purple-500/50 animate-pulse' : ''} ${analysis ? 'border-purple-500/30' : 'border-white/5'}`}>
158
- <div className={`text-4xl ${isRunning ? 'animate-bounce' : ''}`}>{agent.avatar}</div>
159
- <div>
160
- <div className={`text-[10px] font-bold uppercase tracking-widest ${agent.color}`}>{agent.role}</div>
161
- <div className="text-sm font-bold text-white">{agent.name}</div>
162
- {analysis && <div className="text-[9px] text-green-400 font-bold mt-1 uppercase tracking-tighter tracking-widest">Analysis Ready ✓</div>}
163
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  </div>
165
  ))}
166
  </div>
 
8
  tender: Tender | null;
9
  companyProfile: CompanyProfile;
10
  analysis: AnalysisResult | null;
11
+ onAnalyze: (documentText?: string, models?: Record<string, string>) => Promise<void>;
12
  };
13
 
14
  const agents = [
 
23
  const [file, setFile] = useState<File | null>(null);
24
  const [isUploading, setIsUploading] = useState(false);
25
  const [documentText, setDocumentText] = useState<string | "">("");
26
+ const [agentModels, setAgentModels] = useState({
27
+ legal: "Gemini 2.5 Flash",
28
+ tech: "DeepSeek-V3.2 (Featherless)",
29
+ risk: "Qwen-2.5 (Featherless)"
30
+ });
31
+ const [activeSettings, setActiveSettings] = useState<string | null>(null);
32
+
33
 
34
  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
35
  if (event.target.files && event.target.files[0]) {
 
49
  extractedText = uploadResult.text;
50
  setDocumentText(extractedText);
51
  }
52
+ await onAnalyze(extractedText, agentModels);
53
  } catch (error) {
54
  console.error("Error during analysis flow:", error);
55
  } finally {
 
158
  </div>
159
  </div>
160
 
161
+ {/* Agents Row (Visual feedback & Configuration) */}
162
  <div className="grid gap-6 md:grid-cols-3">
163
  {agents.map((agent) => (
164
+ <div key={agent.id} className="relative group">
165
+ <div className={`glass-card rounded-3xl p-6 flex items-center gap-4 transition-all duration-700 ${isRunning ? 'ring-2 ring-purple-500/50 animate-pulse' : ''} ${analysis ? 'border-purple-500/30' : 'border-white/5'} hover:border-purple-500/20`}>
166
+ <div className={`text-4xl ${isRunning ? 'animate-bounce' : ''}`}>{agent.avatar}</div>
167
+ <div className="flex-1">
168
+ <div className={`text-[10px] font-bold uppercase tracking-widest ${agent.color}`}>{agent.role}</div>
169
+ <div className="text-sm font-bold text-white">{agent.name}</div>
170
+ <div className="text-[9px] text-slate-500 font-mono mt-1 flex items-center gap-1">
171
+ <span className="w-1 h-1 rounded-full bg-slate-500" />
172
+ {agentModels[agent.id as keyof typeof agentModels]}
173
+ </div>
174
+ </div>
175
+ <button
176
+ onClick={() => setActiveSettings(activeSettings === agent.id ? null : agent.id)}
177
+ className="p-2 rounded-xl bg-white/5 text-slate-500 hover:bg-white/10 hover:text-white transition-all active:scale-90"
178
+ >
179
+ ⚙️
180
+ </button>
181
+ </div>
182
+
183
+ {/* Model Selector Popover */}
184
+ {activeSettings === agent.id && (
185
+ <div className="absolute top-full left-0 right-0 mt-2 z-50 glass-card rounded-2xl p-4 border border-purple-500/30 shadow-2xl animate-in fade-in zoom-in-95 duration-200">
186
+ <p className="text-[9px] font-black uppercase text-purple-400 mb-3 tracking-widest px-1">Select Engine</p>
187
+ <div className="space-y-1">
188
+ {[
189
+ "Gemini 2.5 Flash",
190
+ "DeepSeek-V3.2 (Featherless)",
191
+ "Qwen-3-32B (Featherless)",
192
+ "Gemma-4-31B (Featherless)",
193
+ "Llama-3.1-8B (Featherless)"
194
+ ].map(model => (
195
+ <button
196
+ key={model}
197
+ onClick={() => {
198
+ setAgentModels(prev => ({ ...prev, [agent.id]: model }));
199
+ setActiveSettings(null);
200
+ }}
201
+ className={`w-full text-left px-3 py-2 rounded-xl text-xs transition-all flex items-center justify-between ${agentModels[agent.id as keyof typeof agentModels] === model ? 'bg-purple-500/20 text-white border border-purple-500/30' : 'text-slate-400 hover:bg-white/5 hover:text-slate-200'}`}
202
+ >
203
+ <span>{model}</span>
204
+ {agentModels[agent.id as keyof typeof agentModels] === model && <span>✓</span>}
205
+ </button>
206
+ ))}
207
+ </div>
208
+ </div>
209
+ )}
210
  </div>
211
  ))}
212
  </div>