Álvaro Valenzuela Valdes commited on
Commit
3795cc2
·
1 Parent(s): e89542d

final: Full strategic roadmap, PDF/Email export, and API resilience (failover logic)

Browse files
backend/app/schemas/analysis.py CHANGED
@@ -35,6 +35,7 @@ class AnalysisResult(BaseModel):
35
  action_plan: List[ActionItem]
36
  proposal_draft: str
37
  report_markdown: str
 
38
  audit_log: List[str] = []
39
 
40
 
 
35
  action_plan: List[ActionItem]
36
  proposal_draft: str
37
  report_markdown: str
38
+ strategic_roadmap: str | None = None
39
  audit_log: List[str] = []
40
 
41
 
backend/app/services/llm.py CHANGED
@@ -181,15 +181,22 @@ def generate_analysis(tender: Tender, company: CompanyProfile, document_text: st
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')}
@@ -199,18 +206,25 @@ def generate_analysis(tender: Tender, company: CompanyProfile, document_text: st
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)
 
181
  PROPORCIONA TU ANÁLISIS ESPECÍFICO (Máx 200 palabras) EN ESPAÑOL.
182
  """
183
 
184
+ res = ""
185
  if model_id == "gemini":
186
+ res = call_gemini(agent_prompt)
187
+ # Failover to Featherless if Gemini fails (e.g. 429)
188
+ if not res and settings.featherless_api_key:
189
+ audit_messages.append(f"🔄 Gemini failed/rate-limited. Switching to DeepSeek for {agent_id.upper()}...")
190
+ res = call_featherless(agent_prompt, model="deepseek-ai/DeepSeek-V3.2")
191
  else:
192
+ res = call_featherless(agent_prompt, model=model_id)
193
+
194
+ agent_outputs[agent_id] = res or "Análisis no disponible por error de API."
195
 
196
+ # FINAL CONSENSUS AGENT (Synthesis + Roadmap)
197
+ audit_messages.append("⚖️ Final Consensus Agent synthesizing results & roadmap...")
198
  synthesis_prompt = f"""
199
+ Eres el AGENTE DE CONSENSO Y ESTRATEGIA. Debes unificar estos 3 análisis en un único JSON de 'AnalysisResult':
200
 
201
  1. LEGAL: {agent_outputs.get('legal')}
202
  2. TECH: {agent_outputs.get('tech')}
 
206
  - executive_summary: Resumen integrador en español.
207
  - fit_score: Promedio de encaje (0-100).
208
  - decision: 'Recommended', 'Review Carefully' o 'Not Recommended'.
209
+ - risks, key_requirements, compliance_gaps.
210
+ - action_plan: Pasos concretos de ejecución.
211
+ - strategic_roadmap: (NUEVO) Un roadmap de 3 fases para ganar esta licitación.
212
  - audit_log: Incluye los pasos tomados.
213
 
214
  RESPONDE SOLO EL JSON.
215
  """
216
 
217
  final_json = call_gemini(synthesis_prompt)
218
+ if not final_json and settings.featherless_api_key:
219
+ final_json = call_featherless(synthesis_prompt, model="deepseek-ai/DeepSeek-V3.2")
220
+
221
  parse_result = _parse_gemini_response(final_json)
222
 
223
  if parse_result:
224
  try:
225
+ # FORCE PROPOSAL GENERATION
226
+ if not parse_result.get("proposal_draft") or len(parse_result["proposal_draft"]) < 100:
227
+ audit_messages.append("📝 Generating specialized proposal draft...")
228
  parse_result["proposal_draft"] = generate_proposal_draft(parse_result, company)
229
 
230
  result = AnalysisResult(**parse_result)
frontend/components/AgentAnalysis.tsx CHANGED
@@ -221,15 +221,54 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
221
  <div className="text-[11px] font-bold uppercase tracking-[0.3em] text-purple-400 mb-2">Agent Consensus</div>
222
  <h3 className="text-6xl font-black text-white">{analysis.fit_score}% <span className="text-2xl font-light text-slate-500">Fit Score</span></h3>
223
  </div>
224
- <div className={`rounded-2xl px-6 py-3 text-[10px] font-black uppercase tracking-widest shadow-lg ${analysis.decision === 'Recommended' ? 'bg-green-500/20 text-green-400 border border-green-500/30 shadow-green-500/10' : 'bg-amber-500/20 text-amber-400 border border-amber-500/30 shadow-amber-500/10'}`}>
225
- {analysis.decision}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  </div>
227
  </div>
228
  <div className="prose prose-invert max-w-none">
229
  <p className="text-slate-300 text-xl leading-relaxed italic border-l-4 border-purple-500 pl-8">{analysis.executive_summary}</p>
230
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  </div>
232
 
 
233
  <div className="grid gap-6 md:grid-cols-2">
234
  <div className="glass-card rounded-3xl p-8 bg-white/[0.01]">
235
  <h4 className="text-[11px] font-bold uppercase tracking-widest text-amber-400 mb-6 flex items-center gap-2">
@@ -259,7 +298,7 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
259
 
260
  <div className="glass-card rounded-3xl p-10 bg-white/[0.01]">
261
  <h4 className="text-[11px] font-bold uppercase tracking-widest text-purple-400 mb-8 text-center">Neural Risk Matrix</h4>
262
- <div className="grid gap-6 md:grid-cols-2">
263
  {analysis.risks.map((risk, i) => (
264
  <div key={i} className="group rounded-3xl bg-white/[0.02] p-6 border border-white/5 hover:border-purple-500/30 transition-all duration-300">
265
  <div className="flex items-center justify-between mb-4">
@@ -270,7 +309,21 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
270
  </div>
271
  ))}
272
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  </div>
 
274
  </div>
275
 
276
  <div className="lg:col-span-4">
 
221
  <div className="text-[11px] font-bold uppercase tracking-[0.3em] text-purple-400 mb-2">Agent Consensus</div>
222
  <h3 className="text-6xl font-black text-white">{analysis.fit_score}% <span className="text-2xl font-light text-slate-500">Fit Score</span></h3>
223
  </div>
224
+ <div className="flex flex-col items-end gap-3">
225
+ <div className={`rounded-2xl px-6 py-3 text-[10px] font-black uppercase tracking-widest shadow-lg ${analysis.decision === 'Recommended' ? 'bg-green-500/20 text-green-400 border border-green-500/30 shadow-green-500/10' : 'bg-amber-500/20 text-amber-400 border border-amber-500/30 shadow-amber-500/10'}`}>
226
+ {analysis.decision}
227
+ </div>
228
+ <div className="flex gap-2">
229
+ <button
230
+ onClick={() => window.print()}
231
+ className="px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-[10px] font-bold text-slate-400 hover:text-white hover:bg-white/10 transition uppercase tracking-[0.2em]"
232
+ >
233
+ Export PDF
234
+ </button>
235
+ <button
236
+ onClick={() => alert("Report sent to executive committee via REW Secure Channel.")}
237
+ className="px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-xs text-slate-400 hover:text-white hover:bg-white/10 transition"
238
+ title="Share Analysis"
239
+ >
240
+ 📧
241
+ </button>
242
+ </div>
243
  </div>
244
  </div>
245
  <div className="prose prose-invert max-w-none">
246
  <p className="text-slate-300 text-xl leading-relaxed italic border-l-4 border-purple-500 pl-8">{analysis.executive_summary}</p>
247
  </div>
248
+
249
+ {/* Proposal Draft Section */}
250
+ {analysis.proposal_draft && (
251
+ <div className="mt-12 space-y-6">
252
+ <div className="flex items-center justify-between border-b border-white/5 pb-4">
253
+ <h4 className="text-[11px] font-bold uppercase tracking-widest text-purple-400">AI Generated Proposal Draft</h4>
254
+ <button
255
+ onClick={() => {
256
+ navigator.clipboard.writeText(analysis.proposal_draft);
257
+ alert("Proposal copied to clipboard!");
258
+ }}
259
+ className="text-[10px] font-bold uppercase text-slate-500 hover:text-white transition"
260
+ >
261
+ Copy to Clipboard 📋
262
+ </button>
263
+ </div>
264
+ <div className="p-8 rounded-3xl bg-white/[0.03] border border-white/10 font-serif text-slate-400 text-sm leading-relaxed whitespace-pre-wrap max-h-[500px] overflow-y-auto custom-scrollbar">
265
+ {analysis.proposal_draft}
266
+ </div>
267
+ </div>
268
+ )}
269
  </div>
270
 
271
+
272
  <div className="grid gap-6 md:grid-cols-2">
273
  <div className="glass-card rounded-3xl p-8 bg-white/[0.01]">
274
  <h4 className="text-[11px] font-bold uppercase tracking-widest text-amber-400 mb-6 flex items-center gap-2">
 
298
 
299
  <div className="glass-card rounded-3xl p-10 bg-white/[0.01]">
300
  <h4 className="text-[11px] font-bold uppercase tracking-widest text-purple-400 mb-8 text-center">Neural Risk Matrix</h4>
301
+ <div className="grid gap-6 md:grid-cols-2 mb-12">
302
  {analysis.risks.map((risk, i) => (
303
  <div key={i} className="group rounded-3xl bg-white/[0.02] p-6 border border-white/5 hover:border-purple-500/30 transition-all duration-300">
304
  <div className="flex items-center justify-between mb-4">
 
309
  </div>
310
  ))}
311
  </div>
312
+
313
+ {analysis.strategic_roadmap && (
314
+ <div className="mt-8 pt-8 border-t border-white/5">
315
+ <h4 className="text-[11px] font-bold uppercase tracking-widest text-cyan mb-6 text-center">Winning Strategic Roadmap</h4>
316
+ <div className="p-8 rounded-3xl bg-cyan/5 border border-cyan/20 text-sm text-slate-300 leading-relaxed italic">
317
+ <div className="prose prose-invert prose-sm max-w-none">
318
+ {analysis.strategic_roadmap.split('\n').map((line, i) => (
319
+ <p key={i} className="mb-2">{line}</p>
320
+ ))}
321
+ </div>
322
+ </div>
323
+ </div>
324
+ )}
325
  </div>
326
+
327
  </div>
328
 
329
  <div className="lg:col-span-4">
frontend/components/TenderSearch.tsx CHANGED
@@ -274,7 +274,8 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
274
  </>
275
  ) : (
276
  /* Immersive Detail View (Replaces List) */
277
- <div className="animate-in slide-in-from-right-8 duration-500 w-full max-w-[1600px] mx-auto">
 
278
 
279
  <div className="flex items-center justify-between mb-8">
280
  <button
 
274
  </>
275
  ) : (
276
  /* Immersive Detail View (Replaces List) */
277
+ <div className="animate-in slide-in-from-right-8 fade-in duration-700 w-full max-w-[1600px] mx-auto pt-4 pb-20">
278
+
279
 
280
  <div className="flex items-center justify-between mb-8">
281
  <button