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)