File size: 7,151 Bytes
2da34a9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | import asyncio
from app.schemas.analysis import AnalysisResult
from app.schemas.company import CompanyProfile
from app.schemas.tender import Tender
from app.services.llm import call_gemini, _parse_gemini_response, call_gemini_with_model
from app.services.report import generate_markdown_report
from app.config import settings
async def legal_agent_task(tender: Tender, company: CompanyProfile, document_text: str = "", model: str | None = None, tender_details: dict | None = None) -> str:
details_str = f"\nSCRAPED DETAILS: {tender_details}" if tender_details else ""
prompt = (
f"AGENT ROLE: Legal & Compliance Expert (Chilean Public Procurement)\n"
f"GOAL: Analyze administrative bases and compliance risks.\n"
f"TENDER: {tender.name} (Type: {tender.type})\n"
f"COMPANY: {company.name}\n"
f"EXTRACTED TEXT: {document_text[:5000]}\n"
f"{details_str}\n"
f"TASK: Identify 3 legal gaps/risks. Respond in Spanish."
)
return await call_gemini_with_model(prompt, model)
async def technical_agent_task(tender: Tender, company: CompanyProfile, document_text: str = "", model: str | None = None, tender_details: dict | None = None) -> str:
details_str = f"\nSCRAPED DETAILS: {tender_details}" if tender_details else ""
prompt = (
f"AGENT ROLE: Technical Architect\n"
f"GOAL: Evaluate technical feasibility.\n"
f"TENDER: {tender.name} - {tender.description}\n"
f"COMPANY: {company.industry} - {company.experience}\n"
f"EXTRACTED TEXT: {document_text[:5000]}\n"
f"{details_str}\n"
f"TASK: Identify 3 technical challenges. Respond in Spanish."
)
return await call_gemini_with_model(prompt, model)
async def strategy_agent_task(tender: Tender, company: CompanyProfile, document_text: str = "", model: str | None = None, tender_details: dict | None = None) -> str:
details_str = f"\nSCRAPED DETAILS: {tender_details}" if tender_details else ""
prompt = (
f"AGENT ROLE: Risk & Strategy Specialist\n"
f"GOAL: Calculate ROI and strategy.\n"
f"TENDER: {tender.name}\n"
f"COMPANY: {company.name}\n"
f"{details_str}\n"
f"TASK: Identify 3 strategic risks and a win strategy. Respond in Spanish."
)
return await call_gemini_with_model(prompt, model)
async def run_full_analysis(tender: Tender, company_profile: CompanyProfile, document_text: str | None = None, models: dict | None = None, tender_details: dict | None = None) -> AnalysisResult:
audit_log = ["🚀 Iniciando mesa de expertos agéntica..."]
doc_text = document_text or ""
# Use selected models or defaults
chosen_models = models or {
"legal": "Llama-3.3-70B (Groq)" if settings.groq_api_key else "Gemini 2.5 Flash",
"tech": "Llama-3.1-8B (Groq)" if settings.groq_api_key else "Qwen-2.5 (Featherless)",
"risk": "Llama-3.3-70B (Groq)" if settings.groq_api_key else "Qwen-2.5 (Featherless)"
}
audit_log.append(f"👨⚖️ Agente Legal ({chosen_models.get('legal')})")
audit_log.append(f"👨💻 Agente Técnico ({chosen_models.get('tech')})")
audit_log.append(f"🕵️ Agente de Riesgo ({chosen_models.get('risk')})")
tasks = [
legal_agent_task(tender, company_profile, doc_text, chosen_models.get("legal"), tender_details),
technical_agent_task(tender, company_profile, doc_text, chosen_models.get("tech"), tender_details),
strategy_agent_task(tender, company_profile, doc_text, chosen_models.get("risk"), tender_details)
]
responses = await asyncio.gather(*tasks)
legal_resp, tech_resp, strat_resp = responses
audit_log.append("💡 Consolidando hallazgos...")
synthesis_prompt = (
f"SISTEMA DE CONSENSO ANDESOPS AI (ESTRUCTURA DE ALTO IMPACTO)\n"
f"Licitación: {tender.name}\n"
f"Comprador: {tender.buyer}\n"
f"Reporte Legal: {legal_resp}\n"
f"Reporte Técnico: {tech_resp}\n"
f"Reporte Estratégico: {strat_resp}\n\n"
f"Genera un JSON 'AnalysisResult' siguiendo estas reglas estrictas:\n"
f"1. fit_score (int 0-100)\n"
f"2. decision ('Recommended', 'Review Carefully', 'Not Recommended')\n"
f"3. executive_summary: Un resumen ejecutivo de alto nivel, profesional y persuasivo.\n"
f"4. risks: Lista de {{title, severity, explanation}} con los riesgos críticos detectados.\n"
f"5. key_requirements: Lista de requisitos técnicos/administrativos ineludibles.\n"
f"6. compliance_gaps: Brechas que la empresa debe cerrar para ganar.\n"
f"7. action_plan: Pasos concretos a seguir.\n"
f"8. strategic_roadmap: Un roadmap estratégico en Markdown que explique cómo ganar.\n"
f"9. proposal_draft: **CRÍTICO** - Genera un borrador de propuesta técnica formal y detallado en Markdown.\n"
f" Debe incluir: \n"
f" - Portada (Título de Licitación, Empresa, Fecha)\n"
f" - Introducción y Objetivos\n"
f" - Solución Técnica Propuesta (basada en el reporte técnico)\n"
f" - Metodología de Implementación\n"
f" - Propuesta de Valor Diferenciadora (por qué elegirnos)\n"
f" - Cronograma estimado\n"
f" - Conclusión Profesional\n"
f"10. requirement_responses: " + (f"Genera exactamente {tender_details.get('metadata', {}).get('question_count', 0)} pares de {{question, answer}} basados en las preguntas reales del mercado. " if tender_details and tender_details.get('metadata', {}).get('question_count', 0) > 0 else "Genera solo 3 preguntas y respuestas basadas en requisitos hipotéticos/claves ya que no hay preguntas de mercado activas. ") + "\n"
f"11. report_markdown: Un reporte general para consumo interno.\n"
f"Responde ÚNICAMENTE con el JSON plano. No incluyas explicaciones fuera del JSON."
)
final_output = await call_gemini(synthesis_prompt, is_json=True)
# Fallback for synthesis if Gemini/Groq failed to return valid JSON
if not final_output and settings.groq_api_key:
from app.services.llm import call_groq
final_output = await call_groq(synthesis_prompt, "llama-3.3-70b-versatile")
parse_result = _parse_gemini_response(final_output)
if parse_result:
try:
# Ensure report_markdown exists
if not parse_result.get("report_markdown"):
parse_result["report_markdown"] = generate_markdown_report(parse_result)
result = AnalysisResult(**parse_result)
result.audit_log = audit_log + (result.audit_log or [])
result.raw_responses = {
"legal": legal_resp,
"technical": tech_resp,
"strategy": strat_resp
}
return result
except Exception as e:
print(f"Synthesis Validation Error: {e}")
# Ultimate fallback to the logic in llm.py
from app.services.llm import generate_analysis
return await generate_analysis(tender, company_profile, doc_text, models)
|