Á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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
else:
|
| 187 |
-
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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=
|
| 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-
|
|
|
|
| 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
|