脕lvaro Valenzuela Valdes
Update Gemini SDK to google-genai v2.0.0 (Copilot/VS Code updates)
2be65c0
import hashlib
import json
import httpx
from google import genai
from app.config import settings
from app.schemas.analysis import AnalysisResult, RiskItem, ActionItem, CompanyProfile, Tender
from app.services.report import generate_markdown_report
async def call_gemini(prompt: str, is_json: bool = False) -> str:
if not settings.gemini_api_key:
return ""
try:
generation_config = {
"temperature": 0.2,
"top_p": 0.95,
"top_k": 40,
"max_output_tokens": 8192,
}
if is_json:
generation_config["response_mime_type"] = "application/json"
async with genai.Client(api_key=settings.gemini_api_key).aio as client:
response = await client.models.generate_content(
model="gemini-2.0-flash",
contents=prompt,
config=generation_config,
)
return getattr(response, "text", "") or ""
except Exception as e:
print(f"Error calling Gemini (is_json={is_json}): {e}, trying fallback...")
if settings.groq_api_key:
return await call_groq(prompt, "llama-3.3-70b-versatile")
return await call_featherless(prompt, "Qwen/Qwen2.5-72B-Instruct")
async def call_featherless(prompt: str, model: str = "Qwen/Qwen2.5-72B-Instruct") -> str:
if not settings.featherless_api_key:
return ""
try:
async with httpx.AsyncClient(timeout=60.0) as client:
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.2
}
if "json" in prompt.lower():
payload["response_format"] = {"type": "json_object"}
response = await client.post(
"https://api.featherless.ai/v1/chat/completions",
headers={
"Authorization": f"Bearer {settings.featherless_api_key}",
"Content-Type": "application/json"
},
json=payload
)
if response.status_code != 200:
print(f"Featherless Error ({model}): {response.status_code} - {response.text}")
return ""
data = response.json()
return data["choices"][0]["message"]["content"]
except Exception as e:
print(f"Error calling Featherless ({model}): {e}")
return ""
async def call_groq(prompt: str, model: str = "llama-3.3-70b-versatile") -> str:
if not settings.groq_api_key:
return ""
try:
async with httpx.AsyncClient(timeout=60.0) as client:
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.2
}
if "json" in prompt.lower():
payload["response_format"] = {"type": "json_object"}
response = await client.post(
"https://api.groq.com/openai/v1/chat/completions",
headers={
"Authorization": f"Bearer {settings.groq_api_key}",
"Content-Type": "application/json"
},
json=payload
)
if response.status_code != 200:
print(f"Groq Error ({model}): {response.status_code} - {response.text}")
return ""
data = response.json()
return data["choices"][0]["message"]["content"]
except Exception as e:
print(f"Error calling Groq ({model}): {e}")
return ""
async def call_amd_mi300x(prompt: str, model: str = "Qwen/Qwen2.5-72B-Instruct") -> str:
"""
Direct inference call to an AMD Instinct MI300X node running vLLM or similar ROCm-based server.
"""
if not settings.amd_inference_url:
return ""
try:
async with httpx.AsyncClient(timeout=120.0) as client:
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1
}
if "json" in prompt.lower():
payload["response_format"] = {"type": "json_object"}
response = await client.post(
settings.amd_inference_url.rstrip("/") + "/chat/completions",
headers={
"Authorization": f"Bearer {settings.amd_api_key}" if settings.amd_api_key else "",
"Content-Type": "application/json"
},
json=payload
)
if response.status_code != 200:
print(f"AMD Node Error: {response.status_code} - {response.text}")
return ""
data = response.json()
return data["choices"][0]["message"]["content"]
except Exception as e:
print(f"Critical connection error to AMD Node: {e}")
return ""
async def call_gemini_with_model(prompt: str, model_name: str | None = None, is_json: bool = False) -> str:
model_map = {
"Gemini 2.5 Flash": "gemini",
"DeepSeek-V3 (Featherless)": "deepseek-ai/DeepSeek-V3",
"Qwen-2.5 (Featherless)": "Qwen/Qwen2.5-72B-Instruct",
"Llama-3.3-70B (Groq)": "groq:llama-3.3-70b-versatile",
"Llama-3.1-8B (Groq)": "groq:llama-3.1-8b-instant",
"Llama-3.1-70B (Groq)": "groq:llama-3.1-70b-versatile",
"Mixtral-8x7B (Groq)": "groq:mixtral-8x7b-32768",
"Gemma-2-9B (Featherless)": "google/gemma-2-9b-it",
"Llama-3.1-8B (Featherless)": "meta-llama/Meta-Llama-3.1-8B-Instruct",
"Llama-3.2-11B-Vision (Groq)": "groq:llama-3.2-11b-vision-preview",
"AMD-Instinct (MI300X Local)": "amd:default",
}
model_id = model_map.get(model_name, "gemini")
print(f"DEBUG: Calling LLM with model_name='{model_name}' -> model_id='{model_id}'")
# Check keys
if model_id.startswith("groq:") and not settings.groq_api_key:
print("DEBUG WARNING: GROQ_API_KEY is missing! Falling back to Gemini.")
model_id = "gemini"
if model_id.startswith("amd:") and not settings.amd_inference_url:
print("DEBUG WARNING: AMD_INFERENCE_URL is missing! Falling back to Gemini.")
model_id = "gemini"
if model_id == "gemini":
res = await call_gemini(prompt, is_json=is_json)
if not res and settings.groq_api_key:
print("DEBUG: Gemini failed or returned empty. Trying Groq fallback.")
return await call_groq(prompt, "llama-3.3-70b-versatile")
return res
elif model_id.startswith("groq:"):
# Check if it's a vision call (hacky way for now, but effective)
if "IMAGE_DATA:" in prompt:
parts = prompt.split("IMAGE_DATA:")
text_prompt = parts[0].strip()
image_b64 = parts[1].strip()
res = await call_groq_vision(text_prompt, image_b64, model=model_id[5:])
else:
res = await call_groq(prompt, model=model_id[5:])
if not res and settings.gemini_api_key:
print("DEBUG: Groq failed or returned empty. Trying Gemini fallback.")
return await call_gemini(prompt, is_json=is_json)
return res
elif model_id.startswith("amd:"):
print(f"馃敟 EXECUTING ON AMD INSTINCT MI300X NODE: {settings.amd_inference_url}")
res = await call_amd_mi300x(prompt)
if not res:
print("DEBUG: AMD Node call failed. Falling back to Gemini.")
return await call_gemini(prompt, is_json=is_json)
return res
else:
res = await call_featherless(prompt, model=model_id)
if not res and settings.groq_api_key:
print("DEBUG: Featherless failed. Trying Groq fallback.")
return await call_groq(prompt, "llama-3.3-70b-versatile")
return res
async def call_groq_vision(prompt: str, image_b64: str, model: str = "llama-3.2-11b-vision-preview") -> str:
if not settings.groq_api_key:
return ""
try:
async with httpx.AsyncClient(timeout=60.0) as client:
# Ensure proper data URL format
if not image_b64.startswith("data:image"):
image_b64 = f"data:image/jpeg;base64,{image_b64}"
payload = {
"model": model,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {"url": image_b64}
}
]
}
],
"temperature": 0.2
}
response = await client.post(
"https://api.groq.com/openai/v1/chat/completions",
headers={
"Authorization": f"Bearer {settings.groq_api_key}",
"Content-Type": "application/json"
},
json=payload
)
if response.status_code != 200:
print(f"Groq Vision Error ({model}): {response.status_code} - {response.text}")
return ""
data = response.json()
return data["choices"][0]["message"]["content"]
except Exception as e:
print(f"Error calling Groq Vision ({model}): {e}")
return ""
def _parse_gemini_response(output: str) -> dict | None:
if not output:
return None
# Remove Markdown code blocks if present
clean_output = output.strip()
if clean_output.startswith("```json"):
clean_output = clean_output[7:-3].strip()
elif clean_output.startswith("```"):
clean_output = clean_output[3:-3].strip()
try:
data = json.loads(clean_output)
except Exception as e:
print(f"JSON Parsing Error: {e}\nRaw Output: {output[:200]}...")
return None
if data:
# Handle nesting (LLMs sometimes wrap the result in a key)
if not all(k in data for k in ["fit_score", "decision", "risks"]):
for val in data.values():
if isinstance(val, dict) and any(k in val for k in ["fit_score", "decision", "risks"]):
data = val
break
# Ensure strategic_roadmap is a string
if "strategic_roadmap" in data:
if isinstance(data["strategic_roadmap"], list):
data["strategic_roadmap"] = "\n".join([str(item) for item in data["strategic_roadmap"]])
elif isinstance(data["strategic_roadmap"], dict):
data["strategic_roadmap"] = json.dumps(data["strategic_roadmap"], indent=2, ensure_ascii=False)
# Ensure risks is a list of objects
if "risks" in data and isinstance(data["risks"], list):
new_risks = []
for item in data["risks"]:
if isinstance(item, str):
new_risks.append({"title": item, "severity": "Medium", "explanation": item})
elif isinstance(item, dict):
new_risks.append(item)
data["risks"] = new_risks
# Ensure action_plan is a list of objects
if "action_plan" in data and isinstance(data["action_plan"], list):
new_plan = []
for item in data["action_plan"]:
if isinstance(item, str):
new_plan.append({"task": item, "priority": "Medium", "owner": "Team", "timeline": "TBD"})
elif isinstance(item, dict):
new_plan.append(item)
data["action_plan"] = new_plan
# Ensure fit_score is int
if "fit_score" in data:
try:
data["fit_score"] = int(data["fit_score"])
except:
data["fit_score"] = 0
return data
return None
def generate_mock_analysis(tender: Tender, company: CompanyProfile) -> AnalysisResult:
raw = f"{tender.code}:{tender.name}:{company.name}"
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
score = int(digest[:8], 16) % 41 + 55
return AnalysisResult(
fit_score=score,
decision="Recommended" if score > 75 else "Review Carefully",
executive_summary=f"An谩lisis autom谩tico para {tender.name}. Se observa un encaje t茅cnico razonable.",
key_requirements=["Documentaci贸n legal", "Experiencia t茅cnica", "Garant铆a de seriedad"],
risks=[{"title": "Plazo ajustado", "severity": "Medium", "explanation": "El tiempo de entrega es cr铆tico."}],
compliance_gaps=["Validar boleta de garant铆a"],
action_plan=[{"task": "Revisar bases", "priority": "High", "owner": "Legal", "timeline": "2 d铆as"}],
proposal_draft="Borrador generado autom谩ticamente...",
report_markdown="# Reporte de Licitaci贸n",
audit_log=["Iniciando an谩lisis de respaldo...", "Generando datos mock."]
)
async def generate_analysis(tender: Tender, company: CompanyProfile, document_text: str | None = None, models: dict | None = None) -> AnalysisResult:
chosen = 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_messages = ["馃殌 Launching Multi-Agent Orchestration Pipeline."]
agent_outputs = {}
agent_definitions = {
"legal": "Experto Legal & Cumplimiento: Eval煤a bases administrativas, multas y garant铆as. Pon especial atenci贸n a los ANEXOS de Sustentabilidad y Admisibilidad.",
"tech": "Ingeniero T茅cnico: Eval煤a arquitectura, stack tecnol贸gico y capacidad de ejecuci贸n. Considera si se requieren certificaciones ambientales.",
"risk": "Estratega Comercial: Eval煤a rentabilidad, competencia y riesgos de mercado. Analiza el impacto de los criterios de evaluaci贸n ESG en el puntaje final."
}
for agent_id, role_desc in agent_definitions.items():
model_name = chosen.get(agent_id, "Gemini 2.5 Flash")
audit_messages.append(f"馃 Agent {agent_id.upper()} calling {model_name}...")
agent_prompt = f"""
Act煤a como {role_desc}
Licitaci贸n: {tender.name} ({tender.code})
Empresa: {company.name}
Contexto Adicional: {document_text[:5000] if document_text else 'No adjunto.'}
PROPORCIONA TU AN脕LISIS ESPEC脥FICO (M谩x 200 palabras) EN ESPA脩OL.
"""
res = await call_gemini_with_model(agent_prompt, model_name=model_name)
agent_outputs[agent_id] = res or "An谩lisis no disponible debido a error de conexi贸n."
audit_messages.append("馃 Synthesis phase: Consolidating agent insights...")
synthesis_prompt = f"""
SISTEMA DE CONSENSO ANDESOPS AI
Licitaci贸n: {tender.name}
Resultados de Agentes:
- LEGAL: {agent_outputs.get('legal')}
- TECH: {agent_outputs.get('tech')}
- RISK: {agent_outputs.get('risk')}
Genera el JSON final AnalysisResult con una decisi贸n fundamentada.
RESPONDE SOLO EL JSON.
"""
final_json = await call_gemini(synthesis_prompt, is_json=True)
if not final_json and settings.groq_api_key:
final_json = await call_groq(synthesis_prompt, model="llama-3.3-70b-versatile")
elif not final_json and settings.featherless_api_key:
final_json = await call_featherless(synthesis_prompt, model="Qwen/Qwen2.5-72B-Instruct")
parse_result = _parse_gemini_response(final_json)
if parse_result:
try:
if not parse_result.get("report_markdown"):
parse_result["report_markdown"] = generate_markdown_report(parse_result)
if not parse_result.get("proposal_draft") or len(parse_result["proposal_draft"]) < 100:
audit_messages.append("馃摑 Generating specialized proposal draft...")
parse_result["proposal_draft"] = await generate_proposal_draft(parse_result, company)
result = AnalysisResult(**parse_result)
result.audit_log = audit_messages + (result.audit_log or [])
return result
except Exception as e:
print(f"Validation Error in generate_analysis: {e}")
analysis = generate_mock_analysis(tender, company)
analysis.audit_log = audit_messages + ["鈿狅笍 Synthesis failed, using emergency fallback."]
return analysis
async def generate_proposal_draft(analysis: dict, company: CompanyProfile) -> str:
prompt = f"""
Como experto redactor de propuestas de licitaci贸n, genera un borrador profesional (en Markdown) basado en este an谩lisis t茅cnico:
{analysis.get('executive_summary', 'Analizar bases adjuntas.')}
Perfil de la Empresa: {company.name} - {company.experience}
Requisitos Cr铆ticos a Abordar: {', '.join(analysis.get('key_requirements', []))}
Estructura la propuesta en ESPA脩OL con:
1. Introducci贸n Ejecutiva
2. Resumen de la Soluci贸n T茅cnica
3. Aseguramiento de Cumplimiento (Compliance)
4. Propuesta de Valor Estrat茅gica
"""
return await call_gemini_with_model(prompt, model_name="Llama-3.3-70B (Groq)" if settings.groq_api_key else "Gemini 2.5 Flash")
async def generate_synthetic_tenders(keyword: str) -> list[Tender]:
"""
Generates realistic synthetic tenders with coherent bidding documents (bases)
when official sources are unavailable or empty.
"""
prompt = f"""
Genera 4 licitaciones de Mercado P煤blico CHILE realistas para el rubro: {keyword}
Para cada licitaci贸n, genera un JSON con:
- code: Formato XXXXX-XX-XX26
- name: Nombre profesional
- buyer: Una instituci贸n p煤blica chilena real
- description: UN DOCUMENTO EXTENSO de 'Bases Administrativas y T茅cnicas' (m铆nimo 300 palabras)
que incluya: Objeto de licitaci贸n, Requisitos t茅cnicos, Plazos, Multas y Criterios de Evaluaci贸n.
- status: 'Publicada'
- closing_date: ISO date en 2 semanas
- estimated_amount: Monto en CLP entre 5M y 50M
- region: Una regi贸n de Chile
RESPONDE SOLO EL JSON (Lista de objetos).
"""
res = await call_gemini(prompt, is_json=True)
items = []
try:
data = json.loads(res)
# Handle if LLM wraps in a key
if isinstance(data, dict):
for v in data.values():
if isinstance(v, list):
data = v
break
for i in data:
items.append(Tender(
code=i.get("code", "000-00-00"),
name=i.get("name", "Licitaci贸n Sint茅tica"),
description=i.get("description", "Documento de bases en proceso..."),
buyer=i.get("buyer", "Organismo P煤blico"),
status=i.get("status", "Publicada"),
closing_date=i.get("closing_date", datetime.now().isoformat()),
estimated_amount=float(i.get("estimated_amount", 0)),
source="AndesOps AI - Intelligent Discovery",
region=i.get("region", "Nacional"),
sector="Privado/P煤blico",
items=[],
attachments=[{
"name": "Bases_Tecnicas_y_Administrativas.pdf",
"url": "#synthetic-doc",
"type": "pdf"
}]
))
except Exception as e:
print(f"Error generating synthetic tenders: {e}")
return items