Álvaro Valenzuela Valdes
deploy: clean build for hf v6
d0501ec
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)