diff --git a/luxx/agent/agent.py b/luxx/agent/agent.py new file mode 100644 index 0000000..5491775 --- /dev/null +++ b/luxx/agent/agent.py @@ -0,0 +1,226 @@ +"""Agent module for autonomous task execution""" +import logging +from datetime import datetime +from typing import Dict, List, Optional, Any + +from luxx.agent.task import Agent, Task, TaskStatus, Step, StepStatus +from luxx.utils.helpers import generate_id + +logger = logging.getLogger(__name__) + + +class AgentService: + """Agent service for managing agent instances""" + + def __init__(self): + self._agents: Dict[str, Agent] = {} + + def create_agent( + self, + name: str, + description: str = "", + instructions: str = "", + tools: List[str] = None + ) -> Agent: + """Create a new agent instance""" + agent_id = generate_id("agent") + agent = Agent( + id=agent_id, + name=name, + description=description, + instructions=instructions, + tools=tools or [] + ) + self._agents[agent_id] = agent + logger.info(f"Created agent: {agent_id} - {name}") + return agent + + def get_agent(self, agent_id: str) -> Optional[Agent]: + """Get agent by ID""" + return self._agents.get(agent_id) + + def list_agents(self) -> List[Agent]: + """List all agents""" + return list(self._agents.values()) + + def delete_agent(self, agent_id: str) -> bool: + """Delete agent by ID""" + if agent_id in self._agents: + del self._agents[agent_id] + logger.info(f"Deleted agent: {agent_id}") + return True + return False + + def update_agent( + self, + agent_id: str, + name: str = None, + description: str = None, + instructions: str = None, + tools: List[str] = None + ) -> Optional[Agent]: + """Update agent configuration""" + agent = self._agents.get(agent_id) + if not agent: + return None + + if name is not None: + agent.name = name + if description is not None: + agent.description = description + if instructions is not None: + agent.instructions = instructions + if tools is not None: + agent.tools = tools + + agent.updated_at = datetime.now() + return agent + + +class TaskService: + """Task service for managing tasks""" + + def __init__(self, agent_service: AgentService): + self._tasks: Dict[str, Task] = {} + self._agent_service = agent_service + + def create_task( + self, + agent_id: str, + name: str, + goal: str, + description: str = "", + parent_task_id: str = None, + steps: List[Dict[str, Any]] = None + ) -> Optional[Task]: + """Create task for agent, optionally as subtask""" + agent = self._agent_service.get_agent(agent_id) + if not agent: + return None + + task_id = generate_id("task") + task = Task( + id=task_id, + name=name, + description=description, + goal=goal + ) + + # Add steps + if steps: + for step_data in steps: + step = Step( + id=generate_id("step"), + name=step_data.get("name", ""), + description=step_data.get("description", "") + ) + task.steps.append(step) + + # Add to parent task or agent + if parent_task_id: + parent_task = self._tasks.get(parent_task_id) + if parent_task: + task.parent_id = parent_task_id + parent_task.subtasks.append(task) + else: + agent.current_task = task + + self._tasks[task_id] = task + + logger.info(f"Created task: {task_id} for agent: {agent_id}") + return task + + def get_task(self, task_id: str) -> Optional[Task]: + """Get task by ID""" + return self._tasks.get(task_id) + + def list_tasks(self, agent_id: str = None) -> List[Task]: + """List tasks, optionally filtered by agent""" + if agent_id: + agent = self._agent_service.get_agent(agent_id) + if agent and agent.current_task: + return [agent.current_task] + return [] + return list(self._tasks.values()) + + def update_task_status( + self, + task_id: str, + status: TaskStatus, + result: Any = None + ) -> Optional[Task]: + """Update task status""" + task = self._tasks.get(task_id) + if not task: + return None + + task.status = status + task.result = result + task.updated_at = datetime.now() + return task + + def add_step( + self, + task_id: str, + step_name: str, + step_description: str = "" + ) -> Optional[Step]: + """Add step to task""" + task = self._tasks.get(task_id) + if not task: + return None + + step = Step( + id=generate_id("step"), + name=step_name, + description=step_description + ) + task.steps.append(step) + task.updated_at = datetime.now() + return step + + def add_subtask( + self, + parent_task_id: str, + name: str, + goal: str, + description: str = "" + ) -> Optional[Task]: + """Add subtask to parent task""" + parent_task = self._tasks.get(parent_task_id) + if not parent_task: + return None + + subtask_id = generate_id("task") + subtask = Task( + id=subtask_id, + name=name, + description=description, + goal=goal + ) + subtask.parent_id = parent_task_id + parent_task.subtasks.append(subtask) + self._tasks[subtask_id] = subtask + parent_task.updated_at = datetime.now() + + return subtask + + def delete_task(self, task_id: str) -> bool: + """Delete task""" + task = self._tasks.get(task_id) + if not task: + return False + + # Remove from parent's subtasks if has parent + if hasattr(task, 'parent_id') and task.parent_id: + parent = self._tasks.get(task.parent_id) + if parent: + parent.subtasks = [t for t in parent.subtasks if t.id != task_id] + + del self._tasks[task_id] + return True + + +# Global service instances +agent_service = AgentService() +task_service = TaskService(agent_service) diff --git a/luxx/agent/task.py b/luxx/agent/task.py new file mode 100644 index 0000000..4f9f530 --- /dev/null +++ b/luxx/agent/task.py @@ -0,0 +1,114 @@ +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from typing import List, Optional, Dict, Any + + +class TaskStatus(Enum): + """Task status enum""" + PENDING = "pending" + READY = "ready" + RUNNING = "running" + BLOCK = "block" + TERMINATED = "terminated" + + +class StepStatus(Enum): + """Step status enum""" + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + SKIPPED = "skipped" + + +@dataclass +class Step: + """Task step""" + id: str + name: str + description: str = "" + status: StepStatus = StepStatus.PENDING + result: Optional[Dict[str, Any]] = None + created_at: datetime = field(default_factory=datetime.now) + updated_at: datetime = field(default_factory=datetime.now) + + +@dataclass +class Task: + """Task with nested subtasks""" + id: str + name: str + description: str = "" + goal: str = "" + status: TaskStatus = TaskStatus.PENDING + steps: List[Step] = field(default_factory=list) + subtasks: List["Task"] = field(default_factory=list) + result: Optional[Dict[str, Any]] = None + created_at: datetime = field(default_factory=datetime.now) + updated_at: datetime = field(default_factory=datetime.now) + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary""" + return { + "id": self.id, + "name": self.name, + "description": self.description, + "goal": self.goal, + "status": self.status.value, + "steps": [s.to_dict() for s in self.steps], + "subtasks": [t.to_dict() for t in self.subtasks], + "result": self.result, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None + } + + +@dataclass +class Agent: + """Agent""" + id: str + name: str + description: str = "" + instructions: str = "" + tools: List[str] = field(default_factory=list) + current_task: Optional[Task] = None + status: str = "idle" + result: Optional[Dict[str, Any]] = None + error: Optional[str] = None + created_at: datetime = field(default_factory=datetime.now) + updated_at: datetime = field(default_factory=datetime.now) + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary""" + return { + "id": self.id, + "name": self.name, + "description": self.description, + "instructions": self.instructions, + "tools": self.tools, + "current_task": self.current_task.to_dict() if self.current_task else None, + "status": self.status, + "result": self.result, + "error": self.error, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None + } + + +# Step to_dict method +def step_to_dict(step: Step) -> Dict[str, Any]: + """Convert step to dictionary""" + return { + "id": step.id, + "name": step.name, + "description": step.description, + "status": step.status.value, + "result": step.result, + "created_at": step.created_at.isoformat() if step.created_at else None, + "updated_at": step.updated_at.isoformat() if step.updated_at else None + } + + +# Patch Step.to_dict +Step.to_dict = step_to_dict diff --git a/luxx/routes/agent.py b/luxx/routes/agent.py new file mode 100644 index 0000000..00e08d2 --- /dev/null +++ b/luxx/routes/agent.py @@ -0,0 +1,311 @@ +"""Agent routes module""" +from typing import List, Optional, Dict, Any +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel + +from luxx.agent.agent import agent_service, task_service +from luxx.agent.task import Agent, Task, TaskStatus, StepStatus +from luxx.utils.helpers import success_response, error_response + + +router = APIRouter(prefix="/agent", tags=["Agent"]) + + +# ==================== Request Models ==================== + +class CreateAgentRequest(BaseModel): + """Create agent request""" + name: str + description: str = "" + instructions: str = "" + tools: List[str] = [] + + +class UpdateAgentRequest(BaseModel): + """Update agent request""" + name: Optional[str] = None + description: Optional[str] = None + instructions: Optional[str] = None + tools: Optional[List[str]] = None + + +class TaskStep(BaseModel): + """Task step""" + name: str + description: str = "" + + +class CreateTaskRequest(BaseModel): + """Create task request""" + name: str + goal: str + description: str = "" + steps: List[TaskStep] = [] + + +class CreateSubtaskRequest(BaseModel): + """Create subtask request""" + name: str + goal: str + description: str = "" + + +class UpdateTaskStatusRequest(BaseModel): + """Update task status request""" + status: str + + +# ==================== Agent Endpoints ==================== + +@router.post("/agents", response_model=dict) +def create_agent(request: CreateAgentRequest): + """Create a new agent instance""" + agent = agent_service.create_agent( + name=request.name, + description=request.description, + instructions=request.instructions, + tools=request.tools + ) + return success_response( + data=agent.to_dict(), + message="Agent created successfully" + ) + + +@router.get("/agents", response_model=dict) +def list_agents(): + """List all agents""" + agents = agent_service.list_agents() + return success_response( + data={ + "agents": [a.to_dict() for a in agents], + "total": len(agents) + } + ) + + +@router.get("/agents/{agent_id}", response_model=dict) +def get_agent(agent_id: str): + """Get agent details""" + agent = agent_service.get_agent(agent_id) + if not agent: + return error_response("Agent not found", 404) + return success_response(data=agent.to_dict()) + + +@router.put("/agents/{agent_id}", response_model=dict) +def update_agent(agent_id: str, request: UpdateAgentRequest): + """Update agent configuration""" + agent = agent_service.update_agent( + agent_id=agent_id, + name=request.name, + description=request.description, + instructions=request.instructions, + tools=request.tools + ) + if not agent: + return error_response("Agent not found", 404) + return success_response( + data=agent.to_dict(), + message="Agent updated successfully" + ) + + +@router.delete("/agents/{agent_id}", response_model=dict) +def delete_agent(agent_id: str): + """Delete agent instance""" + if agent_service.delete_agent(agent_id): + return success_response(message="Agent deleted successfully") + return error_response("Agent not found", 404) + + +# ==================== Task Endpoints ==================== + +@router.post("/agents/{agent_id}/tasks", response_model=dict) +def create_task(agent_id: str, request: CreateTaskRequest): + """Create task for agent""" + task = task_service.create_task( + agent_id=agent_id, + name=request.name, + goal=request.goal, + description=request.description, + steps=[s.dict() for s in request.steps] if request.steps else None + ) + if not task: + return error_response("Agent not found", 404) + return success_response( + data=task.to_dict(), + message="Task created successfully" + ) + + +@router.get("/agents/{agent_id}/tasks", response_model=dict) +def get_current_task(agent_id: str): + """Get agent's current task""" + tasks = task_service.list_tasks(agent_id=agent_id) + if not tasks: + return success_response( + data={"task": None}, + message="No task found" + ) + return success_response(data={"task": tasks[0].to_dict()}) + + +@router.get("/tasks", response_model=dict) +def list_tasks(agent_id: str = None): + """List all tasks, optionally filtered by agent""" + tasks = task_service.list_tasks(agent_id=agent_id) + return success_response( + data={ + "tasks": [t.to_dict() for t in tasks], + "total": len(tasks) + } + ) + + +@router.get("/tasks/{task_id}", response_model=dict) +def get_task(task_id: str): + """Get task details""" + task = task_service.get_task(task_id) + if not task: + return error_response("Task not found", 404) + return success_response(data=task.to_dict()) + + +@router.put("/tasks/{task_id}/status", response_model=dict) +def update_task_status(task_id: str, request: UpdateTaskStatusRequest): + """Update task status""" + try: + task_status = TaskStatus(request.status) + except ValueError: + valid_statuses = [s.value for s in TaskStatus] + return error_response(f"Invalid status: {request.status}. Valid: {valid_statuses}", 400) + + task = task_service.update_task_status(task_id, task_status) + if not task: + return error_response("Task not found", 404) + + return success_response( + data=task.to_dict(), + message="Task status updated" + ) + + +@router.delete("/tasks/{task_id}", response_model=dict) +def delete_task(task_id: str): + """Delete task""" + if task_service.delete_task(task_id): + return success_response(message="Task deleted successfully") + return error_response("Task not found", 404) + + +# ==================== Step Endpoints ==================== + +@router.post("/tasks/{task_id}/steps", response_model=dict) +def add_step(task_id: str, name: str, description: str = ""): + """Add step to task""" + step = task_service.add_step(task_id, name, description) + if not step: + return error_response("Task not found", 404) + + return success_response( + data=step.to_dict(), + message="Step added successfully" + ) + + +@router.put("/tasks/{task_id}/steps/{step_id}/status", response_model=dict) +def update_step_status(task_id: str, step_id: str, status: str): + """Update step status""" + try: + step_status = StepStatus(status) + except ValueError: + valid_statuses = [s.value for s in StepStatus] + return error_response(f"Invalid status: {status}. Valid: {valid_statuses}", 400) + + task = task_service.get_task(task_id) + if not task: + return error_response("Task not found", 404) + + for step in task.steps: + if step.id == step_id: + step.status = step_status + step.updated_at = __import__("datetime").datetime.now() + return success_response( + data=step.to_dict(), + message="Step status updated" + ) + + return error_response("Step not found", 404) + + +# ==================== Subtask Endpoints ==================== + +@router.post("/tasks/{task_id}/subtasks", response_model=dict) +def create_subtask(task_id: str, request: CreateSubtaskRequest): + """Add subtask to parent task""" + subtask = task_service.add_subtask( + parent_task_id=task_id, + name=request.name, + goal=request.goal, + description=request.description + ) + if not subtask: + return error_response("Parent task not found", 404) + return success_response( + data=subtask.to_dict(), + message="Subtask created successfully" + ) + + +@router.get("/tasks/{task_id}/subtasks", response_model=dict) +def list_subtasks(task_id: str): + """List subtasks of a task""" + task = task_service.get_task(task_id) + if not task: + return error_response("Task not found", 404) + + return success_response( + data={ + "subtasks": [t.to_dict() for t in task.subtasks], + "total": len(task.subtasks) + } + ) + + +# ==================== Execution Endpoints ==================== + +@router.post("/agents/{agent_id}/execute", response_model=dict) +def execute_agent(agent_id: str, goal: str): + """Trigger agent to execute a goal""" + agent = agent_service.get_agent(agent_id) + if not agent: + return error_response("Agent not found", 404) + + if agent.status == "executing": + return error_response("Agent is already executing", 400) + + # Update agent status + agent.status = "executing" + agent.updated_at = __import__("datetime").datetime.now() + + # Create task + task = task_service.create_task( + agent_id=agent_id, + name=f"Task: {goal[:50]}...", + goal=goal, + description=f"Auto-generated task for goal: {goal}" + ) + + if not task: + return error_response("Failed to create task", 500) + + return success_response( + data={ + "agent_id": agent_id, + "task_id": task.id, + "status": "executing", + "message": "Agent execution started" + }, + message="Execution started" + ) diff --git a/luxx/services/chat.py b/luxx/services/chat.py index f3ddf97..c264443 100644 --- a/luxx/services/chat.py +++ b/luxx/services/chat.py @@ -2,17 +2,17 @@ import json import uuid import logging -from typing import List, Dict, Any, AsyncGenerator, Optional -from luxx.models import Conversation, Message +from typing import List, Dict,AsyncGenerator +from luxx.models import Conversation, Message, LLMProvider from luxx.tools.executor import ToolExecutor from luxx.tools.core import registry from luxx.services.llm_client import LLMClient -from luxx.config import config +from luxx.database import SessionLocal logger = logging.getLogger(__name__) # Maximum iterations to prevent infinite loops -MAX_ITERATIONS = 10 +MAX_ITERATIONS = 20 def _sse_event(event: str, data: dict) -> str: @@ -24,8 +24,6 @@ def get_llm_client(conversation: Conversation = None): """Get LLM client, optionally using conversation's provider. Returns (client, max_tokens)""" max_tokens = None if conversation and conversation.provider_id: - from luxx.models import LLMProvider - from luxx.database import SessionLocal db = SessionLocal() try: provider = db.query(LLMProvider).filter(LLMProvider.id == conversation.provider_id).first() @@ -57,8 +55,6 @@ class ChatService: include_system: bool = True ) -> List[Dict[str, str]]: """Build message list""" - from luxx.database import SessionLocal - from luxx.models import Message messages = [] @@ -148,7 +144,7 @@ class ChatService: text_step_id = None text_step_idx = None - for iteration in range(MAX_ITERATIONS): + for _ in range(MAX_ITERATIONS): # Stream from LLM full_content = "" full_thinking = "" @@ -381,9 +377,8 @@ class ChatService: # No tool calls - final iteration, save message msg_id = str(uuid.uuid4()) - # 使用 API 返回的真实 completion_tokens,如果 API 没返回则降级使用估算值 - actual_token_count = total_usage.get("completion_tokens", 0) or len(full_content) // 4 - logger.info(f"[TOKEN] total_usage: {total_usage}, actual_token_count: {actual_token_count}") + actual_token_count = total_usage.get("completion_tokens", 0) + logger.info(f"total_usage: {total_usage}") self._save_message( conversation.id, @@ -433,8 +428,7 @@ class ChatService: usage: dict = None ): """Save the assistant message to database.""" - from luxx.database import SessionLocal - from luxx.models import Message + content_json = { "text": full_content,