258 lines
8.6 KiB
Python
258 lines
8.6 KiB
Python
"""Task Executor Service - Integrates Task with Agent execution"""
|
|
import logging
|
|
import json
|
|
from typing import List, Dict, Any, Optional, AsyncGenerator
|
|
from datetime import datetime
|
|
|
|
from luxx.services.task import (
|
|
task_service, TaskService, Task, Step, StepStatus, TaskStatus,
|
|
TaskGraph, Planner
|
|
)
|
|
from luxx.services.events import sse_event
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TaskExecutorService:
|
|
"""Integrates Task/Plan with Agent execution loop"""
|
|
|
|
# System prompt for plan generation
|
|
PLANNER_SYSTEM = """You are a task planning assistant. Break down goals into clear, executable steps.
|
|
|
|
For each step, provide:
|
|
- name: Short descriptive name
|
|
- description: What this step accomplishes
|
|
- goal: Specific objective for the agent to achieve (use this as the actual prompt)
|
|
|
|
Consider dependencies between steps. Steps can run in parallel if they don't depend on each other.
|
|
|
|
Output ONLY a JSON array, no other text."""
|
|
|
|
def __init__(
|
|
self,
|
|
task_svc: TaskService = None,
|
|
llm_client = None # LLM client for plan generation
|
|
):
|
|
self.task_service = task_svc or task_service
|
|
self.llm = llm_client
|
|
|
|
async def generate_plan(
|
|
self,
|
|
goal: str,
|
|
context: Dict[str, Any] = None
|
|
) -> List[Dict[str, Any]]:
|
|
"""Generate a plan from goal using LLM"""
|
|
if not self.llm:
|
|
logger.warning("No LLM client configured for planning")
|
|
return []
|
|
|
|
user_msg = f"Goal: {goal}\n\n"
|
|
if context:
|
|
if context.get("available_tools"):
|
|
user_msg += f"Available tools: {', '.join(context['available_tools'])}\n"
|
|
if context.get("constraints"):
|
|
user_msg += f"Constraints: {context['constraints']}\n"
|
|
|
|
messages = [
|
|
{"role": "system", "content": self.PLANNER_SYSTEM},
|
|
{"role": "user", "content": user_msg}
|
|
]
|
|
|
|
try:
|
|
response = await self.llm.sync_call(messages=messages)
|
|
plan_text = response.get("content", "")
|
|
|
|
# Extract JSON
|
|
start = plan_text.find("[")
|
|
end = plan_text.rfind("]") + 1
|
|
if start != -1 and end > start:
|
|
plan_json = plan_text[start:end]
|
|
steps = json.loads(plan_json)
|
|
|
|
# Normalize steps
|
|
normalized = []
|
|
for i, step in enumerate(steps):
|
|
normalized.append({
|
|
"name": step.get("name", f"Step {i+1}"),
|
|
"description": step.get("description", ""),
|
|
"goal": step.get("goal", step.get("description", "")),
|
|
"depends_on": step.get("depends_on", [])
|
|
})
|
|
return normalized
|
|
|
|
except Exception as e:
|
|
logger.error(f"Plan generation failed: {e}")
|
|
|
|
return []
|
|
|
|
async def create_task_with_plan(
|
|
self,
|
|
name: str,
|
|
goal: str,
|
|
description: str = "",
|
|
auto_plan: bool = True,
|
|
context: Dict[str, Any] = None
|
|
) -> Task:
|
|
"""Create a task and optionally auto-generate plan"""
|
|
task = self.task_service.create_task(
|
|
name=name,
|
|
goal=goal,
|
|
description=description
|
|
)
|
|
|
|
if auto_plan:
|
|
# Generate plan using LLM
|
|
steps = await self.generate_plan(goal, context)
|
|
if steps:
|
|
self.task_service.set_steps_from_plan(task.id, steps)
|
|
logger.info(f"Generated {len(steps)} steps for task {task.id}")
|
|
else:
|
|
# Fallback: single step with the goal
|
|
self.task_service.add_steps(task.id, [{
|
|
"name": "Execute Goal",
|
|
"description": description,
|
|
"goal": goal
|
|
}])
|
|
|
|
return self.task_service.get_task(task.id)
|
|
|
|
def get_execution_plan(self, task_id: str) -> Dict[str, Any]:
|
|
"""Get execution plan for a task"""
|
|
task = self.task_service.get_task(task_id)
|
|
if not task:
|
|
return {"error": "Task not found"}
|
|
|
|
graph = self.task_service.build_graph(task_id)
|
|
if not graph:
|
|
return {"error": "Failed to build graph"}
|
|
|
|
sorted_steps = graph.topological_sort()
|
|
parallel_levels = graph.get_parallel_levels()
|
|
ready_steps = graph.get_ready_steps()
|
|
|
|
return {
|
|
"task_id": task_id,
|
|
"task_name": task.name,
|
|
"goal": task.goal,
|
|
"total_steps": len(task.steps),
|
|
"completed_steps": len(task.get_completed_step_ids()),
|
|
"execution_order": [s.id for s in sorted_steps],
|
|
"parallel_levels": [[s.id for s in level] for level in parallel_levels],
|
|
"ready_steps": [s.id for s in ready_steps],
|
|
"can_execute": len(ready_steps) > 0,
|
|
"steps": [s.to_dict() for s in task.steps]
|
|
}
|
|
|
|
|
|
class AgentWithPlan:
|
|
"""Agent execution with built-in task planning"""
|
|
|
|
def __init__(
|
|
self,
|
|
chat_service,
|
|
task_executor: TaskExecutorService = None
|
|
):
|
|
self.chat_service = chat_service
|
|
self.task_executor = task_executor or TaskExecutorService()
|
|
|
|
async def execute_with_plan(
|
|
self,
|
|
goal: str,
|
|
context: Dict[str, Any]
|
|
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
"""Execute goal with automatic planning"""
|
|
# 1. Create task and generate plan
|
|
yield sse_event("planning", {"status": "generating", "goal": goal})
|
|
|
|
task = await self.task_executor.create_task_with_plan(
|
|
name=f"Task: {goal[:50]}...",
|
|
goal=goal,
|
|
context=context
|
|
)
|
|
|
|
if not task.steps:
|
|
yield sse_event("error", {"content": "Failed to generate plan"})
|
|
return
|
|
|
|
yield sse_event("plan_created", {
|
|
"task_id": task.id,
|
|
"steps": [s.to_dict() for s in task.steps]
|
|
})
|
|
|
|
# 2. Execute steps
|
|
graph = self.task_executor.task_service.build_graph(task.id)
|
|
if not graph:
|
|
yield sse_event("error", {"content": "Failed to build execution graph"})
|
|
return
|
|
|
|
# Get execution order
|
|
sorted_steps = graph.topological_sort()
|
|
completed_ids = []
|
|
|
|
for step in sorted_steps:
|
|
yield sse_event("step_start", {
|
|
"task_id": task.id,
|
|
"step_id": step.id,
|
|
"step_name": step.name,
|
|
"step_goal": step.goal,
|
|
"progress": len(completed_ids) + 1,
|
|
"total": len(sorted_steps)
|
|
})
|
|
|
|
# 3. Execute step with Agent
|
|
# For now, execute as a chat message
|
|
step_messages = [
|
|
{"role": "system", "content": f"You are executing step: {step.name}\n\n{step.goal}"},
|
|
{"role": "user", "content": f"Execute: {step.goal}"}
|
|
]
|
|
|
|
step_result = {"messages": [], "tool_calls": []}
|
|
|
|
# Stream response from agent
|
|
async for event in self._stream_step(step_messages, context):
|
|
if event.startswith("data: "):
|
|
data = event[6:]
|
|
yield event
|
|
# Collect results
|
|
if "step_complete" in data or "done" in data:
|
|
step_result["completed"] = True
|
|
|
|
# Mark step complete
|
|
self.task_executor.task_service.update_step_status(
|
|
task.id, step.id, StepStatus.COMPLETED,
|
|
{"result": step_result}
|
|
)
|
|
completed_ids.append(step.id)
|
|
|
|
yield sse_event("step_complete", {
|
|
"task_id": task.id,
|
|
"step_id": step.id,
|
|
"progress": len(completed_ids),
|
|
"total": len(sorted_steps)
|
|
})
|
|
|
|
# 4. Task complete
|
|
self.task_executor.task_service.update_task_status(
|
|
task.id, TaskStatus.COMPLETED
|
|
)
|
|
|
|
yield sse_event("task_complete", {
|
|
"task_id": task.id,
|
|
"completed_steps": len(completed_ids)
|
|
})
|
|
|
|
async def _stream_step(
|
|
self,
|
|
messages: List[Dict],
|
|
context: Dict[str, Any]
|
|
) -> AsyncGenerator[str, None]:
|
|
"""Stream response for a step (placeholder - integrate with ChatService)"""
|
|
# TODO: Integrate with actual ChatService streaming
|
|
yield sse_event("info", {"content": "Step execution not yet integrated"})
|
|
|
|
|
|
# Factory function
|
|
def create_task_executor(llm_client=None) -> TaskExecutorService:
|
|
"""Create TaskExecutorService with optional LLM client"""
|
|
return TaskExecutorService(llm_client=llm_client)
|