Planner: respuesta directa para saludos y preguntas simples

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) <noreply@anthropic.com>
This commit is contained in:
Jordan Diaz
2026-04-03 20:48:28 +00:00
parent df7dfbc280
commit 56c8a9c066
2 changed files with 74 additions and 17 deletions

View File

@@ -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

View File

@@ -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,
)