""" Archivo: agent.py Fecha de modificación: 03/06/2026 Autor: Equipo AgroVisión Descripción: Orquestador del agente conversacional (RAG con function calling) sobre Groq/Llama 3. Construye la conversación (sistema + memoria + turno), deja que el modelo decida qué herramienta tipada invocar, ejecuta las herramientas reales (NDVI/clima/densidad) y devuelve la respuesta final con la traza de herramientas usadas. Persiste los turnos (user/assistant) en `chat_messages`. Usa la API OpenAI-compatible de Groq vía httpx (async) — sin SDK extra. Acciones Principales: - `run_agent`: ciclo de tool-calling + memoria + respuesta final. Estructura Interna: - `_system_prompt`: instrucciones + parcelas disponibles. - `_groq_chat`: llamada HTTP a Groq (con/sin herramientas). - `run_agent`: bucle de orquestación. Entradas / Dependencias: - `httpx`; `backend.services.tools`, `backend.db.repositories`. Salidas / Efectos: - Lecturas/escrituras en `chat_messages`; llamadas a Groq y a las herramientas. Ejemplo de Integración: from backend.services.agent import run_agent out = await run_agent(session, "¿cómo va el NDVI de Lote A?", "sess-1", keys, ["Lote A"]) """ from __future__ import annotations import json import httpx from sqlalchemy.ext.asyncio import AsyncSession from backend.db import repositories as repo from backend.services.tools import TOOL_DISPATCH, TOOLS_SCHEMA _GROQ_URL = "https://api.groq.com/openai/v1/chat/completions" _MODEL = "llama-3.3-70b-versatile" _MAX_TOOL_ROUNDS = 4 def _system_prompt(field_names: list[str]) -> str: """Construye el prompt de sistema con las parcelas disponibles.""" parcelas = ", ".join(field_names) if field_names else "(ninguna registrada todavía)" return ( "Eres el asistente agronómico de AgroVisión. Respondes en español, de forma " "concisa y técnica. Dispones de herramientas para consultar el NDVI, el clima y " "la densidad de las parcelas del usuario; úsalas en vez de inventar datos. Si una " "herramienta indica que algo está 'en desarrollo', acláralo. " f"Parcelas disponibles: {parcelas}." ) async def _groq_chat(api_key: str, messages: list[dict], use_tools: bool) -> dict: """Llama al endpoint de chat de Groq (con herramientas si `use_tools`).""" payload: dict = {"model": _MODEL, "messages": messages, "temperature": 0.2} if use_tools: payload["tools"] = TOOLS_SCHEMA payload["tool_choice"] = "auto" async with httpx.AsyncClient() as client: response = await client.post( _GROQ_URL, headers={"Authorization": f"Bearer {api_key}"}, json=payload, timeout=60 ) response.raise_for_status() return response.json() async def run_agent( session: AsyncSession, message: str, session_id: str, api_key: str, field_names: list[str], ) -> dict: """ Ejecuta el agente: memoria + tool-calling + respuesta, y persiste los turnos. Args: session (AsyncSession): Sesión de BD. message (str): Mensaje del usuario. session_id (str): Hilo conversacional. api_key (str): Llave de Groq (efímera). field_names (list[str]): Parcelas disponibles (para el prompt de sistema). Returns: dict: {reply, tool_logs, session_id}. """ await repo.save_chat_message(session, session_id=session_id, role="user", content=message) history = await repo.get_chat_history(session, session_id) messages: list[dict] = [{"role": "system", "content": _system_prompt(field_names)}] messages += [{"role": row.role, "content": row.content} for row in history] tool_logs: list[dict] = [] for _ in range(_MAX_TOOL_ROUNDS): data = await _groq_chat(api_key, messages, use_tools=True) choice = data["choices"][0]["message"] tool_calls = choice.get("tool_calls") if not tool_calls: reply = choice.get("content") or "" await repo.save_chat_message( session, session_id=session_id, role="assistant", content=reply ) return {"reply": reply, "tool_logs": tool_logs, "session_id": session_id} messages.append(choice) # mensaje del asistente con las tool_calls for call in tool_calls: name = call["function"]["name"] try: args = json.loads(call["function"].get("arguments") or "{}") except json.JSONDecodeError: args = {} tool = TOOL_DISPATCH.get(name) result = ( await tool(session, **args) if tool else f"Herramienta desconocida: {name}." ) tool_logs.append({"tool": name, "args": args}) messages.append({"role": "tool", "tool_call_id": call["id"], "content": result}) # Si se agotaron las rondas de herramientas, pide una respuesta final sin tools. data = await _groq_chat(api_key, messages, use_tools=False) reply = data["choices"][0]["message"].get("content") or "(sin respuesta)" await repo.save_chat_message(session, session_id=session_id, role="assistant", content=reply) return {"reply": reply, "tool_logs": tool_logs, "session_id": session_id}