Spaces:
Sleeping
Sleeping
| """ | |
| Agent tools and information management | |
| This module contains: | |
| - Agent metadata and configuration | |
| - Agent singleton registry | |
| - Functions to get available agents | |
| - Agent switching tool creation | |
| - Tool conversion utilities for OpenAI function format | |
| """ | |
| import inspect | |
| import logging | |
| from typing import Dict, List, Optional, Callable, Any, get_type_hints | |
| from langchain.agents import Tool | |
| from langchain.tools import StructuredTool | |
| from langchain_openai import ChatOpenAI | |
| from pydantic import BaseModel, Field | |
| logger = logging.getLogger(__name__) | |
| # Agent information dictionary - single source of truth for all agents | |
| AGENT_INFO = { | |
| "document_reader": { | |
| "display_name": "Document Reader", | |
| "description": "Handles documentation queries with RAG capabilities", | |
| "tools": { | |
| "read": ["search_version_documentation", "search_all_versions", "list_available_versions"], | |
| "write": [] # No write operations | |
| } | |
| }, | |
| "profile_settings": { | |
| "display_name": "Profile Settings", | |
| "description": "Manages user preferences and settings", | |
| "tools": { | |
| "read": ["view_profile", "list_preferences", "view_notification_settings"], | |
| "write": ["update_preference", "update_notifications", "reset_profile"] | |
| } | |
| } | |
| } | |
| # Global agent singleton registry | |
| _AGENT_INSTANCES: Dict[str, Any] = {} | |
| _LLM: Optional[ChatOpenAI] = None | |
| _API_GATEWAY = None | |
| _CURRENT_CONTEXT: Dict[str, Any] = {} # Store context for switching tools | |
| def initialize_agents(llm: ChatOpenAI, api_gateway=None) -> None: | |
| """ | |
| Initialize all agent singletons | |
| Args: | |
| llm: The language model to use for all agents | |
| api_gateway: Optional API gateway for certain agents | |
| """ | |
| global _AGENT_INSTANCES, _LLM, _API_GATEWAY | |
| # Avoid circular imports by importing here | |
| from agents.document_reader import DocumentReaderAgent | |
| from agents.profile_settings import ProfileSettingsAgent | |
| _LLM = llm | |
| _API_GATEWAY = api_gateway | |
| logger.info("Initializing agent singletons...") | |
| # Create agent instances (only implemented ones) | |
| _AGENT_INSTANCES = { | |
| "document_reader": DocumentReaderAgent(llm), | |
| "profile_settings": ProfileSettingsAgent(llm), | |
| } | |
| logger.info(f"Initialized {len(_AGENT_INSTANCES)} agents") | |
| def get_agent(agent_id: str) -> Optional[Any]: | |
| """ | |
| Get agent instance by ID | |
| Args: | |
| agent_id: The agent identifier | |
| Returns: | |
| Agent instance or None if not found | |
| """ | |
| return _AGENT_INSTANCES.get(agent_id) | |
| def run_agent(agent_id: str, message: str, context: Optional[Dict] = None) -> Dict[str, Any]: | |
| """ | |
| Run an agent with a message | |
| Args: | |
| agent_id: The agent identifier | |
| message: The message to process | |
| context: Optional context dictionary | |
| Returns: | |
| Agent response dictionary | |
| """ | |
| agent = get_agent(agent_id) | |
| if not agent: | |
| raise ValueError(f"Unknown agent: {agent_id}") | |
| # Store context globally for switching tools to access | |
| global _CURRENT_CONTEXT | |
| _CURRENT_CONTEXT = { | |
| "message": message, | |
| "context": context or {} | |
| } | |
| return agent.run(message, context) | |
| def get_available_agents(exclude_agent_id: Optional[str] = None) -> Dict[str, Dict]: | |
| """ | |
| Get all available agents, optionally excluding one | |
| Args: | |
| exclude_agent_id: Agent ID to exclude from results | |
| Returns: | |
| Dictionary of agent_id -> agent_info | |
| """ | |
| return { | |
| agent_id: info | |
| for agent_id, info in AGENT_INFO.items() | |
| if agent_id != exclude_agent_id | |
| } | |
| def create_agent_switching_tool(target_agent_id: str, target_agent_name: str) -> Callable: | |
| """ | |
| Create a tool function for switching to a specific agent | |
| Args: | |
| target_agent_id: ID of the agent to switch to | |
| target_agent_name: Display name of the agent | |
| Returns: | |
| Callable function that performs the agent switch | |
| """ | |
| def switch_to_agent(reason: str = "") -> str: | |
| """Switch conversation to another agent. | |
| Args: | |
| reason: Brief explanation of why switching (e.g., 'User wants to update profile settings') | |
| """ | |
| # Get current context | |
| global _CURRENT_CONTEXT | |
| message = _CURRENT_CONTEXT.get("message", "") | |
| context = _CURRENT_CONTEXT.get("context", {}) | |
| logger.info(f"Switching from current agent to {target_agent_id}") | |
| # Log the transfer | |
| transfer_msg = f"Transferring to {target_agent_name}" | |
| if reason: | |
| transfer_msg += f": {reason}" | |
| try: | |
| # Run the target agent with the current message | |
| result = run_agent(target_agent_id, message, context) | |
| # Return a special marker with the target agent's response | |
| # This tells BaseAgent to update agent_id and use this response | |
| return f"__SWITCH_AGENT__|{target_agent_id}|{transfer_msg}\n\n{result.get('output', '')}" | |
| except Exception as e: | |
| logger.error(f"Error during agent switch: {e}") | |
| return f"__SWITCH_AGENT__|{target_agent_id}|{transfer_msg}\n\n[Error: Failed to run {target_agent_name} - {str(e)}]" | |
| # Set function metadata for LangChain | |
| switch_to_agent.__name__ = f"switch_to_{target_agent_id}" | |
| switch_to_agent.__doc__ = f"Transfer conversation to {target_agent_name} agent. Always provide a reason parameter explaining why you're switching (e.g., 'User wants to update profile settings')." | |
| return switch_to_agent | |
| def create_switching_tools_for_agent(current_agent_id: str) -> List[StructuredTool]: | |
| """ | |
| Create all agent switching tools for a given agent | |
| Args: | |
| current_agent_id: ID of the current agent | |
| Returns: | |
| List of StructuredTool objects for switching to other agents | |
| """ | |
| tools = [] | |
| other_agents = get_available_agents(exclude_agent_id=current_agent_id) | |
| for agent_id, agent_info in other_agents.items(): | |
| # Create a dynamic Pydantic model for this specific agent | |
| class SwitchToAgentInput(BaseModel): | |
| reason: str = Field( | |
| default="", | |
| description=f"Brief explanation of why switching to {agent_info['display_name']} (e.g., 'User wants to update profile settings')" | |
| ) | |
| # Set the class name dynamically for better debugging | |
| SwitchToAgentInput.__name__ = f"SwitchTo{agent_id.title().replace('_', '')}Input" | |
| switch_func = create_agent_switching_tool( | |
| agent_id, | |
| agent_info['display_name'] | |
| ) | |
| tool = StructuredTool.from_function( | |
| func=switch_func, | |
| name=f"switch_to_{agent_id}", | |
| description=f"Transfer to {agent_info['display_name']} - {agent_info['description']}", | |
| args_schema=SwitchToAgentInput | |
| ) | |
| tools.append(tool) | |
| return tools | |
| def get_agent_tools(agent_id: str) -> Dict[str, List[str]]: | |
| """ | |
| Get the tools (read and write) for a specific agent | |
| Args: | |
| agent_id: ID of the agent | |
| Returns: | |
| Dictionary with 'read' and 'write' tool lists | |
| """ | |
| if agent_id not in AGENT_INFO: | |
| raise ValueError(f"Unknown agent ID: {agent_id}") | |
| return AGENT_INFO[agent_id]["tools"] | |
| def is_write_tool(agent_id: str, tool_name: str) -> bool: | |
| """ | |
| Check if a tool is a write operation (requires confirmation) | |
| Args: | |
| agent_id: ID of the agent | |
| tool_name: Name of the tool | |
| Returns: | |
| True if the tool is a write operation | |
| """ | |
| agent_tools = get_agent_tools(agent_id) | |
| return tool_name in agent_tools.get("write", []) | |
| def convert_tool_to_openai_function(tool: Any) -> Dict[str, Any]: | |
| """ | |
| Convert a LangChain Tool or StructuredTool to OpenAI function format. | |
| Handles both Tool and StructuredTool instances appropriately. | |
| """ | |
| # Check if this is a StructuredTool with args_schema | |
| if hasattr(tool, 'args_schema') and tool.args_schema: | |
| # Use the Pydantic schema directly | |
| schema = tool.args_schema.schema() | |
| # Extract properties and required fields from Pydantic schema | |
| properties = {} | |
| required = [] | |
| for field_name, field_info in schema.get('properties', {}).items(): | |
| properties[field_name] = field_info | |
| # Get required fields from schema | |
| required = schema.get('required', []) | |
| return { | |
| "name": tool.name, | |
| "description": tool.description, | |
| "parameters": { | |
| "type": "object", | |
| "properties": properties, | |
| "required": required | |
| } | |
| } | |
| # Fall back to signature introspection for regular Tool | |
| func = tool.func | |
| sig = inspect.signature(func) | |
| type_hints = get_type_hints(func) | |
| # Build properties from function parameters | |
| properties = {} | |
| required = [] | |
| for param_name, param in sig.parameters.items(): | |
| # Skip 'self' parameter if it exists | |
| if param_name == 'self': | |
| continue | |
| # Determine the type | |
| param_type = type_hints.get(param_name, type(param.default) if param.default != param.empty else str) | |
| # Convert Python types to JSON Schema types | |
| if param_type == str: | |
| json_type = "string" | |
| elif param_type == int: | |
| json_type = "integer" | |
| elif param_type == float: | |
| json_type = "number" | |
| elif param_type == bool: | |
| json_type = "boolean" | |
| elif param_type == list or str(param_type).startswith('typing.List'): | |
| json_type = "array" | |
| elif param_type == dict or str(param_type).startswith('typing.Dict'): | |
| json_type = "object" | |
| else: | |
| json_type = "string" # Default fallback | |
| # Build property definition | |
| prop_def = {"type": json_type} | |
| # Add description from docstring if available | |
| if func.__doc__: | |
| # Simple extraction - could be enhanced with docstring parsing | |
| prop_def["description"] = f"Parameter: {param_name}" | |
| # Add default value if present | |
| if param.default != param.empty: | |
| prop_def["default"] = param.default | |
| else: | |
| # If no default, it's required | |
| required.append(param_name) | |
| properties[param_name] = prop_def | |
| # Return the OpenAI function schema | |
| return { | |
| "name": tool.name, | |
| "description": tool.description, | |
| "parameters": { | |
| "type": "object", | |
| "properties": properties, | |
| "required": required | |
| } | |
| } |