"""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)