solana-agent 11.3.0__py3-none-any.whl → 12.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
solana_agent/ai.py CHANGED
@@ -12,6 +12,7 @@ This module implements a clean architecture approach with:
12
12
 
13
13
  import asyncio
14
14
  import datetime
15
+ import importlib
15
16
  import json
16
17
  import re
17
18
  import traceback
@@ -30,7 +31,7 @@ from typing import (
30
31
  Any,
31
32
  Type,
32
33
  )
33
- from pydantic import BaseModel, Field
34
+ from pydantic import BaseModel, Field, ValidationError
34
35
  from pymongo import MongoClient
35
36
  from openai import OpenAI
36
37
  import pymongo
@@ -45,6 +46,73 @@ from abc import ABC, abstractmethod
45
46
  # DOMAIN MODELS
46
47
  #############################################
47
48
 
49
+ class ToolCallModel(BaseModel):
50
+ """Model for tool calls in agent responses."""
51
+ name: str = Field(..., description="Name of the tool to execute")
52
+ parameters: Dict[str, Any] = Field(
53
+ default_factory=dict, description="Parameters for the tool")
54
+
55
+
56
+ class ToolInstructionModel(BaseModel):
57
+ """Model for tool usage instructions in system prompts."""
58
+ available_tools: List[Dict[str, Any]] = Field(default_factory=list,
59
+ description="Tools available to this agent")
60
+ example_tool: str = Field("search_internet",
61
+ description="Example tool to use in instructions")
62
+ example_query: str = Field("latest Solana news",
63
+ description="Example query to use in instructions")
64
+ valid_agents: List[str] = Field(default_factory=list,
65
+ description="List of valid agents for handoff")
66
+
67
+ def format_instructions(self) -> str:
68
+ """Format the tool and handoff instructions using plain text delimiters."""
69
+ tools_json = json.dumps(self.available_tools, indent=2)
70
+
71
+ # Tool usage instructions with plain text delimiters
72
+ tool_instructions = f"""
73
+ You have access to the following tools:
74
+ {tools_json}
75
+
76
+ IMPORTANT - TOOL USAGE: When you need to use a tool, respond with JSON using these exact plain text delimiters:
77
+
78
+ TOOL_START
79
+ {{
80
+ "name": "tool_name",
81
+ "parameters": {{
82
+ "param1": "value1",
83
+ "param2": "value2"
84
+ }}
85
+ }}
86
+ TOOL_END
87
+
88
+ Example: To search the internet for "{self.example_query}", respond with:
89
+
90
+ TOOL_START
91
+ {{
92
+ "name": "{self.example_tool}",
93
+ "parameters": {{
94
+ "query": "{self.example_query}"
95
+ }}
96
+ }}
97
+ TOOL_END
98
+
99
+ ALWAYS use the search_internet tool when the user asks for current information or facts that might be beyond your knowledge cutoff. DO NOT attempt to handoff for information that could be obtained using search_internet.
100
+ """
101
+
102
+ # Handoff instructions if valid agents are provided
103
+ handoff_instructions = ""
104
+ if self.valid_agents:
105
+ handoff_instructions = f"""
106
+ IMPORTANT - HANDOFFS: You can ONLY hand off to these existing agents: {", ".join(self.valid_agents)}
107
+ DO NOT invent or reference agents that don't exist in this list.
108
+
109
+ To hand off to another agent, use this format:
110
+ {{"handoff": {{"target_agent": "<AGENT_NAME_FROM_LIST_ABOVE>", "reason": "detailed reason for handoff"}}}}
111
+ """
112
+
113
+ return f"{tool_instructions}\n\n{handoff_instructions}"
114
+
115
+
48
116
  class AgentType(str, Enum):
49
117
  """Type of agent (AI or Human)."""
50
118
  AI = "ai"
@@ -742,8 +810,13 @@ class MemoryRepository(Protocol):
742
810
  class AgentRegistry(Protocol):
743
811
  """Interface for agent management."""
744
812
 
745
- def register_ai_agent(self, name: str, agent: Any,
746
- specialization: str) -> None: ...
813
+ def register_ai_agent(
814
+ self,
815
+ name: str,
816
+ instructions: str,
817
+ specialization: str,
818
+ model: str = "gpt-4o-mini",
819
+ ) -> None: ...
747
820
 
748
821
  def register_human_agent(
749
822
  self,
@@ -1010,6 +1083,7 @@ class OpenAIAdapter:
1010
1083
  messages.append({"role": "user", "content": prompt})
1011
1084
 
1012
1085
  try:
1086
+ # First try the beta parsing API
1013
1087
  completion = self.client.beta.chat.completions.parse(
1014
1088
  model=kwargs.get("model", self.model),
1015
1089
  messages=messages,
@@ -1018,7 +1092,25 @@ class OpenAIAdapter:
1018
1092
  )
1019
1093
  return completion.choices[0].message.parsed
1020
1094
  except Exception as e:
1021
- print(f"Error parsing structured output: {e}")
1095
+ print(f"Error with beta.parse method: {e}")
1096
+
1097
+ # Fallback to manual parsing with Pydantic
1098
+ try:
1099
+ response = self.client.chat.completions.create(
1100
+ model=kwargs.get("model", self.model),
1101
+ messages=messages,
1102
+ temperature=kwargs.get("temperature", 0.2),
1103
+ response_format={"type": "json_object"},
1104
+ )
1105
+ response_text = response.choices[0].message.content
1106
+
1107
+ if response_text:
1108
+ # Use Pydantic's parse_raw method instead of json.loads
1109
+ return model_class.parse_raw(response_text)
1110
+
1111
+ except Exception as e:
1112
+ print(f"Error parsing structured output with Pydantic: {e}")
1113
+
1022
1114
  # Return default instance as fallback
1023
1115
  return model_class()
1024
1116
 
@@ -1492,16 +1584,26 @@ class MongoAIAgentRegistry:
1492
1584
  }
1493
1585
 
1494
1586
  def delete_agent(self, name: str) -> bool:
1495
- """Delete an AI agent by name."""
1587
+ """Delete an AI agent from the registry."""
1588
+ # First check if agent exists
1496
1589
  if name not in self.ai_agents_cache:
1497
1590
  return False
1498
1591
 
1499
- # Remove from cache
1500
- del self.ai_agents_cache[name]
1592
+ # Delete from database
1593
+ result = self.db.delete_one(
1594
+ self.collection,
1595
+ {"name": name}
1596
+ )
1501
1597
 
1502
- # Remove from database
1503
- self.db.delete_one(self.collection, {"name": name})
1504
- return True
1598
+ # Delete from cache if database deletion was successful
1599
+ if result:
1600
+ if name in self.ai_agents_cache:
1601
+ del self.ai_agents_cache[name]
1602
+ print(f"Agent {name} successfully deleted from MongoDB and cache")
1603
+ return True
1604
+ else:
1605
+ print(f"Failed to delete agent {name} from MongoDB")
1606
+ return False
1505
1607
 
1506
1608
 
1507
1609
  class MongoHumanAgentRegistry:
@@ -1688,13 +1790,13 @@ class MongoTicketRepository:
1688
1790
  """Count tickets matching query."""
1689
1791
  return self.db.count_documents(self.collection, query)
1690
1792
 
1691
- def find_stalled_tickets(self, cutoff_time, statuses):
1793
+ async def find_stalled_tickets(self, cutoff_time, statuses):
1692
1794
  """Find tickets that haven't been updated since the cutoff time."""
1693
1795
  query = {
1694
1796
  "status": {"$in": [status.value if isinstance(status, Enum) else status for status in statuses]},
1695
1797
  "updated_at": {"$lt": cutoff_time}
1696
1798
  }
1697
- tickets = self.db_adapter.find("tickets", query)
1799
+ tickets = self.db.find("tickets", query)
1698
1800
  return [Ticket(**ticket) for ticket in tickets]
1699
1801
 
1700
1802
 
@@ -2279,79 +2381,91 @@ class RoutingService:
2279
2381
  self.router_model = router_model
2280
2382
 
2281
2383
  async def route_query(self, query: str) -> str:
2282
- """Route query to the most appropriate AI agent."""
2384
+ """Route a query to the appropriate agent based on content."""
2385
+ # Get available agents
2386
+ agents = self.agent_registry.get_all_ai_agents()
2387
+ if not agents:
2388
+ return "default_agent" # Fallback to default if no agents
2389
+
2390
+ agent_names = list(agents.keys())
2391
+
2392
+ # Format agent descriptions for prompt
2393
+ agent_descriptions = []
2283
2394
  specializations = self.agent_registry.get_specializations()
2284
- # Get AI-only specializations
2285
- ai_specialists = {
2286
- k: v
2287
- for k, v in specializations.items()
2288
- if k in self.agent_registry.get_all_ai_agents()
2289
- }
2290
2395
 
2291
- # Create routing prompt
2396
+ for name in agent_names:
2397
+ spec = specializations.get(name, "General assistant")
2398
+ agent_descriptions.append(f"- {name}: {spec}")
2399
+
2400
+ agent_info = "\n".join(agent_descriptions)
2401
+
2402
+ # Create prompt for routing
2292
2403
  prompt = f"""
2293
- Analyze this user query and determine the MOST APPROPRIATE AI specialist.
2404
+ You are a router that determines which AI agent should handle a user query.
2294
2405
 
2295
2406
  User query: "{query}"
2296
2407
 
2297
- Available AI specialists:
2298
- {json.dumps(ai_specialists, indent=2)}
2408
+ Available agents:
2409
+ {agent_info}
2299
2410
 
2300
- CRITICAL INSTRUCTIONS:
2301
- 1. Choose specialists based on domain expertise match.
2302
- 2. Return EXACTLY ONE specialist name from the available list.
2303
- 3. Do not invent new specialist names.
2411
+ Select the most appropriate agent based on the query and agent specializations.
2412
+ Respond with ONLY the agent name, nothing else.
2304
2413
  """
2305
2414
 
2306
- # Generate routing decision using structured output
2307
- response = ""
2308
- async for chunk in self.llm_provider.generate_text(
2309
- "router",
2310
- prompt,
2311
- system_prompt="You are a routing system that matches queries to the best specialist.",
2312
- stream=False,
2313
- model=self.router_model,
2314
- temperature=0.2,
2315
- response_format={"type": "json_object"},
2316
- ):
2317
- response += chunk
2318
-
2415
+ response_text = ""
2319
2416
  try:
2320
- data = json.loads(response)
2321
- selected_agent = data.get("selected_agent", "")
2417
+ async for chunk in self.llm_provider.generate_text(
2418
+ "system",
2419
+ prompt,
2420
+ system_prompt="You are a routing system. Only respond with the name of the most appropriate agent.",
2421
+ model=self.router_model,
2422
+ temperature=0.1,
2423
+ ):
2424
+ response_text += chunk
2322
2425
 
2323
- # Fallback to matching if needed
2324
- if selected_agent not in ai_specialists:
2325
- agent_name = self._match_agent_name(
2326
- selected_agent, list(ai_specialists.keys())
2327
- )
2328
- else:
2329
- agent_name = selected_agent
2426
+ # Clean up the response text to handle different formats
2427
+ response_text = response_text.strip()
2428
+
2429
+ # First try to parse as JSON (old behavior)
2430
+ try:
2431
+ data = json.loads(response_text)
2432
+ if isinstance(data, dict) and "agent" in data:
2433
+ return self._match_agent_name(data["agent"], agent_names)
2434
+ except json.JSONDecodeError:
2435
+ # Not JSON, try to parse as plain text
2436
+ pass
2437
+
2438
+ # Treat as plain text - just match the agent name directly
2439
+ return self._match_agent_name(response_text, agent_names)
2330
2440
 
2331
- return agent_name
2332
2441
  except Exception as e:
2333
- print(f"Error parsing routing decision: {e}")
2334
- # Fallback to the old matching method
2335
- return self._match_agent_name(response.strip(), list(ai_specialists.keys()))
2442
+ print(f"Error in routing: {e}")
2443
+ # Default to the first agent if there's an error
2444
+ return agent_names[0]
2336
2445
 
2337
2446
  def _match_agent_name(self, response: str, agent_names: List[str]) -> str:
2338
- """Match router response to an actual AI agent name."""
2339
- # Exact match (priority)
2340
- if response in agent_names:
2341
- return response
2447
+ """Match the response to a valid agent name."""
2448
+ # Clean up the response
2449
+ if isinstance(response, dict) and "name" in response:
2450
+ response = response["name"] # Handle {"name": "agent_name"} format
2342
2451
 
2343
- # Case-insensitive match
2452
+ # Convert to string and clean it up
2453
+ clean_response = str(response).strip().lower()
2454
+
2455
+ # Direct match first
2344
2456
  for name in agent_names:
2345
- if name.lower() == response.lower():
2457
+ if name.lower() == clean_response:
2346
2458
  return name
2347
2459
 
2348
- # Partial match
2460
+ # Check for partial matches
2349
2461
  for name in agent_names:
2350
- if name.lower() in response.lower():
2462
+ if name.lower() in clean_response or clean_response in name.lower():
2351
2463
  return name
2352
2464
 
2353
- # Fallback to first AI agent
2354
- return agent_names[0] if agent_names else "default"
2465
+ # If no match, return first agent as default
2466
+ print(
2467
+ f"No matching agent found for: '{response}'. Using {agent_names[0]}")
2468
+ return agent_names[0]
2355
2469
 
2356
2470
 
2357
2471
  class TicketService:
@@ -2672,21 +2786,14 @@ class CriticService:
2672
2786
  class NotificationService:
2673
2787
  """Service for sending notifications to human agents or users using notification plugins."""
2674
2788
 
2675
- def __init__(self, human_agent_registry: MongoHumanAgentRegistry):
2789
+ def __init__(self, human_agent_registry: MongoHumanAgentRegistry, tool_registry=None):
2676
2790
  """Initialize the notification service with a human agent registry."""
2677
2791
  self.human_agent_registry = human_agent_registry
2792
+ self.tool_registry = tool_registry
2678
2793
 
2679
2794
  def send_notification(self, recipient_id: str, message: str, metadata: Dict[str, Any] = None) -> bool:
2680
2795
  """
2681
2796
  Send a notification to a human agent using configured notification channels or legacy handler.
2682
-
2683
- Args:
2684
- recipient_id: ID of the human agent to notify
2685
- message: Notification message content
2686
- metadata: Additional data related to the notification (e.g., ticket_id)
2687
-
2688
- Returns:
2689
- True if notification was sent, False otherwise
2690
2797
  """
2691
2798
  # Get human agent information
2692
2799
  agent = self.human_agent_registry.get_human_agent(recipient_id)
@@ -2712,6 +2819,11 @@ class NotificationService:
2712
2819
  f"No notification channels configured for agent {recipient_id}")
2713
2820
  return False
2714
2821
 
2822
+ # No tool registry available
2823
+ if not self.tool_registry:
2824
+ print("No tool registry available for notifications")
2825
+ return False
2826
+
2715
2827
  # Try each notification channel until one succeeds
2716
2828
  success = False
2717
2829
  for channel in notification_channels:
@@ -2728,9 +2840,9 @@ class NotificationService:
2728
2840
  if metadata:
2729
2841
  tool_params["metadata"] = metadata
2730
2842
 
2731
- result = tool_registry.get_tool(f"notify_{channel_type}")
2732
- if result:
2733
- response = result.execute(**tool_params)
2843
+ tool = self.tool_registry.get_tool(f"notify_{channel_type}")
2844
+ if tool:
2845
+ response = tool.execute(**tool_params)
2734
2846
  if response.get("status") == "success":
2735
2847
  success = True
2736
2848
  break
@@ -2763,11 +2875,15 @@ class AgentService:
2763
2875
  human_agent_registry: Optional[MongoHumanAgentRegistry] = None,
2764
2876
  ai_agent_registry: Optional[MongoAIAgentRegistry] = None,
2765
2877
  organization_mission: Optional[OrganizationMission] = None,
2878
+ config: Optional[Dict[str, Any]] = None
2766
2879
  ):
2880
+ """Initialize the agent service with LLM provider and optional registries."""
2767
2881
  self.llm_provider = llm_provider
2768
2882
  self.human_agent_registry = human_agent_registry
2769
2883
  self.ai_agent_registry = ai_agent_registry
2770
2884
  self.organization_mission = organization_mission
2885
+ self.config = config or {}
2886
+ self._last_handoff = None
2771
2887
 
2772
2888
  # For backward compatibility
2773
2889
  self.ai_agents = {}
@@ -2776,9 +2892,22 @@ class AgentService:
2776
2892
 
2777
2893
  self.specializations = {}
2778
2894
 
2779
- # Initialize plugin system
2780
- self.plugin_manager = PluginManager()
2781
- self.plugin_manager.load_all_plugins()
2895
+ # Create our tool registry and plugin manager
2896
+ self.tool_registry = ToolRegistry()
2897
+ self.plugin_manager = PluginManager(
2898
+ config=self.config, tool_registry=self.tool_registry)
2899
+
2900
+ # Load plugins
2901
+ loaded_count = self.plugin_manager.load_all_plugins()
2902
+ print(
2903
+ f"Loaded {loaded_count} plugins with {len(self.tool_registry.list_all_tools())} registered tools")
2904
+
2905
+ # Configure all tools with our config after loading
2906
+ self.tool_registry.configure_all_tools(self.config)
2907
+
2908
+ # Debug output of registered tools
2909
+ print(
2910
+ f"Available tools after initialization: {self.tool_registry.list_all_tools()}")
2782
2911
 
2783
2912
  # If human agent registry is provided, initialize specializations from it
2784
2913
  if self.human_agent_registry:
@@ -2794,6 +2923,205 @@ class AgentService:
2794
2923
  if not self.human_agent_registry:
2795
2924
  self.human_agents = {}
2796
2925
 
2926
+ def get_all_ai_agents(self) -> Dict[str, Any]:
2927
+ """Get all registered AI agents."""
2928
+ return self.ai_agents
2929
+
2930
+ def get_agent_tools(self, agent_name: str) -> List[Dict[str, Any]]:
2931
+ """Get all tools available to a specific agent."""
2932
+ return self.tool_registry.get_agent_tools(agent_name)
2933
+
2934
+ def register_tool_for_agent(self, agent_name: str, tool_name: str) -> None:
2935
+ """Give an agent access to a specific tool."""
2936
+ # Make sure the tool exists
2937
+ if tool_name not in self.tool_registry.list_all_tools():
2938
+ print(
2939
+ f"Error registering tool {tool_name} for agent {agent_name}: Tool not registered")
2940
+ raise ValueError(f"Tool {tool_name} is not registered")
2941
+
2942
+ # Check if agent exists
2943
+ if agent_name not in self.ai_agents and (
2944
+ not self.ai_agent_registry or
2945
+ not self.ai_agent_registry.get_ai_agent(agent_name)
2946
+ ):
2947
+ print(
2948
+ f"Warning: Agent {agent_name} not found but attempting to register tool")
2949
+
2950
+ # Assign the tool to the agent
2951
+ success = self.tool_registry.assign_tool_to_agent(
2952
+ agent_name, tool_name)
2953
+
2954
+ if success:
2955
+ print(
2956
+ f"Successfully registered tool {tool_name} for agent {agent_name}")
2957
+ else:
2958
+ print(
2959
+ f"Failed to register tool {tool_name} for agent {agent_name}")
2960
+
2961
+ def process_json_response(self, response_text: str, agent_name: str) -> str:
2962
+ """Process a complete response to handle any JSON handoffs or tool calls."""
2963
+ # Check if the response is a JSON object for handoff or tool call
2964
+ if response_text.strip().startswith('{') and ('"handoff":' in response_text or '"tool_call":' in response_text):
2965
+ try:
2966
+ data = json.loads(response_text.strip())
2967
+
2968
+ # Handle handoff
2969
+ if "handoff" in data:
2970
+ target_agent = data["handoff"].get(
2971
+ "target_agent", "another agent")
2972
+ reason = data["handoff"].get(
2973
+ "reason", "to better assist with your request")
2974
+ return f"I'll connect you with {target_agent} who can better assist with your request. Reason: {reason}"
2975
+
2976
+ # Handle tool call
2977
+ if "tool_call" in data:
2978
+ tool_data = data["tool_call"]
2979
+ tool_name = tool_data.get("name")
2980
+ parameters = tool_data.get("parameters", {})
2981
+
2982
+ if tool_name:
2983
+ try:
2984
+ # Execute the tool
2985
+ tool_result = self.execute_tool(
2986
+ agent_name, tool_name, parameters)
2987
+
2988
+ # Format the result
2989
+ if tool_result.get("status") == "success":
2990
+ return f"I searched for information and found:\n\n{tool_result.get('result', '')}"
2991
+ else:
2992
+ return f"I tried to search for information, but encountered an error: {tool_result.get('message', 'Unknown error')}"
2993
+ except Exception as e:
2994
+ return f"I tried to use {tool_name}, but encountered an error: {str(e)}"
2995
+ except json.JSONDecodeError:
2996
+ # Not valid JSON
2997
+ pass
2998
+
2999
+ # Return original if not JSON or if processing fails
3000
+ return response_text
3001
+
3002
+ def get_agent_system_prompt(self, agent_name: str) -> str:
3003
+ """Get the system prompt for an agent, including tool instructions if available."""
3004
+ # Get the agent's base instructions
3005
+ if agent_name not in self.ai_agents:
3006
+ raise ValueError(f"Agent {agent_name} not found")
3007
+
3008
+ agent_config = self.ai_agents[agent_name]
3009
+ instructions = agent_config.get("instructions", "")
3010
+
3011
+ # Add tool instructions if any tools are available
3012
+ available_tools = self.get_agent_tools(agent_name)
3013
+ if available_tools:
3014
+ tools_json = json.dumps(available_tools, indent=2)
3015
+
3016
+ # Tool instructions using JSON format similar to handoffs
3017
+ tool_instructions = f"""
3018
+ You have access to the following tools:
3019
+ {tools_json}
3020
+
3021
+ IMPORTANT - TOOL USAGE: When you need to use a tool, respond with a JSON object using this format:
3022
+
3023
+ {{
3024
+ "tool_call": {{
3025
+ "name": "tool_name",
3026
+ "parameters": {{
3027
+ "param1": "value1",
3028
+ "param2": "value2"
3029
+ }}
3030
+ }}
3031
+ }}
3032
+
3033
+ Example: To search the internet for "latest Solana news", respond with:
3034
+
3035
+ {{
3036
+ "tool_call": {{
3037
+ "name": "search_internet",
3038
+ "parameters": {{
3039
+ "query": "latest Solana news"
3040
+ }}
3041
+ }}
3042
+ }}
3043
+
3044
+ ALWAYS use the search_internet tool when the user asks for current information or facts that might be beyond your knowledge cutoff. DO NOT attempt to handoff for information that could be obtained using search_internet.
3045
+ """
3046
+ instructions = f"{instructions}\n\n{tool_instructions}"
3047
+
3048
+ # Add specific instructions about valid handoff agents
3049
+ valid_agents = list(self.ai_agents.keys())
3050
+ if valid_agents:
3051
+ handoff_instructions = f"""
3052
+ IMPORTANT - HANDOFFS: You can ONLY hand off to these existing agents: {', '.join(valid_agents)}
3053
+ DO NOT invent or reference agents that don't exist in this list.
3054
+
3055
+ To hand off to another agent, use this format:
3056
+ {{"handoff": {{"target_agent": "<AGENT_NAME_FROM_LIST_ABOVE>", "reason": "detailed reason for handoff"}}}}
3057
+ """
3058
+ instructions = f"{instructions}\n\n{handoff_instructions}"
3059
+
3060
+ return instructions
3061
+
3062
+ def process_tool_calls(self, agent_name: str, response_text: str) -> str:
3063
+ """Process any tool calls in the agent's response and return updated response."""
3064
+ # Regex to find tool calls in the format TOOL_START {...} TOOL_END
3065
+ tool_pattern = r"TOOL_START\s*([\s\S]*?)\s*TOOL_END"
3066
+ tool_matches = re.findall(tool_pattern, response_text)
3067
+
3068
+ if not tool_matches:
3069
+ return response_text
3070
+
3071
+ print(
3072
+ f"Found {len(tool_matches)} tool calls in response from {agent_name}")
3073
+
3074
+ # Process each tool call
3075
+ modified_response = response_text
3076
+ for tool_json in tool_matches:
3077
+ try:
3078
+ # Parse the tool call JSON
3079
+ tool_call_text = tool_json.strip()
3080
+ print(f"Processing tool call: {tool_call_text[:100]}")
3081
+
3082
+ # Parse the JSON (handle both normal and stringified JSON)
3083
+ try:
3084
+ tool_call = json.loads(tool_call_text)
3085
+ except json.JSONDecodeError as e:
3086
+ # If there are escaped quotes or formatting issues, try cleaning it up
3087
+ cleaned_json = tool_call_text.replace(
3088
+ '\\"', '"').replace('\\n', '\n')
3089
+ tool_call = json.loads(cleaned_json)
3090
+
3091
+ tool_name = tool_call.get("name")
3092
+ parameters = tool_call.get("parameters", {})
3093
+
3094
+ if tool_name:
3095
+ # Execute the tool
3096
+ print(
3097
+ f"Executing tool {tool_name} with parameters: {parameters}")
3098
+ tool_result = self.execute_tool(
3099
+ agent_name, tool_name, parameters)
3100
+
3101
+ # Format the result for inclusion in the response
3102
+ if tool_result.get("status") == "success":
3103
+ formatted_result = f"\n\nI searched for information and found:\n\n{tool_result.get('result', '')}"
3104
+ else:
3105
+ formatted_result = f"\n\nI tried to search for information, but encountered an error: {tool_result.get('message', 'Unknown error')}"
3106
+
3107
+ # Replace the entire tool block with the result
3108
+ full_tool_block = f"TOOL_START\n{tool_json}\nTOOL_END"
3109
+ modified_response = modified_response.replace(
3110
+ full_tool_block, formatted_result)
3111
+ print(f"Successfully processed tool call: {tool_name}")
3112
+ except Exception as e:
3113
+ print(f"Error processing tool call: {str(e)}")
3114
+ # Replace with error message
3115
+ full_tool_block = f"TOOL_START\n{tool_json}\nTOOL_END"
3116
+ modified_response = modified_response.replace(
3117
+ full_tool_block, "\n\nI tried to search for information, but encountered an error processing the tool call.")
3118
+
3119
+ return modified_response
3120
+
3121
+ def get_agent_tools(self, agent_name: str) -> List[Dict[str, Any]]:
3122
+ """Get all tools available to a specific agent."""
3123
+ return self.tool_registry.get_agent_tools(agent_name)
3124
+
2797
3125
  def register_ai_agent(
2798
3126
  self,
2799
3127
  name: str,
@@ -2819,7 +3147,7 @@ class AgentService:
2819
3147
  # Use registry if available
2820
3148
  if self.ai_agent_registry:
2821
3149
  self.ai_agent_registry.register_ai_agent(
2822
- name, full_instructions, specialization, model
3150
+ name, full_instructions, specialization, model,
2823
3151
  )
2824
3152
  # Update local cache for backward compatibility
2825
3153
  self.ai_agents = self.ai_agent_registry.get_all_ai_agents()
@@ -2839,32 +3167,37 @@ class AgentService:
2839
3167
  return merged
2840
3168
  return self.specializations
2841
3169
 
2842
- def register_tool_for_agent(self, agent_name: str, tool_name: str) -> None:
2843
- """Give an agent access to a specific tool."""
2844
- if agent_name not in self.ai_agents:
2845
- raise ValueError(f"Agent {agent_name} not found")
2846
-
2847
- tool_registry.assign_tool_to_agent(agent_name, tool_name)
3170
+ def execute_tool(self, agent_name: str, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
3171
+ """Execute a tool on behalf of an agent."""
3172
+ print(f"Executing tool {tool_name} for agent {agent_name}")
3173
+ print(f"Parameters: {parameters}")
2848
3174
 
2849
- def get_agent_tools(self, agent_name: str) -> List[Dict[str, Any]]:
2850
- """Get all tools available to a specific agent."""
2851
- return tool_registry.get_agent_tools(agent_name)
3175
+ # Get the tool directly from the registry
3176
+ tool = self.tool_registry.get_tool(tool_name)
3177
+ if not tool:
3178
+ print(
3179
+ f"Tool {tool_name} not found in registry. Available tools: {self.tool_registry.list_all_tools()}")
3180
+ return {"status": "error", "message": f"Tool {tool_name} not found"}
2852
3181
 
2853
- def execute_tool(
2854
- self, agent_name: str, tool_name: str, parameters: Dict[str, Any]
2855
- ) -> Dict[str, Any]:
2856
- """Execute a tool on behalf of an agent."""
2857
- # Check if agent has access to this tool
2858
- agent_tools = tool_registry.get_agent_tools(agent_name)
2859
- tool_names = [tool["name"] for tool in agent_tools]
3182
+ # Check if agent has access
3183
+ agent_tools = self.get_agent_tools(agent_name)
3184
+ tool_names = [t["name"] for t in agent_tools]
2860
3185
 
2861
3186
  if tool_name not in tool_names:
2862
- raise ValueError(
2863
- f"Agent {agent_name} does not have access to tool {tool_name}"
2864
- )
3187
+ print(
3188
+ f"Agent {agent_name} does not have access to tool {tool_name}. Available tools: {tool_names}")
3189
+ return {"status": "error", "message": f"Agent {agent_name} does not have access to tool {tool_name}"}
2865
3190
 
2866
- # Execute the tool
2867
- return self.plugin_manager.execute_tool(tool_name, **parameters)
3191
+ # Execute the tool with parameters
3192
+ try:
3193
+ print(
3194
+ f"Executing {tool_name} with config: {'API key present' if hasattr(tool, '_api_key') and tool._api_key else 'No API key'}")
3195
+ result = tool.execute(**parameters)
3196
+ print(f"Tool execution result: {result.get('status', 'unknown')}")
3197
+ return result
3198
+ except Exception as e:
3199
+ print(f"Error executing tool {tool_name}: {str(e)}")
3200
+ return {"status": "error", "message": f"Error: {str(e)}"}
2868
3201
 
2869
3202
  def register_human_agent(
2870
3203
  self,
@@ -2921,69 +3254,109 @@ class AgentService:
2921
3254
 
2922
3255
  agent_config = self.ai_agents[agent_name]
2923
3256
 
2924
- # Get instructions and add memory context
2925
- instructions = agent_config["instructions"]
3257
+ # Get the properly formatted system prompt with tools and handoff instructions
3258
+ instructions = self.get_agent_system_prompt(agent_name)
3259
+
3260
+ # Add memory context
2926
3261
  if memory_context:
2927
3262
  instructions += f"\n\nUser context and history:\n{memory_context}"
2928
3263
 
2929
- # Add tool information if agent has any tools
2930
- tools = self.get_agent_tools(agent_name)
2931
- if tools and "tools" not in kwargs:
2932
- kwargs["tools"] = tools
2933
-
2934
- # Add specific instruction for simple queries to prevent handoff format leakage
2935
- if len(query.strip()) < 10:
2936
- instructions += "\n\nIMPORTANT: If the user sends a simple test message, respond conversationally. Never output raw JSON handoff objects directly to the user."
3264
+ # Add critical instruction to prevent raw JSON
3265
+ instructions += "\n\nCRITICAL: When using tools or making handoffs, ALWAYS respond with properly formatted JSON as instructed."
2937
3266
 
2938
3267
  # Generate response
2939
- response_text = ""
2940
- async for chunk in self.llm_provider.generate_text(
2941
- user_id=user_id,
2942
- prompt=query,
2943
- system_prompt=instructions,
2944
- model=agent_config["model"],
2945
- **kwargs,
2946
- ):
2947
- # Filter out raw handoff JSON before yielding to user
2948
- if not response_text and chunk.strip().startswith('{"handoff":'):
2949
- # If we're starting with a handoff JSON, replace with a proper response
2950
- yield "Hello! I'm here to help. What can I assist you with today?"
2951
- response_text += chunk # Still store it for processing later
2952
- else:
2953
- yield chunk
2954
- response_text += chunk
3268
+ tool_json_found = False
3269
+ full_response = ""
2955
3270
 
2956
- # Process handoffs after yielding response (unchanged code)
2957
3271
  try:
2958
- response_data = json.loads(response_text)
2959
- if "tool_calls" in response_data:
2960
- for tool_call in response_data["tool_calls"]:
2961
- # Extract tool name and arguments
2962
- if isinstance(tool_call, dict):
2963
- # Direct format
2964
- tool_name = tool_call.get("name")
2965
- params = tool_call.get("parameters", {})
2966
-
2967
- # For the updated OpenAI format
2968
- if "function" in tool_call:
2969
- function_data = tool_call["function"]
2970
- tool_name = function_data.get("name")
2971
- try:
2972
- params = json.loads(
2973
- function_data.get("arguments", "{}"))
2974
- except Exception:
2975
- params = {}
3272
+ async for chunk in self.llm_provider.generate_text(
3273
+ user_id=user_id,
3274
+ prompt=query,
3275
+ system_prompt=instructions,
3276
+ model=agent_config["model"],
3277
+ **kwargs,
3278
+ ):
3279
+ # Add to full response
3280
+ full_response += chunk
3281
+
3282
+ # Check if this might be JSON
3283
+ if full_response.strip().startswith("{") and not tool_json_found:
3284
+ tool_json_found = True
3285
+ print(
3286
+ f"Detected potential JSON response starting with: {full_response[:50]}...")
3287
+ continue
3288
+
3289
+ # If not JSON, yield the chunk
3290
+ if not tool_json_found:
3291
+ yield chunk
3292
+
3293
+ # Process JSON if found
3294
+ if tool_json_found:
3295
+ try:
3296
+ print(
3297
+ f"Processing JSON response: {full_response[:100]}...")
3298
+ data = json.loads(full_response.strip())
3299
+ print(
3300
+ f"Successfully parsed JSON with keys: {list(data.keys())}")
3301
+
3302
+ # Handle tool call
3303
+ if "tool_call" in data:
3304
+ tool_data = data["tool_call"]
3305
+ tool_name = tool_data.get("name")
3306
+ parameters = tool_data.get("parameters", {})
3307
+
3308
+ print(
3309
+ f"Processing tool call: {tool_name} with parameters: {parameters}")
2976
3310
 
2977
- # Execute the tool
2978
3311
  if tool_name:
2979
- self.execute_tool(agent_name, tool_name, params)
2980
- except Exception:
2981
- # If it's not JSON or doesn't have tool_calls, we've already yielded the response
2982
- pass
3312
+ try:
3313
+ # Execute tool
3314
+ print(f"Executing tool: {tool_name}")
3315
+ tool_result = self.execute_tool(
3316
+ agent_name, tool_name, parameters)
3317
+ print(
3318
+ f"Tool execution result status: {tool_result.get('status')}")
3319
+
3320
+ if tool_result.get("status") == "success":
3321
+ print(
3322
+ f"Tool executed successfully - yielding result")
3323
+ yield tool_result.get('result', '')
3324
+ else:
3325
+ print(
3326
+ f"Tool execution failed: {tool_result.get('message')}")
3327
+ yield f"Error: {tool_result.get('message', 'Unknown error')}"
3328
+ except Exception as e:
3329
+ print(f"Tool execution exception: {str(e)}")
3330
+ yield f"Error executing tool: {str(e)}"
3331
+
3332
+ # Handle handoff
3333
+ elif "handoff" in data:
3334
+ print(
3335
+ f"Processing handoff to: {data['handoff'].get('target_agent')}")
3336
+ # Store handoff data but don't yield anything
3337
+ self._last_handoff = data["handoff"]
3338
+ return
2983
3339
 
2984
- def get_all_ai_agents(self) -> Dict[str, Any]:
2985
- """Get all registered AI agents."""
2986
- return self.ai_agents
3340
+ # If we got JSON but it's not a tool call or handoff, yield it as text
3341
+ else:
3342
+ print(
3343
+ f"Received JSON but not a tool call or handoff. Keys: {list(data.keys())}")
3344
+ yield full_response
3345
+
3346
+ except json.JSONDecodeError as e:
3347
+ # Not valid JSON, yield it as is
3348
+ print(f"JSON parse error: {str(e)} - yielding as text")
3349
+ yield full_response
3350
+
3351
+ # If nothing has been yielded yet (e.g., failed JSON parsing), yield the full response
3352
+ if not tool_json_found:
3353
+ print(f"Non-JSON response handled normally")
3354
+
3355
+ except Exception as e:
3356
+ print(f"Error in generate_response: {str(e)}")
3357
+ import traceback
3358
+ print(traceback.format_exc())
3359
+ yield f"I'm sorry, I encountered an error: {str(e)}"
2987
3360
 
2988
3361
 
2989
3362
  class ResourceService:
@@ -5214,28 +5587,18 @@ class QueryProcessor:
5214
5587
  self._stalled_ticket_task = loop.create_task(
5215
5588
  self._run_stalled_ticket_checks())
5216
5589
  except RuntimeError:
5217
- # No running event loop - likely in test environment
5218
- # Instead of just passing, log a message for clarity
5219
5590
  import logging
5220
5591
  logging.warning(
5221
5592
  "No running event loop available for stalled ticket checker.")
5222
- # Don't try to create the task - this prevents the coroutine warning
5223
5593
 
5224
5594
  try:
5225
- # Handle human agent messages differently
5226
- if await self._is_human_agent(user_id):
5227
- async for chunk in self._process_human_agent_message(
5228
- user_id, user_text
5229
- ):
5230
- yield chunk
5231
- return
5232
-
5233
- # Handle simple greetings without full agent routing
5234
- if await self._is_simple_greeting(user_text):
5235
- greeting_response = await self._generate_greeting_response(
5236
- user_id, user_text
5237
- )
5238
- yield greeting_response
5595
+ # Special case for "test" and other very simple messages
5596
+ if user_text.strip().lower() in ["test", "hello", "hi", "hey", "ping"]:
5597
+ response = f"Hello! How can I help you today?"
5598
+ yield response
5599
+ # Store this simple interaction in memory
5600
+ if self.memory_provider:
5601
+ await self._store_conversation(user_id, user_text, response)
5239
5602
  return
5240
5603
 
5241
5604
  # Handle system commands
@@ -5244,100 +5607,66 @@ class QueryProcessor:
5244
5607
  yield command_response
5245
5608
  return
5246
5609
 
5610
+ # Route to appropriate agent
5611
+ agent_name = await self.routing_service.route_query(user_text)
5612
+
5247
5613
  # Check for active ticket
5248
5614
  active_ticket = self.ticket_service.ticket_repository.get_active_for_user(
5249
- user_id
5250
- )
5615
+ user_id)
5251
5616
 
5252
5617
  if active_ticket:
5253
5618
  # Process existing ticket
5254
- async for chunk in self._process_existing_ticket(
5255
- user_id, user_text, active_ticket, timezone
5256
- ):
5257
- yield chunk
5619
+ try:
5620
+ response_buffer = ""
5621
+ async for chunk in self._process_existing_ticket(user_id, user_text, active_ticket, timezone):
5622
+ response_buffer += chunk
5623
+ yield chunk
5624
+
5625
+ # Check final response for unprocessed JSON
5626
+ if response_buffer.strip().startswith('{'):
5627
+ agent_name = active_ticket.assigned_to or "default_agent"
5628
+ processed_response = self.agent_service.process_json_response(
5629
+ response_buffer, agent_name)
5630
+ if processed_response != response_buffer:
5631
+ yield "\n\n" + processed_response
5632
+ except ValueError as e:
5633
+ if "Ticket" in str(e) and "not found" in str(e):
5634
+ # Ticket no longer exists - create a new one
5635
+ complexity = await self._assess_task_complexity(user_text)
5636
+ async for chunk in self._process_new_ticket(user_id, user_text, complexity, timezone):
5637
+ yield chunk
5638
+ else:
5639
+ yield f"I'm sorry, I encountered an error: {str(e)}"
5640
+
5258
5641
  else:
5259
5642
  # Create new ticket
5260
- complexity = await self._assess_task_complexity(user_text)
5261
-
5262
- # Process as new ticket
5263
- async for chunk in self._process_new_ticket(
5264
- user_id, user_text, complexity, timezone
5265
- ):
5266
- yield chunk
5643
+ try:
5644
+ complexity = await self._assess_task_complexity(user_text)
5645
+
5646
+ # Process as new ticket
5647
+ response_buffer = ""
5648
+ async for chunk in self._process_new_ticket(user_id, user_text, complexity, timezone):
5649
+ response_buffer += chunk
5650
+ yield chunk
5651
+
5652
+ # Check final response for unprocessed JSON
5653
+ if response_buffer.strip().startswith('{'):
5654
+ processed_response = self.agent_service.process_json_response(
5655
+ response_buffer, agent_name)
5656
+ if processed_response != response_buffer:
5657
+ yield "\n\n" + processed_response
5658
+ except Exception as e:
5659
+ yield f"I'm sorry, I encountered an error: {str(e)}"
5267
5660
 
5268
5661
  except Exception as e:
5269
5662
  print(f"Error in request processing: {str(e)}")
5270
5663
  print(traceback.format_exc())
5271
- # Use yield instead of direct function calling to avoid coroutine warning
5272
- error_msg = "\n\nI apologize for the technical difficulty.\n\n"
5273
- yield error_msg
5664
+ yield "I apologize for the technical difficulty.\n\n"
5274
5665
 
5275
5666
  async def _is_human_agent(self, user_id: str) -> bool:
5276
5667
  """Check if the user is a registered human agent."""
5277
5668
  return user_id in self.agent_service.get_all_human_agents()
5278
5669
 
5279
- async def _is_simple_greeting(self, text: str) -> bool:
5280
- """Determine if the user message is a simple greeting."""
5281
- text_lower = text.lower().strip()
5282
-
5283
- # Common greetings list
5284
- simple_greetings = [
5285
- "hello",
5286
- "hi",
5287
- "hey",
5288
- "greetings",
5289
- "good morning",
5290
- "good afternoon",
5291
- "good evening",
5292
- "what's up",
5293
- "how are you",
5294
- "how's it going",
5295
- ]
5296
-
5297
- # Check if text starts with a greeting and is relatively short
5298
- is_greeting = any(
5299
- text_lower.startswith(greeting) for greeting in simple_greetings
5300
- )
5301
- # Arbitrary threshold for "simple" messages
5302
- is_short = len(text.split()) < 7
5303
-
5304
- return is_greeting and is_short
5305
-
5306
- async def _generate_greeting_response(self, user_id: str, text: str) -> str:
5307
- """Generate a friendly response to a simple greeting."""
5308
- # Get user context if available
5309
- context = ""
5310
- if self.memory_provider:
5311
- context = await self.memory_provider.retrieve(user_id)
5312
-
5313
- # Get first available AI agent for the greeting
5314
- first_agent_name = next(
5315
- iter(self.agent_service.get_all_ai_agents().keys()))
5316
-
5317
- response = ""
5318
- async for chunk in self.agent_service.generate_response(
5319
- first_agent_name,
5320
- user_id,
5321
- text,
5322
- context,
5323
- temperature=0.7,
5324
- max_tokens=100, # Keep it brief
5325
- ):
5326
- response += chunk
5327
-
5328
- # Store in memory if available
5329
- if self.memory_provider:
5330
- await self.memory_provider.store(
5331
- user_id,
5332
- [
5333
- {"role": "user", "content": text},
5334
- {"role": "assistant",
5335
- "content": self._truncate(response, 2500)},
5336
- ],
5337
- )
5338
-
5339
- return response
5340
-
5341
5670
  async def shutdown(self):
5342
5671
  """Clean shutdown of the query processor."""
5343
5672
  self._shutdown_event.set()
@@ -5372,34 +5701,48 @@ class QueryProcessor:
5372
5701
  if self.stalled_ticket_timeout is None:
5373
5702
  return
5374
5703
 
5375
- # Find tickets that haven't been updated in the configured time
5376
- stalled_cutoff = datetime.datetime.now(
5377
- datetime.timezone.utc) - datetime.timedelta(minutes=self.stalled_ticket_timeout)
5704
+ try:
5705
+ # Find tickets that haven't been updated in the configured time
5706
+ cutoff_time = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(
5707
+ minutes=self.stalled_ticket_timeout
5708
+ )
5378
5709
 
5379
- # Query for stalled tickets using the find_stalled_tickets method
5380
- stalled_tickets = await self.ticket_service.ticket_repository.find_stalled_tickets(
5381
- stalled_cutoff, [TicketStatus.ACTIVE, TicketStatus.TRANSFERRED]
5382
- )
5710
+ # The find_stalled_tickets method should be async
5711
+ stalled_tickets = await self.ticket_service.ticket_repository.find_stalled_tickets(
5712
+ cutoff_time, [TicketStatus.ACTIVE, TicketStatus.TRANSFERRED]
5713
+ )
5383
5714
 
5384
- for ticket in stalled_tickets:
5385
- # Re-route using routing service to find the optimal agent
5386
- new_agent = await self.routing_service.route_query(ticket.query)
5715
+ for ticket in stalled_tickets:
5716
+ print(
5717
+ f"Found stalled ticket: {ticket.id} (last updated: {ticket.updated_at})")
5387
5718
 
5388
- # Skip if the routing didn't change
5389
- if new_agent == ticket.assigned_to:
5390
- continue
5719
+ # Skip tickets without an assigned agent
5720
+ if not ticket.assigned_to:
5721
+ continue
5391
5722
 
5392
- # Process as handoff
5393
- await self.handoff_service.process_handoff(
5394
- ticket.id,
5395
- ticket.assigned_to or "unassigned",
5396
- new_agent,
5397
- f"Automatically reassigned after {self.stalled_ticket_timeout} minutes of inactivity"
5398
- )
5723
+ # Re-route the query to see if a different agent is better
5724
+ new_agent = await self.routing_service.route_query(ticket.query)
5399
5725
 
5400
- # Log the reassignment
5401
- print(
5402
- f"Stalled ticket {ticket.id} reassigned from {ticket.assigned_to or 'unassigned'} to {new_agent}")
5726
+ # Only reassign if a different agent is suggested
5727
+ if new_agent != ticket.assigned_to:
5728
+ print(
5729
+ f"Reassigning ticket {ticket.id} from {ticket.assigned_to} to {new_agent}")
5730
+
5731
+ try:
5732
+ await self.handoff_service.process_handoff(
5733
+ ticket.id,
5734
+ ticket.assigned_to,
5735
+ new_agent,
5736
+ f"Automatically reassigned after {self.stalled_ticket_timeout} minutes of inactivity"
5737
+ )
5738
+ except Exception as e:
5739
+ print(
5740
+ f"Error reassigning stalled ticket {ticket.id}: {e}")
5741
+
5742
+ except Exception as e:
5743
+ print(f"Error in stalled ticket check: {e}")
5744
+ import traceback
5745
+ print(traceback.format_exc())
5403
5746
 
5404
5747
  async def _process_system_commands(
5405
5748
  self, user_id: str, user_text: str
@@ -6114,112 +6457,104 @@ class QueryProcessor:
6114
6457
  async def _process_existing_ticket(
6115
6458
  self, user_id: str, user_text: str, ticket: Ticket, timezone: str = None
6116
6459
  ) -> AsyncGenerator[str, None]:
6117
- """Process a message for an existing ticket."""
6460
+ """
6461
+ Process a message for an existing ticket.
6462
+ Checks for handoff data and handles it with ticket-based handoff
6463
+ unless the target agent is set to skip ticket creation.
6464
+ """
6118
6465
  # Get assigned agent or re-route if needed
6119
6466
  agent_name = ticket.assigned_to
6120
-
6121
- # If no valid assignment, route to appropriate agent
6122
- if not agent_name or agent_name not in self.agent_service.get_all_ai_agents():
6467
+ if not agent_name:
6123
6468
  agent_name = await self.routing_service.route_query(user_text)
6124
- # Update ticket with new assignment
6125
6469
  self.ticket_service.update_ticket_status(
6126
- ticket.id, TicketStatus.ACTIVE, assigned_to=agent_name
6470
+ ticket.id, TicketStatus.IN_PROGRESS, assigned_to=agent_name
6127
6471
  )
6128
6472
 
6129
- # Update ticket status
6130
- self.ticket_service.update_ticket_status(
6131
- ticket.id, TicketStatus.ACTIVE)
6132
-
6133
6473
  # Get memory context if available
6134
6474
  memory_context = ""
6135
6475
  if self.memory_provider:
6136
6476
  memory_context = await self.memory_provider.retrieve(user_id)
6137
6477
 
6138
- # Generate response with streaming
6478
+ # Try to generate response
6139
6479
  full_response = ""
6140
6480
  handoff_info = None
6481
+ handoff_detected = False
6141
6482
 
6142
- async for chunk in self.agent_service.generate_response(
6143
- agent_name, user_id, user_text, memory_context, temperature=0.7
6144
- ):
6145
- yield chunk
6146
- full_response += chunk
6147
-
6148
- # Check for handoff in structured format
6149
6483
  try:
6150
- # Look for JSON handoff object in the response
6151
- handoff_match = re.search(r'{"handoff":\s*{.*?}}', full_response)
6152
- if handoff_match:
6153
- handoff_data = json.loads(handoff_match.group(0))
6154
- if "handoff" in handoff_data:
6484
+ # Generate response with streaming
6485
+ async for chunk in self.agent_service.generate_response(
6486
+ agent_name=agent_name,
6487
+ user_id=user_id,
6488
+ query=user_text,
6489
+ memory_context=memory_context,
6490
+ ):
6491
+ # Detect possible handoff signals (JSON or prefix)
6492
+ if chunk.strip().startswith("HANDOFF:") or (
6493
+ not full_response and chunk.strip().startswith("{")
6494
+ ):
6495
+ handoff_detected = True
6496
+ full_response += chunk
6497
+ continue
6498
+
6499
+ full_response += chunk
6500
+ yield chunk
6501
+
6502
+ # After response generation, handle handoff if needed
6503
+ if handoff_detected or (
6504
+ not full_response.strip()
6505
+ and hasattr(self.agent_service, "_last_handoff")
6506
+ ):
6507
+ if hasattr(self.agent_service, "_last_handoff") and self.agent_service._last_handoff:
6508
+ handoff_data = {
6509
+ "handoff": self.agent_service._last_handoff}
6155
6510
  target_agent = handoff_data["handoff"].get("target_agent")
6156
6511
  reason = handoff_data["handoff"].get("reason")
6157
- if target_agent and reason:
6512
+
6513
+ if target_agent:
6158
6514
  handoff_info = {
6159
6515
  "target": target_agent, "reason": reason}
6160
- except Exception as e:
6161
- print(f"Error parsing handoff data: {e}")
6162
-
6163
- # Fall back to old method if structured parsing fails
6164
- if "HANDOFF:" in chunk and not handoff_info:
6165
- handoff_pattern = r"HANDOFF:\s*([A-Za-z0-9_]+)\s*REASON:\s*(.+)"
6166
- match = re.search(handoff_pattern, full_response)
6167
- if match:
6168
- target_agent = match.group(1)
6169
- reason = match.group(2)
6170
- handoff_info = {"target": target_agent, "reason": reason}
6171
-
6172
- # Store conversation in memory if available
6173
- if self.memory_provider:
6174
- await self.memory_provider.store(
6175
- user_id,
6176
- [
6177
- {"role": "user", "content": user_text},
6178
- {
6179
- "role": "assistant",
6180
- "content": self._truncate(full_response, 2500),
6181
- },
6182
- ],
6183
- )
6184
6516
 
6185
- # Process handoff if detected
6186
- if handoff_info:
6187
- try:
6188
- await self.handoff_service.process_handoff(
6189
- ticket.id,
6190
- agent_name,
6191
- handoff_info["target"],
6192
- handoff_info["reason"],
6193
- )
6194
- except ValueError as e:
6195
- # If handoff fails, just continue with current agent
6196
- print(f"Handoff failed: {e}")
6517
+ await self.handoff_service.process_handoff(
6518
+ ticket.id,
6519
+ agent_name,
6520
+ handoff_info["target"],
6521
+ handoff_info["reason"],
6522
+ )
6197
6523
 
6198
- # Check if ticket can be considered resolved
6199
- if not handoff_info:
6200
- resolution = await self._check_ticket_resolution(
6201
- user_id, full_response, user_text
6202
- )
6524
+ print(
6525
+ f"Generating response from new agent: {target_agent}")
6526
+ new_response_buffer = ""
6527
+ async for chunk in self.agent_service.generate_response(
6528
+ agent_name=target_agent,
6529
+ user_id=user_id,
6530
+ query=user_text,
6531
+ memory_context=memory_context,
6532
+ ):
6533
+ new_response_buffer += chunk
6534
+ yield chunk
6203
6535
 
6204
- if resolution.status == "resolved" and resolution.confidence >= 0.7:
6205
- self.ticket_service.mark_ticket_resolved(
6206
- ticket.id,
6207
- {
6208
- "confidence": resolution.confidence,
6209
- "reasoning": resolution.reasoning,
6210
- },
6211
- )
6536
+ full_response = new_response_buffer
6212
6537
 
6213
- # Create NPS survey
6214
- self.nps_service.create_survey(user_id, ticket.id, agent_name)
6538
+ self.agent_service._last_handoff = None
6215
6539
 
6216
- # Extract and store insights in background
6217
- if full_response:
6218
- asyncio.create_task(
6219
- self._extract_and_store_insights(
6220
- user_id, {"message": user_text, "response": full_response}
6540
+ # Store conversation in memory
6541
+ if self.memory_provider:
6542
+ await self.memory_provider.store(
6543
+ user_id,
6544
+ [
6545
+ {"role": "user", "content": user_text},
6546
+ {
6547
+ "role": "assistant",
6548
+ "content": self._truncate(full_response, 2500),
6549
+ },
6550
+ ],
6221
6551
  )
6222
- )
6552
+
6553
+ except Exception as e:
6554
+ print(f"Error processing ticket: {str(e)}")
6555
+ import traceback
6556
+ print(traceback.format_exc())
6557
+ yield f"I'm sorry, I encountered an error processing your request: {str(e)}"
6223
6558
 
6224
6559
  async def _process_new_ticket(
6225
6560
  self,
@@ -6311,106 +6646,113 @@ class QueryProcessor:
6311
6646
  ticket.id, TicketStatus.ACTIVE, assigned_to=agent_name
6312
6647
  )
6313
6648
 
6314
- # Generate response with streaming
6649
+ # Generate initial response with streaming
6315
6650
  full_response = ""
6316
- handoff_info = None
6651
+ handoff_detected = False
6317
6652
 
6318
- async for chunk in self.agent_service.generate_response(
6319
- agent_name, user_id, user_text, memory_context, temperature=0.7
6320
- ):
6321
- yield chunk
6322
- full_response += chunk
6653
+ try:
6654
+ # Generate response with streaming
6655
+ async for chunk in self.agent_service.generate_response(
6656
+ agent_name, user_id, user_text, memory_context, temperature=0.7
6657
+ ):
6658
+ # Check if this looks like a JSON handoff
6659
+ if chunk.strip().startswith("{") and not handoff_detected:
6660
+ handoff_detected = True
6661
+ full_response += chunk
6662
+ continue
6323
6663
 
6324
- # Check for handoff in structured format
6325
- try:
6326
- # Look for JSON handoff object in the response
6327
- handoff_match = re.search(
6328
- r'{"handoff":\s*{.*?}}', full_response)
6329
- if handoff_match:
6330
- handoff_data = json.loads(handoff_match.group(0))
6331
- if "handoff" in handoff_data:
6332
- target_agent = handoff_data["handoff"].get(
6333
- "target_agent")
6334
- reason = handoff_data["handoff"].get("reason")
6335
- if target_agent and reason:
6336
- handoff_info = {
6337
- "target": target_agent, "reason": reason}
6338
- except Exception as e:
6339
- print(f"Error parsing handoff data: {e}")
6340
-
6341
- # Fall back to old method if structured parsing fails
6342
- if "HANDOFF:" in chunk and not handoff_info:
6343
- handoff_pattern = r"HANDOFF:\s*([A-Za-z0-9_]+)\s*REASON:\s*(.+)"
6344
- match = re.search(handoff_pattern, full_response)
6345
- if match:
6346
- target_agent = match.group(1)
6347
- reason = match.group(2)
6348
- handoff_info = {"target": target_agent, "reason": reason}
6349
-
6350
- # Store conversation in memory if available
6351
- if self.memory_provider:
6352
- await self.memory_provider.store(
6353
- user_id,
6354
- [
6355
- {"role": "user", "content": user_text},
6356
- {
6357
- "role": "assistant",
6358
- "content": self._truncate(full_response, 2500),
6359
- },
6360
- ],
6664
+ # Only yield if not a JSON chunk
6665
+ if not handoff_detected:
6666
+ yield chunk
6667
+ full_response += chunk
6668
+
6669
+ # Handle handoff if detected
6670
+ if handoff_detected or (hasattr(self.agent_service, "_last_handoff") and self.agent_service._last_handoff):
6671
+ target_agent = None
6672
+ reason = "Handoff detected"
6673
+
6674
+ # Process the handoff from _last_handoff property
6675
+ if hasattr(self.agent_service, "_last_handoff") and self.agent_service._last_handoff:
6676
+ target_agent = self.agent_service._last_handoff.get(
6677
+ "target_agent")
6678
+ reason = self.agent_service._last_handoff.get(
6679
+ "reason", "No reason provided")
6680
+
6681
+ if target_agent:
6682
+ try:
6683
+ # Process handoff and update ticket
6684
+ await self.handoff_service.process_handoff(
6685
+ ticket.id,
6686
+ agent_name,
6687
+ target_agent,
6688
+ reason,
6689
+ )
6690
+
6691
+ # Generate response from new agent
6692
+ print(
6693
+ f"Generating response from new agent after handoff: {target_agent}")
6694
+ new_response = ""
6695
+ async for chunk in self.agent_service.generate_response(
6696
+ target_agent,
6697
+ user_id,
6698
+ user_text,
6699
+ memory_context,
6700
+ temperature=0.7
6701
+ ):
6702
+ yield chunk
6703
+ new_response += chunk
6704
+
6705
+ # Update full response for storage
6706
+ full_response = new_response
6707
+ except ValueError as e:
6708
+ print(f"Handoff failed: {e}")
6709
+ yield f"\n\nNote: A handoff was attempted but failed: {str(e)}"
6710
+
6711
+ # Reset handoff state
6712
+ self.agent_service._last_handoff = None
6713
+
6714
+ # Check if ticket can be considered resolved
6715
+ resolution = await self._check_ticket_resolution(
6716
+ full_response, user_text
6361
6717
  )
6362
6718
 
6363
- # Process handoff if detected
6364
- if handoff_info:
6365
- try:
6366
- await self.handoff_service.process_handoff(
6719
+ if resolution.status == "resolved" and resolution.confidence >= 0.7:
6720
+ self.ticket_service.mark_ticket_resolved(
6367
6721
  ticket.id,
6368
- agent_name,
6369
- handoff_info["target"],
6370
- handoff_info["reason"],
6722
+ {
6723
+ "confidence": resolution.confidence,
6724
+ "reasoning": resolution.reasoning,
6725
+ },
6371
6726
  )
6372
- except ValueError as e:
6373
- print(f"Handoff failed: {e}")
6374
6727
 
6375
- # Process handoff if detected
6376
- if handoff_info:
6377
- try:
6378
- await self.handoff_service.process_handoff(
6379
- ticket.id,
6380
- agent_name,
6381
- handoff_info["target"],
6382
- handoff_info["reason"],
6383
- )
6384
- except ValueError as e:
6385
- print(f"Handoff failed: {e}")
6386
-
6387
- # Check if ticket can be considered resolved
6388
- if not handoff_info:
6389
- resolution = await self._check_ticket_resolution(
6390
- user_id, full_response, user_text
6391
- )
6392
-
6393
- if resolution.status == "resolved" and resolution.confidence >= 0.7:
6394
- self.ticket_service.mark_ticket_resolved(
6395
- ticket.id,
6396
- {
6397
- "confidence": resolution.confidence,
6398
- "reasoning": resolution.reasoning,
6399
- },
6400
- )
6728
+ # Create NPS survey
6729
+ self.nps_service.create_survey(
6730
+ user_id, ticket.id, agent_name)
6401
6731
 
6402
- # Create NPS survey
6403
- self.nps_service.create_survey(
6404
- user_id, ticket.id, agent_name)
6732
+ # Store in memory provider
6733
+ if self.memory_provider:
6734
+ await self.memory_provider.store(
6735
+ user_id,
6736
+ [
6737
+ {"role": "user", "content": user_text},
6738
+ {"role": "assistant", "content": self._truncate(
6739
+ full_response, 2500)},
6740
+ ],
6741
+ )
6405
6742
 
6406
- # Extract and store insights in background
6407
- if full_response:
6408
- asyncio.create_task(
6409
- self._extract_and_store_insights(
6410
- user_id, {"message": user_text,
6411
- "response": full_response}
6412
- )
6743
+ # Extract and store insights in background
6744
+ if full_response:
6745
+ asyncio.create_task(
6746
+ self._extract_and_store_insights(
6747
+ user_id, {"message": user_text,
6748
+ "response": full_response}
6413
6749
  )
6750
+ )
6751
+
6752
+ except Exception as e:
6753
+ print(f"Error in _process_new_ticket: {str(e)}")
6754
+ print(traceback.format_exc())
6755
+ yield f"I'm sorry, I encountered an error processing your request: {str(e)}"
6414
6756
 
6415
6757
  async def _process_human_agent_message(
6416
6758
  self, user_id: str, user_text: str
@@ -6514,7 +6856,7 @@ class QueryProcessor:
6514
6856
  return result
6515
6857
 
6516
6858
  async def _check_ticket_resolution(
6517
- self, user_id: str, response: str, query: str
6859
+ self, response: str, query: str
6518
6860
  ) -> TicketResolution:
6519
6861
  """Determine if a ticket can be considered resolved based on the response."""
6520
6862
  # Get first AI agent for analysis
@@ -6532,39 +6874,48 @@ class QueryProcessor:
6532
6874
  2. "needs_followup" - The assistant couldn't fully address the issue or more information is needed
6533
6875
  3. "cannot_determine" - Cannot tell if the issue is resolved
6534
6876
 
6535
- Return a JSON object with:
6877
+ Return a structured output with:
6536
6878
  - "status": One of the above values
6537
6879
  - "confidence": A score from 0.0 to 1.0 indicating confidence in this assessment
6538
6880
  - "reasoning": Brief explanation for your decision
6539
6881
  - "suggested_actions": Array of recommended next steps (if any)
6540
6882
  """
6541
6883
 
6542
- # Generate resolution assessment
6543
- resolution_text = ""
6544
- async for chunk in self.agent_service.generate_response(
6545
- first_agent,
6546
- "resolution_checker",
6547
- prompt,
6548
- "", # No memory context needed
6549
- stream=False,
6550
- temperature=0.2,
6551
- response_format={"type": "json_object"},
6552
- ):
6553
- resolution_text += chunk
6554
-
6555
6884
  try:
6556
- data = json.loads(resolution_text)
6557
- return TicketResolution(**data)
6558
- except Exception as e:
6559
- print(f"Error parsing resolution decision: {e}")
6560
- return TicketResolution(
6561
- status="cannot_determine",
6562
- confidence=0.2,
6563
- reasoning="Failed to analyze resolution status",
6885
+ # Use structured output parsing with the Pydantic model directly
6886
+ resolution = await self.agent_service.llm_provider.parse_structured_output(
6887
+ prompt=prompt,
6888
+ system_prompt="You are a resolution analysis system. Analyze conversations and determine if queries have been resolved.",
6889
+ model_class=TicketResolution,
6890
+ model=self.agent_service.ai_agents[first_agent].get(
6891
+ "model", "gpt-4o-mini"),
6892
+ temperature=0.2,
6564
6893
  )
6894
+ return resolution
6895
+ except Exception as e:
6896
+ print(f"Exception in resolution check: {e}")
6897
+
6898
+ # Default fallback if anything fails
6899
+ return TicketResolution(
6900
+ status="cannot_determine",
6901
+ confidence=0.2,
6902
+ reasoning="Failed to analyze resolution status",
6903
+ suggested_actions=["Review conversation manually"]
6904
+ )
6565
6905
 
6566
6906
  async def _assess_task_complexity(self, query: str) -> Dict[str, Any]:
6567
6907
  """Assess the complexity of a task using standardized metrics."""
6908
+ # Special handling for very simple messages
6909
+ if len(query.strip()) <= 10 and query.lower().strip() in ["test", "hello", "hi", "hey", "ping", "thanks"]:
6910
+ print(f"Using pre-defined complexity for simple message: {query}")
6911
+ return {
6912
+ "t_shirt_size": "XS",
6913
+ "story_points": 1,
6914
+ "estimated_minutes": 5,
6915
+ "technical_complexity": 1,
6916
+ "domain_knowledge": 1,
6917
+ }
6918
+
6568
6919
  # Get first AI agent for analysis
6569
6920
  first_agent = next(iter(self.agent_service.get_all_ai_agents().keys()))
6570
6921
 
@@ -6594,16 +6945,28 @@ class QueryProcessor:
6594
6945
  ):
6595
6946
  response_text += chunk
6596
6947
 
6948
+ if not response_text.strip():
6949
+ print("Empty response from complexity assessment")
6950
+ return {
6951
+ "t_shirt_size": "S",
6952
+ "story_points": 2,
6953
+ "estimated_minutes": 15,
6954
+ "technical_complexity": 3,
6955
+ "domain_knowledge": 2,
6956
+ }
6957
+
6597
6958
  complexity_data = json.loads(response_text)
6959
+ print(f"Successfully parsed complexity: {complexity_data}")
6598
6960
  return complexity_data
6599
6961
  except Exception as e:
6600
6962
  print(f"Error assessing complexity: {e}")
6963
+ print(f"Failed response text: '{response_text}'")
6601
6964
  return {
6602
- "t_shirt_size": "M",
6603
- "story_points": 3,
6604
- "estimated_minutes": 30,
6605
- "technical_complexity": 5,
6606
- "domain_knowledge": 5,
6965
+ "t_shirt_size": "S",
6966
+ "story_points": 2,
6967
+ "estimated_minutes": 15,
6968
+ "technical_complexity": 3,
6969
+ "domain_knowledge": 2,
6607
6970
  }
6608
6971
 
6609
6972
  async def _extract_and_store_insights(
@@ -6638,6 +7001,20 @@ class QueryProcessor:
6638
7001
 
6639
7002
  return truncated + "..."
6640
7003
 
7004
+ async def _store_conversation(self, user_id: str, user_text: str, response_text: str) -> None:
7005
+ """Store conversation history in memory provider."""
7006
+ if self.memory_provider:
7007
+ try:
7008
+ await self.memory_provider.store(
7009
+ user_id,
7010
+ [
7011
+ {"role": "user", "content": user_text},
7012
+ {"role": "assistant", "content": response_text},
7013
+ ],
7014
+ )
7015
+ except Exception as e:
7016
+ print(f"Error storing conversation: {e}")
7017
+ # Don't let memory storage errors affect the user experience
6641
7018
 
6642
7019
  #############################################
6643
7020
  # FACTORY AND DEPENDENCY INJECTION
@@ -6715,16 +7092,25 @@ class SolanaAgentFactory:
6715
7092
 
6716
7093
  # Create services
6717
7094
  agent_service = AgentService(
6718
- llm_adapter, human_agent_repo, ai_agent_repo, organization_mission)
7095
+ llm_adapter, human_agent_repo, ai_agent_repo, organization_mission, config)
7096
+
7097
+ # Debug the agent service tool registry to confirm tools were registered
7098
+ print(
7099
+ f"Agent service tools after initialization: {agent_service.tool_registry.list_all_tools()}")
7100
+
6719
7101
  routing_service = RoutingService(
6720
7102
  llm_adapter,
6721
7103
  agent_service,
6722
7104
  router_model=config.get("router_model", "gpt-4o-mini"),
6723
7105
  )
7106
+
6724
7107
  ticket_service = TicketService(ticket_repo)
7108
+
6725
7109
  handoff_service = HandoffService(
6726
7110
  handoff_repo, ticket_repo, agent_service)
7111
+
6727
7112
  memory_service = MemoryService(memory_repo, llm_adapter)
7113
+
6728
7114
  nps_service = NPSService(nps_repo, ticket_repo)
6729
7115
 
6730
7116
  # Create critic service if enabled
@@ -6737,7 +7123,11 @@ class SolanaAgentFactory:
6737
7123
  ticket_repo, llm_adapter, agent_service
6738
7124
  )
6739
7125
 
6740
- notification_service = NotificationService(human_agent_repo)
7126
+ notification_service = NotificationService(
7127
+ human_agent_registry=human_agent_repo,
7128
+ tool_registry=agent_service.tool_registry
7129
+ )
7130
+
6741
7131
  project_approval_service = ProjectApprovalService(
6742
7132
  ticket_repo, human_agent_repo, notification_service
6743
7133
  )
@@ -6763,6 +7153,27 @@ class SolanaAgentFactory:
6763
7153
  loaded_plugins = agent_service.plugin_manager.load_all_plugins()
6764
7154
  print(f"Loaded {loaded_plugins} plugins")
6765
7155
 
7156
+ # Get list of all agents defined in config
7157
+ config_defined_agents = [agent["name"]
7158
+ for agent in config.get("ai_agents", [])]
7159
+
7160
+ # Sync MongoDB with config-defined agents (delete any agents not in config)
7161
+ all_db_agents = ai_agent_repo.db.find(ai_agent_repo.collection, {})
7162
+ db_agent_names = [agent["name"] for agent in all_db_agents]
7163
+
7164
+ # Find agents that exist in DB but not in config
7165
+ agents_to_delete = [
7166
+ name for name in db_agent_names if name not in config_defined_agents]
7167
+
7168
+ # Delete those agents
7169
+ for agent_name in agents_to_delete:
7170
+ print(
7171
+ f"Deleting agent '{agent_name}' from MongoDB - no longer defined in config")
7172
+ ai_agent_repo.db.delete_one(
7173
+ ai_agent_repo.collection, {"name": agent_name})
7174
+ if agent_name in ai_agent_repo.ai_agents_cache:
7175
+ del ai_agent_repo.ai_agents_cache[agent_name]
7176
+
6766
7177
  # Register predefined agents if any
6767
7178
  for agent_config in config.get("ai_agents", []):
6768
7179
  agent_service.register_ai_agent(
@@ -6775,10 +7186,15 @@ class SolanaAgentFactory:
6775
7186
  # Register tools for this agent if specified
6776
7187
  if "tools" in agent_config:
6777
7188
  for tool_name in agent_config["tools"]:
7189
+ # Print available tools before registering
7190
+ print(
7191
+ f"Available tools before registering {tool_name}: {agent_service.tool_registry.list_all_tools()}")
6778
7192
  try:
6779
7193
  agent_service.register_tool_for_agent(
6780
7194
  agent_config["name"], tool_name
6781
7195
  )
7196
+ print(
7197
+ f"Successfully registered {tool_name} for agent {agent_config['name']}")
6782
7198
  except ValueError as e:
6783
7199
  print(
6784
7200
  f"Error registering tool {tool_name} for agent {agent_config['name']}: {e}"
@@ -7456,51 +7872,57 @@ class SolanaAgent:
7456
7872
  # PLUGIN SYSTEM
7457
7873
  #############################################
7458
7874
 
7875
+ class AutoTool:
7876
+ """Base class for tools that automatically register with the system."""
7459
7877
 
7460
- class Tool(ABC):
7461
- """Base class for all agent tools."""
7878
+ def __init__(self, name: str, description: str, registry=None):
7879
+ """Initialize the tool with name and description."""
7880
+ self.name = name
7881
+ self.description = description
7882
+ self._config = {}
7462
7883
 
7463
- @property
7464
- @abstractmethod
7465
- def name(self) -> str:
7466
- """Unique name of the tool."""
7467
- pass
7884
+ # Register with the provided registry if given
7885
+ if registry is not None:
7886
+ registry.register_tool(self)
7468
7887
 
7469
- @property
7470
- @abstractmethod
7471
- def description(self) -> str:
7472
- """Human-readable description of what the tool does."""
7473
- pass
7888
+ def configure(self, config: Dict[str, Any]) -> None:
7889
+ """Configure the tool with settings from config."""
7890
+ self._config = config
7474
7891
 
7475
- @property
7476
- @abstractmethod
7477
- def parameters_schema(self) -> Dict[str, Any]:
7478
- """JSON Schema for tool parameters."""
7479
- pass
7892
+ def get_schema(self) -> Dict[str, Any]:
7893
+ """Return the JSON schema for this tool's parameters."""
7894
+ # Override in subclasses
7895
+ return {}
7480
7896
 
7481
- @abstractmethod
7482
- def execute(self, **kwargs) -> Dict[str, Any]:
7483
- """Execute the tool with provided parameters."""
7484
- pass
7897
+ def execute(self, **params) -> Dict[str, Any]:
7898
+ """Execute the tool with the provided parameters."""
7899
+ # Override in subclasses
7900
+ raise NotImplementedError()
7485
7901
 
7486
7902
 
7487
7903
  class ToolRegistry:
7488
- """Central registry for all available tools."""
7904
+ """Instance-based registry that manages tools and their access permissions."""
7489
7905
 
7490
7906
  def __init__(self):
7491
- self._tools: Dict[str, Type[Tool]] = {}
7492
- # agent_name -> [tool_names]
7493
- self._agent_tools: Dict[str, List[str]] = {}
7907
+ """Initialize an empty tool registry."""
7908
+ self._tools = {} # name -> tool instance
7909
+ self._agent_tools = {} # agent_name -> [tool_names]
7910
+
7911
+ def register_tool(self, tool) -> bool:
7912
+ """Register a tool with this registry."""
7913
+ self._tools[tool.name] = tool
7914
+ print(f"Registered tool: {tool.name}")
7915
+ return True
7494
7916
 
7495
- def register_tool(self, tool_class: Type[Tool]) -> None:
7496
- """Register a tool in the global registry."""
7497
- instance = tool_class()
7498
- self._tools[instance.name] = tool_class
7917
+ def get_tool(self, tool_name: str):
7918
+ """Get a tool by name."""
7919
+ return self._tools.get(tool_name)
7499
7920
 
7500
- def assign_tool_to_agent(self, agent_name: str, tool_name: str) -> None:
7501
- """Grant an agent access to a specific tool."""
7921
+ def assign_tool_to_agent(self, agent_name: str, tool_name: str) -> bool:
7922
+ """Give an agent access to a specific tool."""
7502
7923
  if tool_name not in self._tools:
7503
- raise ValueError(f"Tool {tool_name} is not registered")
7924
+ print(f"Error: Tool {tool_name} is not registered")
7925
+ return False
7504
7926
 
7505
7927
  if agent_name not in self._agent_tools:
7506
7928
  self._agent_tools[agent_name] = []
@@ -7508,83 +7930,86 @@ class ToolRegistry:
7508
7930
  if tool_name not in self._agent_tools[agent_name]:
7509
7931
  self._agent_tools[agent_name].append(tool_name)
7510
7932
 
7933
+ return True
7934
+
7511
7935
  def get_agent_tools(self, agent_name: str) -> List[Dict[str, Any]]:
7512
- """Get all tools available to a specific agent."""
7936
+ """Get all tools available to an agent."""
7513
7937
  tool_names = self._agent_tools.get(agent_name, [])
7514
- tool_defs = []
7515
-
7516
- for name in tool_names:
7517
- if name in self._tools:
7518
- tool_instance = self._tools[name]()
7519
- tool_defs.append(
7520
- {
7521
- "name": tool_instance.name,
7522
- "description": tool_instance.description,
7523
- "parameters": tool_instance.parameters_schema,
7524
- }
7525
- )
7526
-
7527
- return tool_defs
7528
-
7529
- def get_tool(self, tool_name: str) -> Optional[Tool]:
7530
- """Get a tool by name."""
7531
- tool_class = self._tools.get(tool_name)
7532
- return tool_class() if tool_class else None
7938
+ return [
7939
+ {
7940
+ "name": name,
7941
+ "description": self._tools[name].description,
7942
+ "parameters": self._tools[name].get_schema()
7943
+ }
7944
+ for name in tool_names if name in self._tools
7945
+ ]
7533
7946
 
7534
7947
  def list_all_tools(self) -> List[str]:
7535
- """List all registered tool names."""
7948
+ """List all registered tools."""
7536
7949
  return list(self._tools.keys())
7537
7950
 
7538
-
7539
- # Global registry instance
7540
- tool_registry = ToolRegistry()
7951
+ def configure_all_tools(self, config: Dict[str, Any]) -> None:
7952
+ """Configure all registered tools with the same config."""
7953
+ for tool in self._tools.values():
7954
+ tool.configure(config)
7541
7955
 
7542
7956
 
7543
7957
  class PluginManager:
7544
- """Manages discovery, loading and execution of plugins."""
7958
+ """Manager for discovering and loading plugins."""
7545
7959
 
7546
- def __init__(self):
7547
- self.tools = {}
7960
+ # Class variable to track loaded entry points
7961
+ _loaded_entry_points = set()
7548
7962
 
7549
- def load_all_plugins(self) -> int:
7550
- """Load all plugins using setuptools entry points.
7963
+ def __init__(self, config: Optional[Dict[str, Any]] = None, tool_registry: Optional[ToolRegistry] = None):
7964
+ """Initialize with optional configuration and tool registry."""
7965
+ self.config = config or {}
7966
+ self.tool_registry = tool_registry or ToolRegistry()
7551
7967
 
7552
- Returns the number of plugins loaded for backwards compatibility.
7553
- """
7554
- import importlib.metadata
7968
+ def load_all_plugins(self) -> int:
7969
+ """Load all plugins using entry points and apply configuration."""
7970
+ loaded_count = 0
7971
+ plugins = []
7555
7972
 
7556
- count = 0
7557
- # Discover plugins registered via entry_points
7973
+ # Discover plugins through entry points
7558
7974
  for entry_point in importlib.metadata.entry_points(group='solana_agent.plugins'):
7975
+ # Skip if this entry point has already been loaded
7976
+ entry_point_id = f"{entry_point.name}:{entry_point.value}"
7977
+ if entry_point_id in PluginManager._loaded_entry_points:
7978
+ print(f"Skipping already loaded plugin: {entry_point.name}")
7979
+ continue
7980
+
7559
7981
  try:
7560
- plugin_class = entry_point.load()
7561
- plugin = plugin_class()
7562
-
7563
- # Register all tools from this plugin
7564
- for tool in plugin.get_tools():
7565
- self.tools[tool.name] = tool
7566
- print(f"Registered tool: {tool.name}")
7567
- count += 1
7982
+ print(f"Found plugin entry point: {entry_point.name}")
7983
+ PluginManager._loaded_entry_points.add(entry_point_id)
7984
+ plugin_factory = entry_point.load()
7985
+ plugin = plugin_factory()
7986
+ plugins.append(plugin)
7987
+
7988
+ # Initialize the plugin with config
7989
+ if hasattr(plugin, 'initialize') and callable(plugin.initialize):
7990
+ plugin.initialize(self.config)
7991
+ print(
7992
+ f"Initialized plugin {entry_point.name} with config keys: {list(self.config.keys() if self.config else [])}")
7993
+
7994
+ loaded_count += 1
7568
7995
  except Exception as e:
7569
7996
  print(f"Error loading plugin {entry_point.name}: {e}")
7570
7997
 
7571
- return count
7572
-
7573
- def get_tool(self, name):
7574
- """Get a tool by name."""
7575
- return self.tools.get(name)
7576
-
7577
- def list_tools(self):
7578
- """List all available tools."""
7579
- return list(self.tools.keys())
7580
-
7581
- def execute_tool(self, tool_name: str, **kwargs) -> Dict[str, Any]:
7582
- """Execute a tool with provided parameters."""
7583
- tool = self.tools.get(tool_name)
7584
- if not tool:
7585
- raise ValueError(f"Tool {tool_name} not found")
7998
+ # After all plugins are initialized, register their tools
7999
+ for plugin in plugins:
8000
+ try:
8001
+ if hasattr(plugin, 'get_tools') and callable(plugin.get_tools):
8002
+ tools = plugin.get_tools()
8003
+ # Register each tool with our registry
8004
+ if isinstance(tools, list):
8005
+ for tool in tools:
8006
+ self.tool_registry.register_tool(tool)
8007
+ tool.configure(self.config)
8008
+ else:
8009
+ # Single tool case
8010
+ self.tool_registry.register_tool(tools)
8011
+ tools.configure(self.config)
8012
+ except Exception as e:
8013
+ print(f"Error registering tools from plugin: {e}")
7586
8014
 
7587
- try:
7588
- return tool.execute(**kwargs)
7589
- except Exception as e:
7590
- return {"error": str(e), "status": "error"}
8015
+ return loaded_count