diff --git a/backend/app/agents/memory/__init__.py b/backend/app/agents/memory/__init__.py new file mode 100644 index 0000000..cb2951e --- /dev/null +++ b/backend/app/agents/memory/__init__.py @@ -0,0 +1,20 @@ +""" +Memory System Package +===================== + +Memory management for agents including conversation, +vector, and session memory. +""" + +from app.agents.memory.memory_manager import MemoryManager, get_memory_manager +from app.agents.memory.conversation_memory import ConversationMemory +from app.agents.memory.vector_memory import VectorMemory +from app.agents.memory.session_memory import SessionMemory + +__all__ = [ + "MemoryManager", + "get_memory_manager", + "ConversationMemory", + "VectorMemory", + "SessionMemory", +] diff --git a/backend/app/agents/orchestrator/memory/conversation.py b/backend/app/agents/memory/conversation_memory.py similarity index 100% rename from backend/app/agents/orchestrator/memory/conversation.py rename to backend/app/agents/memory/conversation_memory.py diff --git a/backend/app/agents/orchestrator/memory/manager.py b/backend/app/agents/memory/memory_manager.py similarity index 97% rename from backend/app/agents/orchestrator/memory/manager.py rename to backend/app/agents/memory/memory_manager.py index 3dc8133..9dfe804 100644 --- a/backend/app/agents/orchestrator/memory/manager.py +++ b/backend/app/agents/memory/memory_manager.py @@ -9,9 +9,9 @@ from typing import Any, Dict, List, Optional from uuid import UUID -from app.agents.orchestrator.memory.conversation import ConversationMemory -from app.agents.orchestrator.memory.vector import VectorMemory -from app.agents.orchestrator.memory.session import SessionMemory +from app.agents.memory.conversation_memory import ConversationMemory +from app.agents.memory.vector_memory import VectorMemory +from app.agents.memory.session_memory import SessionMemory from app.agents.orchestrator.config import get_orchestrator_config, MemoryConfig from app.agents.orchestrator.exceptions import MemoryError diff --git a/backend/app/agents/orchestrator/memory/session.py b/backend/app/agents/memory/session_memory.py similarity index 100% rename from backend/app/agents/orchestrator/memory/session.py rename to backend/app/agents/memory/session_memory.py diff --git a/backend/app/agents/orchestrator/memory/vector.py b/backend/app/agents/memory/vector_memory.py similarity index 100% rename from backend/app/agents/orchestrator/memory/vector.py rename to backend/app/agents/memory/vector_memory.py diff --git a/backend/app/agents/orchestrator/__init__.py b/backend/app/agents/orchestrator/__init__.py index 6bdd92a..aadfb25 100644 --- a/backend/app/agents/orchestrator/__init__.py +++ b/backend/app/agents/orchestrator/__init__.py @@ -13,7 +13,7 @@ - Security features (prompt injection protection, permissions) """ -from app.agents.orchestrator.core import AgentOrchestrator, get_orchestrator, ExecutionResult +from app.agents.orchestrator.orchestrator import AgentOrchestrator, get_orchestrator, ExecutionResult from app.agents.orchestrator.config import OrchestratorConfig, get_orchestrator_config from app.agents.orchestrator.security import SecurityGuard, get_security_guard from app.agents.orchestrator.exceptions import ( diff --git a/backend/app/agents/orchestrator/core.py b/backend/app/agents/orchestrator/core.py deleted file mode 100644 index b5af3fc..0000000 --- a/backend/app/agents/orchestrator/core.py +++ /dev/null @@ -1,586 +0,0 @@ -""" -Agent Orchestrator Core -======================= - -Main orchestrator for dynamic agent execution using LangGraph. -""" - -import logging -import time -import uuid -from typing import Any, Dict, List, Optional, AsyncIterator -from uuid import UUID as UUIDType - -from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, BaseMessage -from langgraph.graph import StateGraph, START, END -from langgraph.prebuilt import ToolNode, tools_condition -from langgraph.checkpoint.memory import MemorySaver -from pydantic import BaseModel - -from app.agents.orchestrator.config import get_orchestrator_config, OrchestratorConfig -from app.agents.orchestrator.llm import LLMRouter, get_llm_router, LLMResponse -from app.agents.orchestrator.tools import ToolRegistry, get_tool_registry -from app.agents.orchestrator.memory import MemoryManager, get_memory_manager -from app.agents.orchestrator.exceptions import ( - OrchestratorError, - AgentNotFoundError, - PromptInjectionError, - PermissionDeniedError, - GraphExecutionError, -) -from app.agents.orchestrator.security import SecurityGuard, get_security_guard -from app.services.mcp_client import MCPClient, MCPTool - -logger = logging.getLogger(__name__) - - -class AgentState(BaseModel): - """State for agent execution graph.""" - - messages: List[BaseMessage] = [] - agent_id: int = 0 - thread_id: str = "" - current_step: str = "start" - tool_results: List[Dict[str, Any]] = [] - iteration: int = 0 - max_iterations: int = 10 - should_stop: bool = False - - -class ExecutionResult(BaseModel): - """Result of agent execution.""" - - response: str - tool_calls: List[Dict[str, Any]] = [] - tokens_input: int = 0 - tokens_output: int = 0 - tokens_total: int = 0 - duration_ms: int = 0 - thread_id: str = "" - steps: List[str] = [] - - -class AgentOrchestrator: - """ - Main orchestrator for dynamic agent execution. - - Creates and manages LangGraph-powered agents with: - - Dynamic LLM provider routing - - Tool loading and execution - - Memory management - - Security checks - """ - - def __init__( - self, - config: Optional[OrchestratorConfig] = None, - llm_router: Optional[LLMRouter] = None, - tool_registry: Optional[ToolRegistry] = None, - memory_manager: Optional[MemoryManager] = None, - security_guard: Optional["SecurityGuard"] = None, - ): - """ - Initialize the orchestrator. - - Args: - config: Orchestrator configuration - llm_router: LLM router instance - tool_registry: Tool registry instance - memory_manager: Memory manager instance - security_guard: Security guard instance - """ - self.config = config or get_orchestrator_config() - self.llm_router = llm_router or get_llm_router() - self.tool_registry = tool_registry or get_tool_registry() - self.memory_manager = memory_manager or get_memory_manager() - self.security_guard = security_guard or get_security_guard() - - # Checkpointer for state persistence - self._checkpointer = MemorySaver() - - # Cache for compiled graphs - self._graphs: Dict[int, StateGraph] = {} - - async def run( - self, - agent_config: Dict[str, Any], - message: str, - thread_id: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, - ) -> ExecutionResult: - """ - Run an agent with a message. - - Args: - agent_config: Agent configuration dictionary - message: User message - thread_id: Optional thread ID for context - metadata: Optional execution metadata - - Returns: - ExecutionResult with response and metrics - """ - start_time = time.time() - agent_id = agent_config.get("id", 0) - thread_id = thread_id or str(uuid.uuid4()) - steps = [] - - try: - # Security check on input - if not self.security_guard.validate_input(message, agent_config): - raise PromptInjectionError() - - steps.append("security_check") - - # Build system prompt - system_prompt = await self._build_system_prompt(agent_config, thread_id) - steps.append("build_prompt") - - # Get conversation history - history = await self.memory_manager.get_conversation_history( - agent_id, thread_id, as_langchain=True - ) - steps.append("load_history") - - # Build messages - messages = [SystemMessage(content=system_prompt)] - messages.extend(history) - messages.append(HumanMessage(content=message)) - - # Get LLM - provider = agent_config.get("llm_provider", "openai") - model = agent_config.get("model_name", "gpt-4o") - llm = self.llm_router.get_langchain_model(provider, model) - steps.append("load_llm") - - # Load tools - tool_configs = agent_config.get("tools", []) - tool_names = [ - t.get("name") if isinstance(t, dict) else t - for t in tool_configs - ] - permissions = agent_config.get("permissions", {}) - # Load MCP Tools - mcp_servers = agent_config.get("mcp_servers", []) - if agent_config.get("mcp_enabled", False) and mcp_servers: - for server in mcp_servers: - try: - # server is dict with server_url, auth_credentials, etc. - client = MCPClient( - server_url=server.get("server_url"), - auth_token=server.get("encrypted_auth") # Decryption happens in service/model usually, assume clear or handled - ) - # Fetch tools (with short timeout) - mcp_tools = await client.list_tools() - self.tool_registry.register_mcp_tools(client, mcp_tools) - - # Add to tool_names/allowed_tools if we want strict permissioning - # For now, we allow all discovered MCP tools if MCP is enabled - for t in mcp_tools: - tool_names.append(t.name) - - except Exception as e: - logger.warning(f"Failed to load MCP tools from server: {e}") - - allowed_tools = self.tool_registry.validate_tools_for_agent( - tool_names, permissions - ) - langchain_tools = self.tool_registry.get_langchain_tools(allowed_tools) - steps.append("load_tools") - - # Bind tools to LLM - if langchain_tools: - llm_with_tools = llm.bind_tools(langchain_tools) - else: - llm_with_tools = llm - - # Determine mode - action_mode = agent_config.get("action_mode_enabled", False) - - # Create and run graph - graph = self._build_graph(llm_with_tools, langchain_tools, action_mode) - steps.append("build_graph") - - # Execute - config = { - "configurable": { - "thread_id": thread_id, - }, - "recursion_limit": agent_config.get("max_steps", 10) + 1 - } - - # Initial state - state = { - "messages": messages, - } - - # Run the graph - result = await graph.ainvoke(state, config) - steps.append("execute_graph") - - # Extract response - final_messages = result.get("messages", []) - response_content = "" - tool_calls = [] - - for msg in reversed(final_messages): - if isinstance(msg, AIMessage): - response_content = msg.content if isinstance(msg.content, str) else "" - if hasattr(msg, "tool_calls") and msg.tool_calls: - tool_calls = [ - {"name": tc.get("name", ""), "args": tc.get("args", {})} - for tc in msg.tool_calls - ] - break - - # Store in conversation memory - await self.memory_manager.add_to_conversation( - agent_id, thread_id, "user", message - ) - await self.memory_manager.add_to_conversation( - agent_id, thread_id, "assistant", response_content - ) - steps.append("store_memory") - - duration_ms = int((time.time() - start_time) * 1000) - - return ExecutionResult( - response=response_content, - tool_calls=tool_calls, - duration_ms=duration_ms, - thread_id=thread_id, - steps=steps, - ) - - except PromptInjectionError: - raise - except Exception as e: - logger.error(f"Agent execution error: {e}", exc_info=True) - raise GraphExecutionError("run", str(e)) - - async def stream( - self, - agent_config: Dict[str, Any], - message: str, - thread_id: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, - ) -> AsyncIterator[Dict[str, Any]]: - """ - Stream an agent response. - - Args: - agent_config: Agent configuration dictionary - message: User message - thread_id: Optional thread ID - metadata: Optional metadata - - Yields: - Stream chunks with type and content - """ - agent_id = agent_config.get("id", 0) - thread_id = thread_id or str(uuid.uuid4()) - - try: - # Security check - if not self.security_guard.validate_input(message, agent_config): - yield {"type": "error", "error": "Prompt injection detected"} - return - - yield {"type": "step", "step": "Starting agent..."} - - # Build system prompt - system_prompt = await self._build_system_prompt(agent_config, thread_id) - - # Get conversation history - history = await self.memory_manager.get_conversation_history( - agent_id, thread_id, as_langchain=True - ) - - # Build messages - messages = [SystemMessage(content=system_prompt)] - messages.extend(history) - messages.append(HumanMessage(content=message)) - - # Get LLM - provider = agent_config.get("llm_provider", "openai") - model = agent_config.get("model_name", "gpt-4o") - llm = self.llm_router.get_langchain_model(provider, model) - - # Load tools - tool_configs = agent_config.get("tools", []) - tool_names = [ - t.get("name") if isinstance(t, dict) else t - for t in tool_configs - ] - permissions = agent_config.get("permissions", {}) - allowed_tools = self.tool_registry.validate_tools_for_agent( - tool_names, permissions - ) - # Load MCP Tools - mcp_servers = agent_config.get("mcp_servers", []) - if agent_config.get("mcp_enabled", False) and mcp_servers: - for server in mcp_servers: - try: - client = MCPClient( - server_url=server.get("server_url"), - auth_token=server.get("encrypted_auth") - ) - mcp_tools_list = await client.list_tools() - self.tool_registry.register_mcp_tools(client, mcp_tools_list) - for t in mcp_tools_list: - if t.name not in tool_names: - tool_names.append(t.name) - except Exception as e: - logger.warning(f"Failed to load MCP tools in stream: {e}") - - # Re-collect tools including MCP ones - allowed_tools = self.tool_registry.validate_tools_for_agent( - tool_names, permissions - ) - langchain_tools = self.tool_registry.get_langchain_tools(allowed_tools) - - if langchain_tools: - llm_with_tools = llm.bind_tools(langchain_tools) - else: - llm_with_tools = llm - - # Build graph - action_mode = agent_config.get("action_mode_enabled", False) - graph = self._build_graph(llm_with_tools, langchain_tools, action_mode) - - config = { - "configurable": {"thread_id": thread_id}, - "recursion_limit": agent_config.get("max_steps", 10) + 1 - } - state = {"messages": messages} - - yield {"type": "step", "step": "Generating response..."} - - # Stream the graph - full_response = "" - async for event in graph.astream_events(state, config, version="v2"): - kind = event.get("event", "") - - if kind == "on_chat_model_stream": - data = event.get("data", {}) - chunk = data.get("chunk") - if chunk and hasattr(chunk, "content") and chunk.content: - full_response += chunk.content - yield {"type": "token", "content": chunk.content} - - elif kind == "on_tool_start": - tool_name = event.get("name", "") - yield {"type": "tool_start", "tool": tool_name} - - elif kind == "on_tool_end": - tool_name = event.get("name", "") - output = event.get("data", {}).get("output", "") - yield {"type": "tool_end", "tool": tool_name, "result": str(output)[:500]} - - # Store in memory - await self.memory_manager.add_to_conversation( - agent_id, thread_id, "user", message - ) - await self.memory_manager.add_to_conversation( - agent_id, thread_id, "assistant", full_response - ) - - yield {"type": "done", "thread_id": thread_id} - - except Exception as e: - logger.error(f"Stream error: {e}", exc_info=True) - yield {"type": "error", "error": str(e)} - - async def _build_system_prompt(self, agent_config: Dict[str, Any], thread_id: str = "", message: str = "") -> str: - """ - Build the complete system prompt for an agent. - Supports both legacy configuration and new structured prompt templates. - """ - # Check for structured prompt template - if "prompt_template" in agent_config and agent_config["prompt_template"]: - return await self._build_structured_system_prompt(agent_config, thread_id, message) - - # Legacy Prompt Building - parts = [] - - # Base system prompt - system_prompt = agent_config.get("system_prompt", "") - if system_prompt: - parts.append(system_prompt) - - # Identity guidance - identity = agent_config.get("identity_guidance", "") - if identity: - parts.append(f"\n\nIdentity:\n{identity}") - - # Goal - goal = agent_config.get("goal", "") - if goal: - parts.append(f"\n\nGoal:\n{goal}") - - # Capabilities - tools = agent_config.get("tools", []) - if tools: - tool_names = [ - t.get("name") if isinstance(t, dict) else t - for t in tools - ] - parts.append(f"\n\nAvailable tools: {', '.join(tool_names)}") - - return "\n".join(parts) - - async def _build_structured_system_prompt(self, agent_config: Dict[str, Any], thread_id: str, message: str) -> str: - """ - Build a LangChain-style structured prompt from templates. - """ - # 1. Get Template Parts - # Try to get from config first (runtime override), then fall back to DB/Defaults - template = agent_config.get("prompt_template", {}) - - system_part = template.get("system_prompt", "") or agent_config.get("system_prompt", "") - goal_part = template.get("goal_prompt", "") - instruction_part = template.get("instruction_prompt", "") - output_part = template.get("output_prompt", "") - tool_part = template.get("tool_prompt", "") - - # 2. Dynamic Runtime Data - context_part = "" - memory_part = "" - scratchpad_part = "" - user_prompt = message - task_prompt = "Interact with the user to help them achieve their goals." - - # RAG Integration (Context) - if message: - try: - from app.ai.rag.rag_service import get_rag_service - rag_service = get_rag_service() - # Get relevant chunks - chunks = await rag_service.retrieve_context(message, k=5) - if chunks: - context_part = "\n---\n".join(chunks) - except Exception as e: - logger.warning(f"RAG retrieval failed: {e}") - - # Memory Integration - if thread_id: - try: - agent_id = agent_config.get("id", 0) - # Get session memory summary or last K messages if needed explicitly here - # Note: The conversation history is usually appended as messages list in LangChain - # This 'memory_prompt' section is for specific summarized context - session_mem = self.memory_manager.get_session_memory(agent_id, thread_id) - session_data = session_mem.get_all() - if session_data: - memory_items = [f"{k}: {v}" for k, v in session_data.items()] - memory_part = "\n".join(memory_items) - except Exception as e: - logger.warning(f"Failed to load session memory: {e}") - - # Tools Default Description - if not tool_part: - tools = agent_config.get("tools", []) - if tools: - tool_names = [t.get("name") if isinstance(t, dict) else t for t in tools] - tool_part = f"Available Tools: {', '.join(tool_names)}" - - # 3. Assemble Prompt - # This matches the requested format in the prompt - assembled_prompt = f""" -{system_part} - -GOAL: -{goal_part} - -INSTRUCTIONS: -{instruction_part} - -TOOLS: -{tool_part} - -CONTEXT: -{context_part} - -MEMORY: -{memory_part} - -SCRATCHPAD: -{scratchpad_part} - -USER: -{user_prompt} - -TASK: -{task_prompt} - -OUTPUT FORMAT: -{output_part} -""" - return assembled_prompt.strip() - - def _build_graph(self, llm, tools, action_mode: bool = False): - """Build the LangGraph execution graph.""" - from langgraph.graph import StateGraph - from langgraph.graph.message import add_messages - from typing import Annotated - - class GraphState(BaseModel): - messages: Annotated[list, add_messages] - - # Define the agent node - async def agent_node(state: GraphState): - response = await llm.ainvoke(state.messages) - return {"messages": [response]} - - # Build graph - builder = StateGraph(GraphState) - builder.add_node("agent", agent_node) - - if tools: - tool_node = ToolNode(tools=tools) - builder.add_node("tools", tool_node) - - builder.add_edge(START, "agent") - - # If Action Mode is enabled, allow loop. - # If NOT enabled (Assist Mode), we stop after agent generates tool calls creates a "break" (but ToolNode doesn't run) - # Actually, standard LangGraph behavior: - # If agent returns tool_calls, tools_condition returns "tools". - # If we want to STOP before tools in "Assist Mode", we shouldn't add the edge to tools or conditional edge. - - if action_mode: - builder.add_conditional_edges( - "agent", - tools_condition, - ) - builder.add_edge("tools", "agent") - else: - # In Chat/Assist mode, if tools are present, the LLM allows them, - # but we do NOT execute them automatically. - # So we just end after agent. - builder.add_edge("agent", END) - else: - builder.add_edge(START, "agent") - builder.add_edge("agent", END) - - return builder.compile(checkpointer=self._checkpointer) - - def get_available_providers(self) -> List[Dict[str, Any]]: - """Get list of available LLM providers.""" - return self.llm_router.get_all_providers_info() - - def get_available_tools(self) -> List[Dict[str, Any]]: - """Get list of available tools.""" - return self.tool_registry.get_tools_info() - - -# Global orchestrator instance -_orchestrator: Optional[AgentOrchestrator] = None - - -def get_orchestrator() -> AgentOrchestrator: - """Get the global orchestrator instance.""" - global _orchestrator - if _orchestrator is None: - _orchestrator = AgentOrchestrator() - return _orchestrator diff --git a/backend/app/agents/orchestrator/execution_loop.py b/backend/app/agents/orchestrator/execution_loop.py new file mode 100644 index 0000000..cfa82fc --- /dev/null +++ b/backend/app/agents/orchestrator/execution_loop.py @@ -0,0 +1,136 @@ +""" +Execution Loop +============== + +Core agent execution loop using LangGraph. +""" + +import logging +import uuid +from typing import Any, Dict, List, Optional, AsyncIterator, Annotated, TypedDict +from langchain_core.messages import BaseMessage, AIMessage +from langgraph.graph import StateGraph, START, END, add_messages +from langgraph.prebuilt import ToolNode, tools_condition +from langgraph.checkpoint.memory import MemorySaver + +from app.agents.providers.provider_router import ProviderRouter +from app.agents.tools.tool_registry import ToolRegistry, get_tool_registry +from app.agents.prompts.prompt_builder import PromptBuilder +from app.agents.memory.memory_manager import MemoryManager, get_memory_manager +from app.services.mcp_client import MCPClient + +logger = logging.getLogger(__name__) + +class GraphState(TypedDict): + messages: Annotated[list, add_messages] + +class ExecutionLoop: + """ + Core agent execution loop using LangGraph. + """ + + def __init__( + self, + agent_config: Dict[str, Any], + memory_manager: Optional[MemoryManager] = None, + tool_registry: Optional[ToolRegistry] = None + ): + self.config = agent_config + self.memory_manager = memory_manager or get_memory_manager() + self.tool_registry = tool_registry or get_tool_registry() + self.checkpointer = MemorySaver() + + async def run( + self, + message: str, + thread_id: str, + agent_id: int + ) -> Dict[str, Any]: + """ + Run the execution loop. + """ + # 1. Setup LLM + provider_name = self.config.get("llm_provider", "openai") + model_name = self.config.get("model_name", "gpt-4o") + + try: + llm = ProviderRouter().get_langchain_model(provider_name, model_name) + except Exception as e: + logger.error(f"Failed to load LLM: {e}") + raise + + # 2. Setup Tools + tool_configs = self.config.get("tools", []) + tool_names = [t.get("name") if isinstance(t, dict) else t for t in tool_configs] + permissions = self.config.get("permissions", {}) + + # Load MCP Tools + mcp_servers = self.config.get("mcp_servers", []) + if self.config.get("mcp_enabled", False) and mcp_servers: + for server in mcp_servers: + try: + client = MCPClient( + server_url=server.get("server_url"), + auth_token=server.get("encrypted_auth") + ) + mcp_tools = await client.list_tools() + self.tool_registry.register_mcp_tools(client, mcp_tools) + + for t in mcp_tools: + if t.name not in tool_names: + tool_names.append(t.name) + except Exception as e: + logger.warning(f"Failed to load MCP tools from server: {e}") + + allowed_tools = self.tool_registry.validate_tools_for_agent(tool_names, permissions) + langchain_tools = self.tool_registry.get_langchain_tools(allowed_tools) + + if langchain_tools: + llm = llm.bind_tools(langchain_tools) + + # 3. Build Prompt + prompt_builder = PromptBuilder(self.config) + history = await self.memory_manager.get_conversation_history(agent_id, thread_id, as_langchain=True) + initial_messages = await prompt_builder.build_messages(message, conversation_history=history) + + # 4. Construct Graph + workflow = StateGraph(GraphState) + + async def agent_node(state: GraphState): + response = await llm.ainvoke(state["messages"]) + return {"messages": [response]} + + workflow.add_node("agent", agent_node) + + if langchain_tools: + tool_node = ToolNode(langchain_tools) + workflow.add_node("tools", tool_node) + + workflow.add_edge(START, "agent") + workflow.add_conditional_edges("agent", tools_condition) + workflow.add_edge("tools", "agent") + else: + workflow.add_edge(START, "agent") + workflow.add_edge("agent", END) + + app = workflow.compile(checkpointer=self.checkpointer) + + # 5. Execute Graph + graph_thread_id = str(uuid.uuid4()) + + config = { + "configurable": {"thread_id": graph_thread_id}, + "recursion_limit": self.config.get("max_steps", 10) + 1 + } + + final_state = await app.ainvoke({"messages": initial_messages}, config) + + # 6. Extract Result + last_message = final_state["messages"][-1] + response_content = last_message.content if isinstance(last_message.content, str) else "" + + return { + "response": response_content, + "tool_calls": getattr(last_message, "tool_calls", []), + "messages": final_state["messages"] + } diff --git a/backend/app/agents/orchestrator/llm/__init__.py b/backend/app/agents/orchestrator/llm/__init__.py deleted file mode 100644 index 0a22cdc..0000000 --- a/backend/app/agents/orchestrator/llm/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -LLM Provider Module -================== - -Multi-provider LLM support with dynamic routing. -""" - -from app.agents.orchestrator.llm.router import LLMRouter, get_llm_router -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse -from app.agents.orchestrator.llm.providers.openai_provider import OpenAIProvider -from app.agents.orchestrator.llm.providers.ollama_provider import OllamaProvider -from app.agents.orchestrator.llm.providers.gemini_provider import GeminiProvider -from app.agents.orchestrator.llm.providers.claude_provider import ClaudeProvider -from app.agents.orchestrator.llm.providers.huggingface_provider import HuggingFaceProvider -from app.agents.orchestrator.llm.providers.groq_provider import GroqProvider - -__all__ = [ - "LLMRouter", - "get_llm_router", - "BaseLLMProvider", - "LLMResponse", - "OpenAIProvider", - "OllamaProvider", - "GeminiProvider", - "ClaudeProvider", - "HuggingFaceProvider", - "GroqProvider", -] diff --git a/backend/app/agents/orchestrator/llm/providers/__init__.py b/backend/app/agents/orchestrator/llm/providers/__init__.py deleted file mode 100644 index 03f7bfd..0000000 --- a/backend/app/agents/orchestrator/llm/providers/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -LLM Providers Package -===================== - -Multi-provider LLM support for the Agent Orchestrator. -""" - -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider -from app.agents.orchestrator.llm.providers.openai_provider import OpenAIProvider -from app.agents.orchestrator.llm.providers.ollama_provider import OllamaProvider -from app.agents.orchestrator.llm.providers.gemini_provider import GeminiProvider -from app.agents.orchestrator.llm.providers.claude_provider import ClaudeProvider -from app.agents.orchestrator.llm.providers.huggingface_provider import HuggingFaceProvider - -__all__ = [ - "BaseLLMProvider", - "OpenAIProvider", - "OllamaProvider", - "GeminiProvider", - "ClaudeProvider", - "HuggingFaceProvider", -] diff --git a/backend/app/agents/orchestrator/llm/router.py b/backend/app/agents/orchestrator/llm/router.py deleted file mode 100644 index 47572c0..0000000 --- a/backend/app/agents/orchestrator/llm/router.py +++ /dev/null @@ -1,267 +0,0 @@ -""" -LLM Router -========== - -Dynamic LLM provider routing based on agent configuration. -Routes requests to the appropriate provider (OpenAI, Ollama, Gemini, Claude, HuggingFace). -""" - -import logging -from typing import Any, Dict, List, Optional, Type -from langchain_core.language_models import BaseChatModel -from langchain_core.messages import BaseMessage -from langchain_core.tools import BaseTool - -from app.agents.orchestrator.config import OrchestratorConfig, LLMProviderConfig, get_orchestrator_config -from app.agents.orchestrator.exceptions import LLMProviderError, LLMProviderNotFoundError -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse -from app.agents.orchestrator.llm.providers.openai_provider import OpenAIProvider -from app.agents.orchestrator.llm.providers.ollama_provider import OllamaProvider -from app.agents.orchestrator.llm.providers.gemini_provider import GeminiProvider -from app.agents.orchestrator.llm.providers.claude_provider import ClaudeProvider -from app.agents.orchestrator.llm.providers.huggingface_provider import HuggingFaceProvider -from app.agents.orchestrator.llm.providers.groq_provider import GroqProvider -from app.agents.orchestrator.llm.providers.azure_openai_provider import AzureOpenAIProvider -from app.agents.orchestrator.llm.providers.aws_bedrock_provider import BedrockProvider -from app.agents.orchestrator.llm.providers.deepseek_provider import DeepSeekProvider - -logger = logging.getLogger(__name__) - - -class LLMRouter: - """ - LLM Router for dynamic provider selection. - - Routes requests to the appropriate LLM provider based on - agent configuration. Supports multiple providers and - automatic fallback. - """ - - # Provider class mapping - PROVIDER_CLASSES: Dict[str, Type[BaseLLMProvider]] = { - "openai": OpenAIProvider, - "ollama": OllamaProvider, - "gemini": GeminiProvider, - "google": GeminiProvider, # Alias for gemini - "claude": ClaudeProvider, - "huggingface": HuggingFaceProvider, - "groq": GroqProvider, - "azure_openai": AzureOpenAIProvider, - "aws_bedrock": BedrockProvider, - "deepseek": DeepSeekProvider, - } - - def __init__(self, config: Optional[OrchestratorConfig] = None): - """ - Initialize the LLM Router. - - Args: - config: Orchestrator configuration. Uses default if not provided. - """ - self.config = config or get_orchestrator_config() - self._providers: Dict[str, BaseLLMProvider] = {} - - def get_provider(self, provider_name: str) -> BaseLLMProvider: - """ - Get or create an LLM provider instance. - - Args: - provider_name: Name of the provider (openai, ollama, etc.) - - Returns: - BaseLLMProvider instance - - Raises: - LLMProviderNotFoundError: If provider is not supported - """ - provider_name = provider_name.lower() - - # Return cached provider if available - if provider_name in self._providers: - return self._providers[provider_name] - - # Validate provider name - if provider_name not in self.PROVIDER_CLASSES: - raise LLMProviderNotFoundError(provider_name) - - # Get provider configuration - provider_config = self.config.get_provider_config(provider_name) - if not provider_config: - raise LLMProviderError(provider_name, "Provider configuration not found") - - # Create provider instance - provider_class = self.PROVIDER_CLASSES[provider_name] - provider = provider_class(provider_config) - - # Cache and return - self._providers[provider_name] = provider - return provider - - def get_langchain_model( - self, - provider_name: str, - model_name: Optional[str] = None, - ) -> BaseChatModel: - """ - Get a LangChain model for the specified provider. - - Args: - provider_name: Name of the provider - model_name: Optional specific model name - - Returns: - LangChain BaseChatModel instance - """ - provider = self.get_provider(provider_name) - return provider.get_langchain_model(model_name) - - async def generate( - self, - provider_name: str, - messages: List[BaseMessage], - tools: Optional[List[BaseTool]] = None, - model: Optional[str] = None, - temperature: Optional[float] = None, - max_tokens: Optional[int] = None, - **kwargs, - ) -> LLMResponse: - """ - Generate a response using the specified provider. - - Args: - provider_name: Name of the provider to use - messages: List of chat messages - tools: Optional list of tools - model: Optional model override - temperature: Optional temperature override - max_tokens: Optional max_tokens override - **kwargs: Additional arguments - - Returns: - LLMResponse with the model's response - """ - provider = self.get_provider(provider_name) - - return await provider.generate( - messages=messages, - tools=tools, - model=model, - temperature=temperature, - max_tokens=max_tokens, - **kwargs, - ) - - async def stream( - self, - provider_name: str, - messages: List[BaseMessage], - tools: Optional[List[BaseTool]] = None, - model: Optional[str] = None, - temperature: Optional[float] = None, - max_tokens: Optional[int] = None, - **kwargs, - ): - """ - Stream a response using the specified provider. - - Args: - provider_name: Name of the provider to use - messages: List of chat messages - tools: Optional list of tools - model: Optional model override - temperature: Optional temperature override - max_tokens: Optional max_tokens override - **kwargs: Additional arguments - - Yields: - String chunks of the response - """ - provider = self.get_provider(provider_name) - - async for chunk in provider.stream( - messages=messages, - tools=tools, - model=model, - temperature=temperature, - max_tokens=max_tokens, - **kwargs, - ): - yield chunk - - def get_available_providers(self) -> List[str]: - """Get list of providers with valid credentials.""" - return self.config.get_available_providers() - - def get_provider_info(self, provider_name: str) -> Dict[str, Any]: - """ - Get information about a specific provider. - - Args: - provider_name: Name of the provider - - Returns: - Dictionary with provider information - """ - provider_config = self.config.get_provider_config(provider_name) - if not provider_config: - raise LLMProviderNotFoundError(provider_name) - - # Check if provider has valid credentials - available_providers = self.config.get_available_providers() - is_available = provider_name in available_providers - - return { - "name": provider_name, - "display_name": provider_name.title(), - "available": is_available, - "models": provider_config.available_models, - "default_model": provider_config.default_model, - "supports_streaming": True, - "supports_tools": provider_name != "huggingface", - } - - def get_all_providers_info(self) -> List[Dict[str, Any]]: - """Get information about all supported providers.""" - providers_info = [] - for provider_name in self.PROVIDER_CLASSES.keys(): - try: - info = self.get_provider_info(provider_name) - providers_info.append(info) - except Exception as e: - logger.warning(f"Could not get info for provider {provider_name}: {e}") - return providers_info - - def validate_provider_and_model( - self, - provider_name: str, - model_name: Optional[str] = None, - ) -> bool: - """ - Validate that a provider and model combination is valid. - - Args: - provider_name: Name of the provider - model_name: Optional model name to validate - - Returns: - True if valid, False otherwise - """ - try: - provider = self.get_provider(provider_name) - if model_name: - return provider.validate_model(model_name) - return True - except Exception: - return False - - -# Global router instance -_router: Optional[LLMRouter] = None - - -def get_llm_router() -> LLMRouter: - """Get the global LLM router instance.""" - global _router - if _router is None: - _router = LLMRouter() - return _router diff --git a/backend/app/agents/orchestrator/memory/__init__.py b/backend/app/agents/orchestrator/memory/__init__.py deleted file mode 100644 index 3a48187..0000000 --- a/backend/app/agents/orchestrator/memory/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Memory System Package -===================== - -Memory management for agents including conversation, -vector, and session memory. -""" - -from app.agents.orchestrator.memory.manager import MemoryManager, get_memory_manager -from app.agents.orchestrator.memory.conversation import ConversationMemory -from app.agents.orchestrator.memory.vector import VectorMemory -from app.agents.orchestrator.memory.session import SessionMemory - -__all__ = [ - "MemoryManager", - "get_memory_manager", - "ConversationMemory", - "VectorMemory", - "SessionMemory", -] diff --git a/backend/app/agents/orchestrator/orchestrator.py b/backend/app/agents/orchestrator/orchestrator.py new file mode 100644 index 0000000..2f3c91f --- /dev/null +++ b/backend/app/agents/orchestrator/orchestrator.py @@ -0,0 +1,99 @@ +""" +Agent Orchestrator +================== + +Main orchestrator for dynamic agent execution. +""" + +import logging +import time +import uuid +from typing import Any, Dict, List, Optional, AsyncIterator +from pydantic import BaseModel + +from app.agents.orchestrator.execution_loop import ExecutionLoop +from app.agents.orchestrator.planner import ExecutionPlanner +from app.agents.memory.memory_manager import get_memory_manager +from app.agents.orchestrator.config import get_orchestrator_config +from app.agents.orchestrator.security import SecurityGuard, get_security_guard +from app.agents.orchestrator.exceptions import OrchestratorError, PromptInjectionError + +logger = logging.getLogger(__name__) + +class ExecutionResult(BaseModel): + """Result of agent execution.""" + response: str + tool_calls: List[Dict[str, Any]] = [] + thread_id: str = "" + duration_ms: int = 0 + +class AgentOrchestrator: + """ + Main orchestrator for dynamic agent execution. + """ + + def __init__(self): + self.config = get_orchestrator_config() + self.memory_manager = get_memory_manager() + self.security_guard = get_security_guard() + + async def run( + self, + agent_config: Dict[str, Any], + message: str, + thread_id: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + ) -> ExecutionResult: + """ + Run an agent with a message. + """ + start_time = time.time() + agent_id = agent_config.get("id", 0) + thread_id = thread_id or str(uuid.uuid4()) + + try: + # 0. Security Check + if not self.security_guard.validate_input(message, agent_config): + raise PromptInjectionError() + + # 1. Execution Loop + loop = ExecutionLoop(agent_config, self.memory_manager) + result = await loop.run(message, thread_id, agent_id) + + # 2. Store Memory + # Persist User and Assistant messages to ConversationMemory + # We assume ConversationMemory is the high-level chat history. + # Intermediate tool steps are generally not stored in "chat history" but in "execution logs". + + await self.memory_manager.add_to_conversation( + agent_id, thread_id, "user", message + ) + + # If response is empty (e.g. tool loop fail), we should handle it. + response = result.get("response", "") + if response: + await self.memory_manager.add_to_conversation( + agent_id, thread_id, "assistant", response + ) + + duration_ms = int((time.time() - start_time) * 1000) + + return ExecutionResult( + response=response, + tool_calls=result.get("tool_calls", []), + thread_id=thread_id, + duration_ms=duration_ms + ) + + except Exception as e: + logger.error(f"Orchestrator run error: {e}", exc_info=True) + raise OrchestratorError(str(e)) + +# Global instance +_orchestrator: Optional[AgentOrchestrator] = None + +def get_orchestrator() -> AgentOrchestrator: + global _orchestrator + if _orchestrator is None: + _orchestrator = AgentOrchestrator() + return _orchestrator diff --git a/backend/app/agents/orchestrator/planner.py b/backend/app/agents/orchestrator/planner.py new file mode 100644 index 0000000..45987bc --- /dev/null +++ b/backend/app/agents/orchestrator/planner.py @@ -0,0 +1,22 @@ +""" +Execution Planner +================= + +Plans the execution steps for an agent. +""" + +from typing import List, Dict, Any +from langchain_core.messages import BaseMessage +from langchain_core.language_models import BaseChatModel + +class ExecutionPlanner: + """ + Plans the execution steps for an agent. + """ + def __init__(self, llm: BaseChatModel): + self.llm = llm + + async def plan(self, messages: List[BaseMessage]) -> List[str]: + # Simple planning: just return default plan or use LLM to break down task + # For now, placeholder. + return ["understand_request", "execute_tools", "finalize_response"] diff --git a/backend/app/agents/orchestrator/service.py b/backend/app/agents/orchestrator/service.py index 6ad428a..b6cd20e 100644 --- a/backend/app/agents/orchestrator/service.py +++ b/backend/app/agents/orchestrator/service.py @@ -20,7 +20,7 @@ AgentResponse, ExecutionRequest, ) -from app.agents.orchestrator.core import AgentOrchestrator, get_orchestrator, ExecutionResult +from app.agents.orchestrator.orchestrator import AgentOrchestrator, get_orchestrator, ExecutionResult from app.agents.orchestrator.exceptions import AgentNotFoundError, OrchestratorError logger = logging.getLogger(__name__) diff --git a/backend/app/agents/orchestrator/tools/__init__.py b/backend/app/agents/orchestrator/tools/__init__.py deleted file mode 100644 index 166c0d9..0000000 --- a/backend/app/agents/orchestrator/tools/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Agent Tools Package -=================== - -Dynamic tool loading system for agents. -Provides MCP connectors and custom tools. -""" - -from app.agents.orchestrator.tools.registry import ToolRegistry, get_tool_registry -from app.agents.orchestrator.tools.base import BaseTool, ToolResult - -__all__ = [ - "ToolRegistry", - "get_tool_registry", - "BaseTool", - "ToolResult", -] diff --git a/backend/app/agents/orchestrator/tools/builtin/__init__.py b/backend/app/agents/orchestrator/tools/builtin/__init__.py deleted file mode 100644 index 0824910..0000000 --- a/backend/app/agents/orchestrator/tools/builtin/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Built-in Tools Package -====================== - -Built-in tools for agent operations. -""" - -from app.agents.orchestrator.tools.builtin import github_tools -from app.agents.orchestrator.tools.builtin import slack_tools -from app.agents.orchestrator.tools.builtin import teams_tools -from app.agents.orchestrator.tools.builtin import telegram_tools -from app.agents.orchestrator.tools.builtin import web_tools -from app.agents.orchestrator.tools.builtin import file_tools - -__all__ = [ - "github_tools", - "slack_tools", - "teams_tools", - "telegram_tools", - "web_tools", - "file_tools", -] diff --git a/backend/app/agents/prompts/prompt_builder.py b/backend/app/agents/prompts/prompt_builder.py new file mode 100644 index 0000000..47b4bb7 --- /dev/null +++ b/backend/app/agents/prompts/prompt_builder.py @@ -0,0 +1,96 @@ +""" +Prompt Builder +============== + +Builds dynamic prompts for agents. +""" + +import logging +from typing import List, Optional, Dict, Any +from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage + +from app.agents.prompts.prompt_types import PromptType, PromptPart +from app.agents.prompts.prompt_templates import PromptTemplateStore, DEFAULT_SYSTEM_PART +from app.agents.rag.retriever import get_retriever + +logger = logging.getLogger(__name__) + +class PromptBuilder: + """ + Builds dynamic prompts for agents. + """ + + def __init__(self, agent_config: Dict[str, Any]): + self.config = agent_config + self.retriever = get_retriever() + + async def build_messages( + self, + user_message: str, + conversation_history: List[BaseMessage] = None, + context: Optional[str] = None + ) -> List[BaseMessage]: + """ + Build the full message list for the LLM. + """ + parts: List[PromptPart] = [] + + # 1. Fetch from DB/Template + template_name = self.config.get("prompt_template_name") + template = PromptTemplateStore.get_template(template_name) if template_name else None + + if template: + parts.extend(template.parts) + else: + # Fallback/Default Construction + if self.config.get("system_prompt"): + parts.append(PromptPart(type=PromptType.SYSTEM, content=self.config["system_prompt"])) + else: + parts.append(DEFAULT_SYSTEM_PART) + + if self.config.get("goal"): + parts.append(PromptPart(type=PromptType.GOAL, content=self.config["goal"])) + + if self.config.get("instruction"): + parts.append(PromptPart(type=PromptType.INSTRUCTION, content=self.config["instruction"])) + + # Tools (optional description in prompt) + # Typically handled by bind_tools, but can be added here if needed for older models + # We skip explicit tool descriptions here assuming native tool calling or bind_tools usage + + # 2. RAG Context (if enabled) + if self.config.get("rag_enabled", False): + # If context not provided, retrieve it + if not context and user_message: + try: + docs = await self.retriever.retrieve(user_message, k=5) + if docs: + context = "\n---\n".join(docs) + except Exception as e: + logger.error(f"Failed to retrieve context: {e}") + + if context: + parts.append(PromptPart(type=PromptType.CONTEXT, content=f"Relevant Context:\n{context}")) + + # 3. Assemble System Prompt + system_content_parts = [] + for p in parts: + if p.type in [PromptType.SYSTEM, PromptType.GOAL, PromptType.INSTRUCTION, PromptType.CONTEXT, PromptType.TOOL, PromptType.OUTPUT]: + # Uppercase prefix for clarity + prefix = p.type.name.upper() + if p.content: + system_content_parts.append(f"{prefix}:\n{p.content}") + + full_system_prompt = "\n\n".join(system_content_parts) + + messages = [SystemMessage(content=full_system_prompt)] + + # 4. Add Conversation History + if conversation_history: + messages.extend(conversation_history) + + # 5. Add User Message + if user_message: + messages.append(HumanMessage(content=user_message)) + + return messages diff --git a/backend/app/agents/prompts/prompt_templates.py b/backend/app/agents/prompts/prompt_templates.py new file mode 100644 index 0000000..92ac9b6 --- /dev/null +++ b/backend/app/agents/prompts/prompt_templates.py @@ -0,0 +1,36 @@ +""" +Prompt Templates +================ + +Templates and storage for dynamic prompts. +""" + +from typing import Dict, List, Optional +from app.agents.prompts.prompt_types import PromptType, PromptPart + +class PromptTemplate: + """ + Template for a prompt. + """ + def __init__(self, name: str, parts: List[PromptPart]): + self.name = name + self.parts = parts + +class PromptTemplateStore: + """ + Store for prompt templates. In a real app, this would be a DB. + """ + _templates: Dict[str, PromptTemplate] = {} + + @classmethod + def get_template(cls, name: str) -> Optional[PromptTemplate]: + return cls._templates.get(name) + + @classmethod + def save_template(cls, template: PromptTemplate): + cls._templates[template.name] = template + +# Default parts +DEFAULT_SYSTEM_PART = PromptPart(type=PromptType.SYSTEM, content="You are a helpful AI assistant.") +DEFAULT_GOAL_PART = PromptPart(type=PromptType.GOAL, content="Help the user with their request.") +DEFAULT_INSTRUCTION_PART = PromptPart(type=PromptType.INSTRUCTION, content="Answer concisely and accurately.") diff --git a/backend/app/agents/prompts/prompt_types.py b/backend/app/agents/prompts/prompt_types.py new file mode 100644 index 0000000..0ce2500 --- /dev/null +++ b/backend/app/agents/prompts/prompt_types.py @@ -0,0 +1,31 @@ +""" +Prompt Types +============ + +Enumerations and types for the dynamic prompt system. +""" + +from enum import Enum +from typing import Any, Dict, List, Optional +from pydantic import BaseModel, Field + +class PromptType(str, Enum): + """ + Types of prompt components. + """ + SYSTEM = "system" + GOAL = "goal" + INSTRUCTION = "instruction" + CONTEXT = "context" + TOOL = "tool" + MEMORY = "memory" + SCRATCHPAD = "scratchpad" + OUTPUT = "output" + +class PromptPart(BaseModel): + """ + A single part of a prompt. + """ + type: PromptType + content: str + metadata: Dict[str, Any] = Field(default_factory=dict) diff --git a/backend/app/agents/providers/__init__.py b/backend/app/agents/providers/__init__.py new file mode 100644 index 0000000..0f983c7 --- /dev/null +++ b/backend/app/agents/providers/__init__.py @@ -0,0 +1,22 @@ +""" +LLM Providers Package +===================== + +Multi-provider LLM support for the Agent Orchestrator. +""" + +from app.agents.providers.base import BaseLLMProvider +from app.agents.providers.openai_provider import OpenAIProvider +from app.agents.providers.ollama_provider import OllamaProvider +from app.agents.providers.gemini_provider import GeminiProvider +from app.agents.providers.claude_provider import ClaudeProvider +from app.agents.providers.huggingface_provider import HuggingFaceProvider + +__all__ = [ + "BaseLLMProvider", + "OpenAIProvider", + "OllamaProvider", + "GeminiProvider", + "ClaudeProvider", + "HuggingFaceProvider", +] diff --git a/backend/app/agents/orchestrator/llm/providers/aws_bedrock_provider.py b/backend/app/agents/providers/aws_bedrock_provider.py similarity index 98% rename from backend/app/agents/orchestrator/llm/providers/aws_bedrock_provider.py rename to backend/app/agents/providers/aws_bedrock_provider.py index 8f80add..1418c76 100644 --- a/backend/app/agents/orchestrator/llm/providers/aws_bedrock_provider.py +++ b/backend/app/agents/providers/aws_bedrock_provider.py @@ -12,7 +12,7 @@ from langchain_core.tools import BaseTool from langchain_aws import ChatBedrock -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse +from app.agents.providers.base import BaseLLMProvider, LLMResponse from app.agents.orchestrator.config import LLMProviderConfig from app.agents.orchestrator.exceptions import LLMProviderError diff --git a/backend/app/agents/orchestrator/llm/providers/azure_openai_provider.py b/backend/app/agents/providers/azure_openai_provider.py similarity index 98% rename from backend/app/agents/orchestrator/llm/providers/azure_openai_provider.py rename to backend/app/agents/providers/azure_openai_provider.py index 4988bcd..33df31e 100644 --- a/backend/app/agents/orchestrator/llm/providers/azure_openai_provider.py +++ b/backend/app/agents/providers/azure_openai_provider.py @@ -12,7 +12,7 @@ from langchain_core.tools import BaseTool from langchain_openai import AzureChatOpenAI -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse +from app.agents.providers.base import BaseLLMProvider, LLMResponse from app.agents.orchestrator.config import LLMProviderConfig from app.agents.orchestrator.exceptions import LLMProviderError diff --git a/backend/app/agents/orchestrator/llm/providers/base.py b/backend/app/agents/providers/base.py similarity index 100% rename from backend/app/agents/orchestrator/llm/providers/base.py rename to backend/app/agents/providers/base.py diff --git a/backend/app/agents/orchestrator/llm/providers/claude_provider.py b/backend/app/agents/providers/claude_provider.py similarity index 98% rename from backend/app/agents/orchestrator/llm/providers/claude_provider.py rename to backend/app/agents/providers/claude_provider.py index 935b551..04a174b 100644 --- a/backend/app/agents/orchestrator/llm/providers/claude_provider.py +++ b/backend/app/agents/providers/claude_provider.py @@ -12,7 +12,7 @@ from langchain_core.tools import BaseTool from langchain_anthropic import ChatAnthropic -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse +from app.agents.providers.base import BaseLLMProvider, LLMResponse from app.agents.orchestrator.config import LLMProviderConfig from app.agents.orchestrator.exceptions import LLMProviderError diff --git a/backend/app/agents/orchestrator/llm/providers/deepseek_provider.py b/backend/app/agents/providers/deepseek_provider.py similarity index 98% rename from backend/app/agents/orchestrator/llm/providers/deepseek_provider.py rename to backend/app/agents/providers/deepseek_provider.py index 680926f..d796f6d 100644 --- a/backend/app/agents/orchestrator/llm/providers/deepseek_provider.py +++ b/backend/app/agents/providers/deepseek_provider.py @@ -12,7 +12,7 @@ from langchain_core.tools import BaseTool from langchain_openai import ChatOpenAI -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse +from app.agents.providers.base import BaseLLMProvider, LLMResponse from app.agents.orchestrator.config import LLMProviderConfig from app.agents.orchestrator.exceptions import LLMProviderError diff --git a/backend/app/agents/orchestrator/llm/providers/gemini_provider.py b/backend/app/agents/providers/gemini_provider.py similarity index 98% rename from backend/app/agents/orchestrator/llm/providers/gemini_provider.py rename to backend/app/agents/providers/gemini_provider.py index 6fac83e..79ba9da 100644 --- a/backend/app/agents/orchestrator/llm/providers/gemini_provider.py +++ b/backend/app/agents/providers/gemini_provider.py @@ -12,7 +12,7 @@ from langchain_core.tools import BaseTool from langchain_google_genai import ChatGoogleGenerativeAI -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse +from app.agents.providers.base import BaseLLMProvider, LLMResponse from app.agents.orchestrator.config import LLMProviderConfig from app.agents.orchestrator.exceptions import LLMProviderError diff --git a/backend/app/agents/orchestrator/llm/providers/groq_provider.py b/backend/app/agents/providers/groq_provider.py similarity index 98% rename from backend/app/agents/orchestrator/llm/providers/groq_provider.py rename to backend/app/agents/providers/groq_provider.py index 124ce4c..1934bb1 100644 --- a/backend/app/agents/orchestrator/llm/providers/groq_provider.py +++ b/backend/app/agents/providers/groq_provider.py @@ -12,7 +12,7 @@ from langchain_core.tools import BaseTool from langchain_openai import ChatOpenAI -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse +from app.agents.providers.base import BaseLLMProvider, LLMResponse from app.agents.orchestrator.exceptions import LLMProviderError logger = logging.getLogger(__name__) diff --git a/backend/app/agents/orchestrator/llm/providers/huggingface_provider.py b/backend/app/agents/providers/huggingface_provider.py similarity index 98% rename from backend/app/agents/orchestrator/llm/providers/huggingface_provider.py rename to backend/app/agents/providers/huggingface_provider.py index 852661b..807d399 100644 --- a/backend/app/agents/orchestrator/llm/providers/huggingface_provider.py +++ b/backend/app/agents/providers/huggingface_provider.py @@ -12,7 +12,7 @@ from langchain_core.tools import BaseTool from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse +from app.agents.providers.base import BaseLLMProvider, LLMResponse from app.agents.orchestrator.config import LLMProviderConfig from app.agents.orchestrator.exceptions import LLMProviderError diff --git a/backend/app/agents/orchestrator/llm/providers/ollama_provider.py b/backend/app/agents/providers/ollama_provider.py similarity index 98% rename from backend/app/agents/orchestrator/llm/providers/ollama_provider.py rename to backend/app/agents/providers/ollama_provider.py index 4d2bd93..aa81913 100644 --- a/backend/app/agents/orchestrator/llm/providers/ollama_provider.py +++ b/backend/app/agents/providers/ollama_provider.py @@ -12,7 +12,7 @@ from langchain_core.tools import BaseTool from langchain_ollama import ChatOllama -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse +from app.agents.providers.base import BaseLLMProvider, LLMResponse from app.agents.orchestrator.config import LLMProviderConfig from app.agents.orchestrator.exceptions import LLMProviderError diff --git a/backend/app/agents/orchestrator/llm/providers/openai_provider.py b/backend/app/agents/providers/openai_provider.py similarity index 98% rename from backend/app/agents/orchestrator/llm/providers/openai_provider.py rename to backend/app/agents/providers/openai_provider.py index 126d968..2e0bed3 100644 --- a/backend/app/agents/orchestrator/llm/providers/openai_provider.py +++ b/backend/app/agents/providers/openai_provider.py @@ -12,7 +12,7 @@ from langchain_core.tools import BaseTool from langchain_openai import ChatOpenAI -from app.agents.orchestrator.llm.providers.base import BaseLLMProvider, LLMResponse +from app.agents.providers.base import BaseLLMProvider, LLMResponse from app.agents.orchestrator.config import LLMProviderConfig from app.agents.orchestrator.exceptions import LLMProviderError diff --git a/backend/app/agents/providers/provider_router.py b/backend/app/agents/providers/provider_router.py new file mode 100644 index 0000000..b703d4b --- /dev/null +++ b/backend/app/agents/providers/provider_router.py @@ -0,0 +1,67 @@ +""" +Provider Router +=============== + +Dynamic LLM provider routing. +""" + +from typing import Any, Dict, List, Optional, Type +from langchain_core.language_models import BaseChatModel + +from app.agents.orchestrator.config import OrchestratorConfig, get_orchestrator_config +from app.agents.providers.base import BaseLLMProvider +from app.agents.providers.openai_provider import OpenAIProvider +from app.agents.providers.ollama_provider import OllamaProvider +from app.agents.providers.gemini_provider import GeminiProvider +from app.agents.providers.claude_provider import ClaudeProvider +from app.agents.providers.huggingface_provider import HuggingFaceProvider +from app.agents.providers.groq_provider import GroqProvider +from app.agents.providers.azure_openai_provider import AzureOpenAIProvider +from app.agents.providers.aws_bedrock_provider import BedrockProvider +from app.agents.providers.deepseek_provider import DeepSeekProvider +from app.agents.orchestrator.exceptions import LLMProviderNotFoundError + +class ProviderRouter: + """ + Dynamic LLM Provider Router. + """ + + PROVIDER_CLASSES: Dict[str, Type[BaseLLMProvider]] = { + "openai": OpenAIProvider, + "ollama": OllamaProvider, + "gemini": GeminiProvider, + "google": GeminiProvider, + "claude": ClaudeProvider, + "huggingface": HuggingFaceProvider, + "groq": GroqProvider, + "azure_openai": AzureOpenAIProvider, + "aws_bedrock": BedrockProvider, + "deepseek": DeepSeekProvider, + } + + def __init__(self, config: Optional[OrchestratorConfig] = None): + self.config = config or get_orchestrator_config() + + def get_provider(self, provider_name: str) -> BaseLLMProvider: + provider_name = provider_name.lower() + if provider_name not in self.PROVIDER_CLASSES: + raise LLMProviderNotFoundError(provider_name) + + provider_config = self.config.get_provider_config(provider_name) + provider_class = self.PROVIDER_CLASSES[provider_name] + return provider_class(provider_config) + + @classmethod + def load(cls, provider_name: str, user_id: Optional[str] = None) -> BaseChatModel: + """ + Load a provider dynamically. + """ + router = cls() + # In a real app, user_id might be used to fetch user-specific keys + # For now, we use the global config + provider = router.get_provider(provider_name) + return provider.get_langchain_model() + + def get_langchain_model(self, provider_name: str, model_name: Optional[str] = None) -> BaseChatModel: + provider = self.get_provider(provider_name) + return provider.get_langchain_model(model_name) diff --git a/backend/app/ai/rag/ingestion/file_loader.py b/backend/app/agents/rag/document_loader.py similarity index 100% rename from backend/app/ai/rag/ingestion/file_loader.py rename to backend/app/agents/rag/document_loader.py diff --git a/backend/app/ai/rag/embeddings/embedding_service.py b/backend/app/agents/rag/embedding_service.py similarity index 100% rename from backend/app/ai/rag/embeddings/embedding_service.py rename to backend/app/agents/rag/embedding_service.py diff --git a/backend/app/ai/rag/retriever.py b/backend/app/agents/rag/retriever.py similarity index 83% rename from backend/app/ai/rag/retriever.py rename to backend/app/agents/rag/retriever.py index 405c3a6..ba8d6cc 100644 --- a/backend/app/ai/rag/retriever.py +++ b/backend/app/agents/rag/retriever.py @@ -6,7 +6,7 @@ """ from typing import List, Dict, Any, Optional -from app.ai.rag.vectorstore.chroma_store import ChromaVectorStore # Default to Chroma for now +from app.agents.rag.vectorstore.chroma_store import ChromaVectorStore # Default to Chroma for now from app.core.config import settings class Retriever: @@ -16,11 +16,11 @@ def __init__(self): self.vector_store_type = settings.RAG_VECTOR_DB_TYPE if self.vector_store_type == "pgvector": - from app.ai.rag.vectorstore.pgvector_store import PGVectorStore + from app.agents.rag.vectorstore.pgvector_store import PGVectorStore self.vector_store = PGVectorStore() else: # Default to Chroma for dev - from app.ai.rag.vectorstore.chroma_store import ChromaVectorStore + from app.agents.rag.vectorstore.chroma_store import ChromaVectorStore self.vector_store = ChromaVectorStore() async def retrieve(self, query: str, k: int = 5, filter: Optional[Dict[str, Any]] = None) -> List[str]: diff --git a/backend/app/ai/rag/vectorstore/base.py b/backend/app/agents/rag/vectorstore/base.py similarity index 100% rename from backend/app/ai/rag/vectorstore/base.py rename to backend/app/agents/rag/vectorstore/base.py diff --git a/backend/app/ai/rag/vectorstore/chroma_store.py b/backend/app/agents/rag/vectorstore/chroma_store.py similarity index 95% rename from backend/app/ai/rag/vectorstore/chroma_store.py rename to backend/app/agents/rag/vectorstore/chroma_store.py index 1885252..fa403f9 100644 --- a/backend/app/ai/rag/vectorstore/chroma_store.py +++ b/backend/app/agents/rag/vectorstore/chroma_store.py @@ -7,8 +7,8 @@ import chromadb from typing import List, Dict, Any, Optional -from app.ai.rag.vectorstore.base import VectorStore -from app.ai.rag.embeddings.embedding_service import get_embedding_service +from app.agents.rag.vectorstore.base import VectorStore +from app.agents.rag.embedding_service import get_embedding_service from app.core.config import settings class ChromaVectorStore(VectorStore): diff --git a/backend/app/ai/rag/vectorstore/pgvector_store.py b/backend/app/agents/rag/vectorstore/pgvector_store.py similarity index 95% rename from backend/app/ai/rag/vectorstore/pgvector_store.py rename to backend/app/agents/rag/vectorstore/pgvector_store.py index 55c7271..b0f5a3f 100644 --- a/backend/app/ai/rag/vectorstore/pgvector_store.py +++ b/backend/app/agents/rag/vectorstore/pgvector_store.py @@ -11,8 +11,8 @@ from langchain_core.documents import Document from app.core.config import settings -from app.ai.rag.embeddings.embedding_service import get_embedding_service -from app.ai.rag.vectorstore.base import VectorStore +from app.agents.rag.embedding_service import get_embedding_service +from app.agents.rag.vectorstore.base import VectorStore logger = logging.getLogger(__name__) diff --git a/backend/app/agents/runtime/agent_runtime.py b/backend/app/agents/runtime/agent_runtime.py new file mode 100644 index 0000000..fabe5de --- /dev/null +++ b/backend/app/agents/runtime/agent_runtime.py @@ -0,0 +1,124 @@ +""" +Agent Runtime +============= + +High-level runtime for AI Agents. +Handling request lifecycle, context, and orchestration dispatch. +""" + +import logging +from typing import Any, Dict, Optional, List +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from sqlalchemy.orm import selectinload + +from app.agents.orchestrator.orchestrator import get_orchestrator, ExecutionResult +from app.agents.runtime.execution_context import ExecutionContext +from app.services.agent import agent_service +from app.models.agent.model import Agent, AgentUserPermission + +logger = logging.getLogger(__name__) + +class AgentRuntime: + """ + High-level runtime for AI Agents. + Handling request lifecycle, context, and orchestration dispatch. + """ + + def __init__(self): + self.orchestrator = get_orchestrator() + + async def execute_agent( + self, + db: AsyncSession, + agent_id: int, + message: str, + user_id: int, + thread_id: Optional[str] = None + ) -> ExecutionResult: + """ + Execute an agent request. + """ + # 1. Fetch Agent Config with Tools + query = select(Agent).options( + selectinload(Agent.tools) + ).filter(Agent.id == agent_id) + + result = await db.execute(query) + agent = result.scalars().first() + + if not agent: + raise ValueError(f"Agent {agent_id} not found") + + # 2. Fetch User Permissions + perm_query = select(AgentUserPermission).filter( + AgentUserPermission.agent_id == agent_id, + AgentUserPermission.user_id == user_id + ) + perm_result = await db.execute(perm_query) + user_perm = perm_result.scalars().first() + + # Construct Permissions Dict + permissions = { + "can_use": True, # Default if accessing public/creator agent, strictly checked in service usually + "can_execute_code": False, # Default safe + "can_access_internet": False # Default safe + } + + if user_perm: + permissions["can_use"] = user_perm.can_use + # specific permissions could be mapped if they existed in the model + # For now we assume standard permissions or map from custom fields if added later + + # 3. Map Tools + tool_configs = [] + if agent.tools: + for tool in agent.tools: + tool_configs.append({ + "name": tool.name, + "description": tool.description, + "parameters": tool.function_schema + }) + + # 4. Construct Config + agent_config = { + "id": agent.id, + "name": agent.name, + "system_prompt": agent.system_prompt, + "llm_provider": agent.llm_provider.value if hasattr(agent.llm_provider, 'value') else str(agent.llm_provider), + "model_name": agent.llm_model, + "tools": tool_configs, + "permissions": permissions, + "goal": agent.goals[0] if agent.goals else "", + "instruction": "", + "rag_enabled": False, # Could be added to Agent model + "max_steps": 15 + } + + # 5. Create Context + context = ExecutionContext( + agent_id=agent_id, + user_id=str(user_id), + thread_id=thread_id + ) + + # 6. Invoke Orchestrator + try: + result = await self.orchestrator.run( + agent_config=agent_config, + message=message, + thread_id=thread_id + ) + return result + except Exception as e: + logger.error(f"Runtime execution failed: {e}") + raise + +# Global Runtime +_runtime = None + +def get_agent_runtime() -> AgentRuntime: + global _runtime + if _runtime is None: + _runtime = AgentRuntime() + return _runtime diff --git a/backend/app/agents/runtime/execution_context.py b/backend/app/agents/runtime/execution_context.py new file mode 100644 index 0000000..28e26b5 --- /dev/null +++ b/backend/app/agents/runtime/execution_context.py @@ -0,0 +1,18 @@ +""" +Execution Context +================= + +Context object for agent execution. +""" + +from typing import Optional, Dict, Any +from pydantic import BaseModel, Field + +class ExecutionContext(BaseModel): + """ + Context for a single agent execution request. + """ + agent_id: int + user_id: str + thread_id: Optional[str] = None + metadata: Dict[str, Any] = Field(default_factory=dict) diff --git a/backend/app/agents/testing/agent_tester.py b/backend/app/agents/testing/agent_tester.py new file mode 100644 index 0000000..1007aad --- /dev/null +++ b/backend/app/agents/testing/agent_tester.py @@ -0,0 +1,32 @@ +""" +Agent Tester +============ + +Test agent responses end-to-end. +""" + +from typing import Dict, Any, List +from sqlalchemy.ext.asyncio import AsyncSession +from app.agents.runtime.agent_runtime import get_agent_runtime + +class AgentTester: + """ + Test agent functionality end-to-end. + """ + def __init__(self): + self.runtime = get_agent_runtime() + + async def test_agent(self, db: AsyncSession, agent_id: int, message: str, user_id: int) -> Dict[str, Any]: + """ + Simulate a conversation with an agent. + """ + try: + result = await self.runtime.execute_agent(db, agent_id, message, user_id) + return { + "success": True, + "response": result.response, + "tool_calls": result.tool_calls, + "duration_ms": result.duration_ms + } + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/backend/app/agents/testing/provider_tester.py b/backend/app/agents/testing/provider_tester.py new file mode 100644 index 0000000..f485975 --- /dev/null +++ b/backend/app/agents/testing/provider_tester.py @@ -0,0 +1,27 @@ +""" +Provider Tester +=============== + +Test LLM providers. +""" + +from typing import Tuple +from app.agents.providers.provider_router import ProviderRouter + +class ProviderTester: + """ + Test LLM providers. + """ + + async def test_provider(self, provider_name: str, model_name: str) -> Tuple[bool, str]: + """ + Test if a provider is working. + """ + try: + llm = ProviderRouter().get_langchain_model(provider_name, model_name) + # Simple ping + response = await llm.ainvoke("Hello, this is a connectivity test. Respond with 'OK'.") + content = response.content if hasattr(response, 'content') else str(response) + return True, str(content) + except Exception as e: + return False, str(e) diff --git a/backend/app/agents/testing/tool_tester.py b/backend/app/agents/testing/tool_tester.py new file mode 100644 index 0000000..ff0ca67 --- /dev/null +++ b/backend/app/agents/testing/tool_tester.py @@ -0,0 +1,28 @@ +""" +Tool Tester +=========== + +Test tools functionality. +""" + +from typing import Dict, Any, Optional +from app.agents.tools.tool_registry import get_tool_registry +from app.agents.tools.tool_executor import ToolExecutor + +class ToolTester: + """ + Test agent tools. + """ + + async def test_tool(self, tool_name: str, args: Dict[str, Any], permissions: Optional[Dict[str, bool]] = None) -> Dict[str, Any]: + """ + Run a tool with test arguments. + """ + registry = get_tool_registry() + executor = ToolExecutor(registry) + + # If testing, usually we want to bypass permission checks or pass minimal ones + # But we allow passing permissions + + result = await executor.execute(tool_name, args, agent_permissions=permissions) + return result.model_dump() diff --git a/backend/app/agents/tools/__init__.py b/backend/app/agents/tools/__init__.py new file mode 100644 index 0000000..bf63a90 --- /dev/null +++ b/backend/app/agents/tools/__init__.py @@ -0,0 +1,16 @@ +""" +Tool System Package +=================== + +Agent tools management and execution. +""" + +from app.agents.tools.tool_registry import ToolRegistry, get_tool_registry +from app.agents.tools.base import BaseTool, ToolResult + +__all__ = [ + "ToolRegistry", + "get_tool_registry", + "BaseTool", + "ToolResult", +] diff --git a/backend/app/agents/orchestrator/tools/base.py b/backend/app/agents/tools/base.py similarity index 100% rename from backend/app/agents/orchestrator/tools/base.py rename to backend/app/agents/tools/base.py diff --git a/backend/app/agents/tools/builtin/__init__.py b/backend/app/agents/tools/builtin/__init__.py new file mode 100644 index 0000000..b75296b --- /dev/null +++ b/backend/app/agents/tools/builtin/__init__.py @@ -0,0 +1,22 @@ +""" +Built-in Tools Package +====================== + +Built-in tools for agent operations. +""" + +from app.agents.tools.builtin import github_tools +from app.agents.tools.builtin import slack_tools +from app.agents.tools.builtin import teams_tools +from app.agents.tools.builtin import telegram_tools +from app.agents.tools.builtin import web_tools +from app.agents.tools.builtin import file_tools + +__all__ = [ + "github_tools", + "slack_tools", + "teams_tools", + "telegram_tools", + "web_tools", + "file_tools", +] diff --git a/backend/app/agents/orchestrator/tools/builtin/file_tools.py b/backend/app/agents/tools/builtin/file_tools.py similarity index 99% rename from backend/app/agents/orchestrator/tools/builtin/file_tools.py rename to backend/app/agents/tools/builtin/file_tools.py index 9096143..4b1b1cd 100644 --- a/backend/app/agents/orchestrator/tools/builtin/file_tools.py +++ b/backend/app/agents/tools/builtin/file_tools.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Type -from app.agents.orchestrator.tools.base import BaseTool, ToolResult +from app.agents.tools.base import BaseTool, ToolResult logger = logging.getLogger(__name__) diff --git a/backend/app/agents/orchestrator/tools/builtin/github_tools.py b/backend/app/agents/tools/builtin/github_tools.py similarity index 99% rename from backend/app/agents/orchestrator/tools/builtin/github_tools.py rename to backend/app/agents/tools/builtin/github_tools.py index 163b6f0..3d06f5d 100644 --- a/backend/app/agents/orchestrator/tools/builtin/github_tools.py +++ b/backend/app/agents/tools/builtin/github_tools.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Type import httpx -from app.agents.orchestrator.tools.base import BaseTool, ToolResult +from app.agents.tools.base import BaseTool, ToolResult from app.agents.orchestrator.config import get_orchestrator_config logger = logging.getLogger(__name__) diff --git a/backend/app/agents/orchestrator/tools/builtin/slack_tools.py b/backend/app/agents/tools/builtin/slack_tools.py similarity index 99% rename from backend/app/agents/orchestrator/tools/builtin/slack_tools.py rename to backend/app/agents/tools/builtin/slack_tools.py index 05edb2c..b5bd45c 100644 --- a/backend/app/agents/orchestrator/tools/builtin/slack_tools.py +++ b/backend/app/agents/tools/builtin/slack_tools.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Type import httpx -from app.agents.orchestrator.tools.base import BaseTool, ToolResult +from app.agents.tools.base import BaseTool, ToolResult from app.agents.orchestrator.config import get_orchestrator_config logger = logging.getLogger(__name__) diff --git a/backend/app/agents/orchestrator/tools/builtin/teams_tools.py b/backend/app/agents/tools/builtin/teams_tools.py similarity index 98% rename from backend/app/agents/orchestrator/tools/builtin/teams_tools.py rename to backend/app/agents/tools/builtin/teams_tools.py index 7081e3c..6322cb8 100644 --- a/backend/app/agents/orchestrator/tools/builtin/teams_tools.py +++ b/backend/app/agents/tools/builtin/teams_tools.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Type import httpx -from app.agents.orchestrator.tools.base import BaseTool, ToolResult +from app.agents.tools.base import BaseTool, ToolResult from app.agents.orchestrator.config import get_orchestrator_config logger = logging.getLogger(__name__) diff --git a/backend/app/agents/orchestrator/tools/builtin/telegram_tools.py b/backend/app/agents/tools/builtin/telegram_tools.py similarity index 99% rename from backend/app/agents/orchestrator/tools/builtin/telegram_tools.py rename to backend/app/agents/tools/builtin/telegram_tools.py index b75ec64..8de3868 100644 --- a/backend/app/agents/orchestrator/tools/builtin/telegram_tools.py +++ b/backend/app/agents/tools/builtin/telegram_tools.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Type import httpx -from app.agents.orchestrator.tools.base import BaseTool, ToolResult +from app.agents.tools.base import BaseTool, ToolResult from app.agents.orchestrator.config import get_orchestrator_config logger = logging.getLogger(__name__) diff --git a/backend/app/agents/orchestrator/tools/builtin/web_tools.py b/backend/app/agents/tools/builtin/web_tools.py similarity index 99% rename from backend/app/agents/orchestrator/tools/builtin/web_tools.py rename to backend/app/agents/tools/builtin/web_tools.py index 048e26b..f345bbf 100644 --- a/backend/app/agents/orchestrator/tools/builtin/web_tools.py +++ b/backend/app/agents/tools/builtin/web_tools.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Type import httpx -from app.agents.orchestrator.tools.base import BaseTool, ToolResult +from app.agents.tools.base import BaseTool, ToolResult logger = logging.getLogger(__name__) diff --git a/backend/app/agents/tools/tool_executor.py b/backend/app/agents/tools/tool_executor.py new file mode 100644 index 0000000..aea332a --- /dev/null +++ b/backend/app/agents/tools/tool_executor.py @@ -0,0 +1,109 @@ +""" +Tool Executor +============= + +Executes tools safely with validation, permissions, and error handling. +""" + +import asyncio +import logging +from typing import Any, Dict, List, Optional, Union +from langchain_core.tools import BaseTool as LangChainBaseTool + +from app.agents.tools.base import BaseTool, ToolResult +from app.agents.orchestrator.exceptions import ToolExecutionError, PermissionDeniedError +from app.agents.tools.tool_registry import ToolRegistry, get_tool_registry + +logger = logging.getLogger(__name__) + +class ToolExecutor: + """ + Executes tools safely with validation and error handling. + """ + + def __init__(self, registry: Optional[ToolRegistry] = None): + self.registry = registry or get_tool_registry() + + async def execute( + self, + tool_name: str, + tool_args: Dict[str, Any], + agent_permissions: Optional[Dict[str, bool]] = None, + timeout: int = 30 + ) -> ToolResult: + """ + Execute a tool safely. + """ + try: + # 1. Get Tool + tool_instance = None + is_internal = False + + try: + # First try getting as BaseTool (internal wrapper) + # This might raise ToolNotFoundError if not found + tool_instance = self.registry.get_tool(tool_name) + is_internal = True + except Exception: + # Maybe it's a dynamic LangChain tool (like MCP) + tool_instance = self.registry.get_mcp_tool(tool_name) + is_internal = False + + if not tool_instance: + return ToolResult( + data=f"Error: Tool {tool_name} not found.", + error="Tool not found", + success=False + ) + + # 2. Permission Check (only for internal tools that support it) + if is_internal and agent_permissions: + if not tool_instance.validate_permissions(agent_permissions): + return ToolResult( + data=f"Error: Permission denied for tool {tool_name}.", + error="Permission denied", + success=False + ) + + # 3. Execution with Timeout + # We need to distinguish between our BaseTool and LangChain BaseTool + if is_internal: + # It's our BaseTool wrapper + # BaseTool.execute returns ToolResult + result = await asyncio.wait_for( + tool_instance.execute(**tool_args), + timeout=timeout + ) + return result + else: + # It's a LangChain BaseTool (e.g. MCP) + # We need to run it and wrap result + try: + output = await asyncio.wait_for( + tool_instance.ainvoke(tool_args), + timeout=timeout + ) + return ToolResult( + data=str(output), + success=True + ) + except Exception as e: + return ToolResult( + data=f"Error executing tool {tool_name}: {e}", + error=str(e), + success=False + ) + + except asyncio.TimeoutError: + return ToolResult( + data=f"Error: Tool {tool_name} timed out after {timeout}s.", + error="Timeout", + success=False + ) + except Exception as e: + logger.error(f"Tool execution error: {e}", exc_info=True) + return ToolResult( + data=f"Error: {e}", + error=str(e), + success=False + ) diff --git a/backend/app/agents/orchestrator/tools/registry.py b/backend/app/agents/tools/tool_registry.py similarity index 98% rename from backend/app/agents/orchestrator/tools/registry.py rename to backend/app/agents/tools/tool_registry.py index fbfd37f..3c42352 100644 --- a/backend/app/agents/orchestrator/tools/registry.py +++ b/backend/app/agents/tools/tool_registry.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Type from langchain_core.tools import BaseTool as LangChainBaseTool, StructuredTool -from app.agents.orchestrator.tools.base import BaseTool, ToolResult +from app.agents.tools.base import BaseTool, ToolResult from app.agents.orchestrator.exceptions import ToolNotFoundError from app.agents.orchestrator.config import get_orchestrator_config from app.services.mcp_client import MCPClient, MCPTool @@ -236,7 +236,7 @@ def load_builtin_tools(self) -> None: return # Import and register built-in tools - from app.agents.orchestrator.tools.builtin import ( + from app.agents.tools.builtin import ( github_tools, slack_tools, teams_tools, diff --git a/backend/app/agents/tools/tool_validator.py b/backend/app/agents/tools/tool_validator.py new file mode 100644 index 0000000..fb2a873 --- /dev/null +++ b/backend/app/agents/tools/tool_validator.py @@ -0,0 +1,19 @@ +""" +Tool Validator +============== + +Validates tool usage and schemas. +""" + +from typing import Any, Dict, List +from pydantic import ValidationError + +class ToolValidator: + """ + Validates tool usage and schemas. + """ + + @staticmethod + def validate_args(tool_schema: Dict[str, Any], args: Dict[str, Any]) -> bool: + # This is a placeholder. Real validation would use Pydantic or jsonschema + return True diff --git a/backend/app/ai/rag/rag_service.py b/backend/app/ai/rag/rag_service.py deleted file mode 100644 index 4de0449..0000000 --- a/backend/app/ai/rag/rag_service.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -RAG Service -=========== - -High-level service API for RAG operations: retrieval and optional synchronous ingestion (if needed). -Most ingestion happens asynchronously via Celery. -""" - -from typing import List, Optional, Dict, Any -from app.ai.rag.retriever import get_retriever -# from app.models.rag import RagDocument - -class RAGService: - """Service layer for RAG operations.""" - - def __init__(self): - self.retriever = get_retriever() - - async def retrieve_context(self, query: str, k: int = 5, filter: Optional[Dict[str, Any]] = None) -> List[str]: - """ - Retrieve context text chunks for a given user query. - This is used by the Agent Orchestrator. - """ - return await self.retriever.retrieve(query, k=k, filter=filter) - - async def retrieve_with_metadata(self, query: str, k: int = 5, filter: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: - """ - Retrieve context with metadata. - """ - return await self.retriever.retrieve_with_metadata(query, k=k, filter=filter) - - # Note: Ingestion is now handled by app.worker.tasks.rag.process_rag_document - -# Global instance -_rag_service = None - -def get_rag_service() -> RAGService: - global _rag_service - if _rag_service is None: - _rag_service = RAGService() - return _rag_service diff --git a/backend/app/api/v1/routers/agent_builder.py b/backend/app/api/v1/routers/agent_builder.py index 63c0340..493b400 100644 --- a/backend/app/api/v1/routers/agent_builder.py +++ b/backend/app/api/v1/routers/agent_builder.py @@ -39,7 +39,7 @@ # Execution AgentExecutionRequest, AgentExecutionResponse, ) -from app.agents.orchestrator.core import get_orchestrator +from app.agents.orchestrator.orchestrator import get_orchestrator logger = logging.getLogger(__name__) router = APIRouter() diff --git a/backend/app/api/v1/routers/agent_chat.py b/backend/app/api/v1/routers/agent_chat.py index e97de59..23fcce9 100644 --- a/backend/app/api/v1/routers/agent_chat.py +++ b/backend/app/api/v1/routers/agent_chat.py @@ -29,7 +29,7 @@ ChatFileResponse, FileUploadResponse, ) -from app.agents.orchestrator.core import get_orchestrator +from app.agents.orchestrator.orchestrator import get_orchestrator logger = logging.getLogger(__name__) router = APIRouter() diff --git a/backend/app/api/v1/routers/agents.py b/backend/app/api/v1/routers/agents.py index 9c38f26..2f72bcd 100644 --- a/backend/app/api/v1/routers/agents.py +++ b/backend/app/api/v1/routers/agents.py @@ -26,7 +26,7 @@ LLMProviderEnum, ) from app.agents.orchestrator.service import AgentService -from app.agents.orchestrator.core import get_orchestrator +from app.agents.orchestrator.orchestrator import get_orchestrator from app.agents.orchestrator.exceptions import ( AgentNotFoundError, OrchestratorError, diff --git a/backend/app/worker/tasks/rag.py b/backend/app/worker/tasks/rag.py index 474ac9e..9f349a2 100644 --- a/backend/app/worker/tasks/rag.py +++ b/backend/app/worker/tasks/rag.py @@ -4,8 +4,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_db from app.models.rag import RagDocument, RagChunk -from app.ai.rag.embeddings.embedding_service import EmbeddingService -from app.ai.rag.vectorstore.pgvector_store import PGVectorStore +from app.agents.rag.embedding_service import EmbeddingService +from app.agents.rag.vectorstore.pgvector_store import PGVectorStore from app.core.config import settings # Text Extraction (Simple implementation for now) diff --git a/backend/tests/fixtures/auth_fixtures.py b/backend/tests/fixtures/auth_fixtures.py index 27838fa..2b41ff0 100644 --- a/backend/tests/fixtures/auth_fixtures.py +++ b/backend/tests/fixtures/auth_fixtures.py @@ -25,3 +25,8 @@ async def admin_user_headers(auth_headers, admin_user): async def manager_user_headers(auth_headers, manager_user): """Auth headers for a manager user.""" return auth_headers(manager_user) + +@pytest_asyncio.fixture +async def super_user_headers(auth_headers, super_user): + """Auth headers for a super user.""" + return auth_headers(super_user)