From 56c8a9c0667f49a8df76c830b004f670a33faf49 Mon Sep 17 00:00:00 2001 From: Jordan Diaz Date: Fri, 3 Apr 2026 20:48:28 +0000 Subject: [PATCH] Planner: respuesta directa para saludos y preguntas simples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El planner ahora puede devolver direct_response en vez de un plan cuando el mensaje no requiere herramientas (saludos, preguntas generales, conversación casual). - planner.py: prompt actualizado con formato direct_response - engine.py: si planner devuelve string, emitir como texto y completar sin ejecutar steps "hola" → "¡Hola! ¿En qué puedo ayudarte hoy?" (0 steps, 0 tools) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/orchestrator/agents/planner.py | 39 ++++++++++++++-------- src/orchestrator/engine.py | 52 ++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/orchestrator/agents/planner.py b/src/orchestrator/agents/planner.py index 8ed46a9..e94a4df 100644 --- a/src/orchestrator/agents/planner.py +++ b/src/orchestrator/agents/planner.py @@ -12,16 +12,21 @@ from .base import BaseAgent logger = logging.getLogger(__name__) -PLANNER_SYSTEM_PROMPT = """Eres un Agente Planificador. Tu rol es descomponer un objetivo en un plan de ejecución estructurado. +PLANNER_SYSTEM_PROMPT = """Eres un Agente Planificador de Acai CMS. Tu rol es analizar el mensaje del usuario y decidir cómo responder. ## Instrucciones -- Analiza el objetivo y divídelo en pasos concretos y ordenados. -- Cada paso debe ser ejecutable de forma independiente por un agente especializado. -- Asigna cada paso al rol de agente apropiado: coder, collector o reviewer. +- Si el mensaje es un saludo, pregunta general, conversación casual o no requiere herramientas → devuelve una respuesta directa. +- Si el mensaje requiere acción (crear módulos, editar contenido, explorar web, consultar datos) → genera un plan de ejecución. - Responde SIEMPRE en español. ## Formato de salida -Devuelve SOLO un objeto JSON: + +### Para respuestas directas (saludos, preguntas simples): +{ + "direct_response": "Tu respuesta aquí. Sé amable y conciso." +} + +### Para tareas que requieren herramientas: { "plan": [ {"description": "descripción del paso", "agent_role": "coder|collector|reviewer"}, @@ -31,7 +36,7 @@ Devuelve SOLO un objeto JSON: "facts": ["hechos establecidos del análisis"] } -NO incluyas comentarios fuera del JSON.""" +Devuelve SOLO el objeto JSON, sin comentarios fuera.""" def create_planner_profile() -> AgentProfile: @@ -55,26 +60,34 @@ def create_planner_profile() -> AgentProfile: class PlannerAgent(BaseAgent): """Generates execution plans from objectives.""" - async def plan(self, session: SessionState) -> tuple[list[TaskStep], dict[str, int]]: - """Generate a plan and return (TaskSteps, usage).""" + async def plan(self, session: SessionState) -> tuple[list[TaskStep] | str, dict[str, int]]: + """Generate a plan or a direct response. + + Returns: + (steps, usage) if plan needed + (direct_response_string, usage) if no plan needed + """ result = await self.execute(session, max_steps=1) usage = result.get("usage", {"input_tokens": 0, "output_tokens": 0}) content = result["content"].strip() - # Parse the JSON plan from the model output + # Parse the JSON from the model output try: - # Try to extract JSON from the content json_str = content if "```" in content: - # Extract from code block start = content.find("{") end = content.rfind("}") + 1 if start >= 0 and end > start: json_str = content[start:end] parsed = json.loads(json_str) - steps: list[TaskStep] = [] + # Check for direct response (no plan needed) + if "direct_response" in parsed: + return parsed["direct_response"], usage + + # Build plan steps + steps: list[TaskStep] = [] for item in parsed.get("plan", []): steps.append( TaskStep( @@ -84,7 +97,6 @@ class PlannerAgent(BaseAgent): ) ) - # Extract constraints and facts into task state if session.current_task: session.current_task.constraints.extend( parsed.get("constraints", []) @@ -97,7 +109,6 @@ class PlannerAgent(BaseAgent): except (json.JSONDecodeError, KeyError) as e: logger.warning("Failed to parse planner output: %s", e) - # Fallback: single step with the full objective return [ TaskStep( description=session.current_task.objective diff --git a/src/orchestrator/engine.py b/src/orchestrator/engine.py index b89ffa8..78cbb0f 100644 --- a/src/orchestrator/engine.py +++ b/src/orchestrator/engine.py @@ -118,8 +118,54 @@ class OrchestratorEngine: planner_usage: dict[str, int] = {"input_tokens": 0, "output_tokens": 0} try: planner = self._create_agent(AgentRole.PLANNER) - plan_steps, planner_usage = await planner.plan(session) - task.plan = plan_steps + plan_result, planner_usage = await planner.plan(session) + + # Direct response — no plan needed (saludo, pregunta simple) + if isinstance(plan_result, str): + logger.info("Planner returned direct response for task %s", task.task_id) + task.status = TaskStatus.COMPLETED + session.complete_task() + + # Emit as text streaming for the frontend + await self.sse.emit( + EventType.AGENT_DELTA, + {"agent": "planner", "delta": plan_result, "step": 0}, + session_id=session.session_id, + ) + + cost_usd = ( + (planner_usage.get("input_tokens", 0) / 1_000_000) * settings.cost_per_1m_input + + (planner_usage.get("output_tokens", 0) / 1_000_000) * settings.cost_per_1m_output + ) + + await self.sse.emit( + EventType.EXECUTION_COMPLETED, + { + "session_id": session.session_id, + "task_id": task.task_id, + "steps_completed": 0, + "steps_failed": [], + "status": "completed", + "usage": planner_usage, + "total_cost_usd": round(cost_usd, 6), + }, + session_id=session.session_id, + ) + + return { + "session_id": session.session_id, + "task_id": task.task_id, + "content": plan_result, + "steps_completed": 0, + "steps_failed": [], + "artifacts_count": 0, + "review": "", + "status": "completed", + "usage": planner_usage, + "total_cost_usd": round(cost_usd, 6), + } + + task.plan = plan_result task.status = TaskStatus.EXECUTING except Exception as e: logger.error("Planning failed: %s", e) @@ -134,7 +180,7 @@ class OrchestratorEngine: logger.info( "Plan created with %d steps for task %s", - len(plan_steps), + len(plan_result), task.task_id, )