solana-agent 11.3.0__py3-none-any.whl → 14.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.
Files changed (39) hide show
  1. solana_agent/__init__.py +33 -1
  2. solana_agent/adapters/__init__.py +6 -0
  3. solana_agent/adapters/llm_adapter.py +123 -0
  4. solana_agent/adapters/mongodb_adapter.py +69 -0
  5. solana_agent/client/__init__.py +0 -0
  6. solana_agent/client/solana_agent.py +75 -0
  7. solana_agent/domains/__init__.py +6 -0
  8. solana_agent/domains/agents.py +80 -0
  9. solana_agent/domains/routing.py +14 -0
  10. solana_agent/factories/__init__.py +0 -0
  11. solana_agent/factories/agent_factory.py +148 -0
  12. solana_agent/interfaces/__init__.py +11 -0
  13. solana_agent/interfaces/plugins/plugins.py +118 -0
  14. solana_agent/interfaces/providers/data_storage.py +53 -0
  15. solana_agent/interfaces/providers/llm.py +34 -0
  16. solana_agent/interfaces/providers/memory.py +38 -0
  17. solana_agent/interfaces/repositories/agent.py +33 -0
  18. solana_agent/interfaces/services/agent.py +41 -0
  19. solana_agent/interfaces/services/query.py +11 -0
  20. solana_agent/interfaces/services/routing.py +19 -0
  21. solana_agent/plugins/__init__.py +6 -0
  22. solana_agent/plugins/manager.py +147 -0
  23. solana_agent/plugins/registry.py +64 -0
  24. solana_agent/plugins/tools/__init__.py +10 -0
  25. solana_agent/plugins/tools/auto_tool.py +47 -0
  26. solana_agent/repositories/__init__.py +6 -0
  27. solana_agent/repositories/agent.py +99 -0
  28. solana_agent/repositories/memory.py +134 -0
  29. solana_agent/services/__init__.py +6 -0
  30. solana_agent/services/agent.py +320 -0
  31. solana_agent/services/query.py +223 -0
  32. solana_agent/services/routing.py +149 -0
  33. solana_agent-14.0.0.dist-info/METADATA +121 -0
  34. solana_agent-14.0.0.dist-info/RECORD +36 -0
  35. solana_agent/ai.py +0 -7590
  36. solana_agent-11.3.0.dist-info/METADATA +0 -443
  37. solana_agent-11.3.0.dist-info/RECORD +0 -6
  38. {solana_agent-11.3.0.dist-info → solana_agent-14.0.0.dist-info}/LICENSE +0 -0
  39. {solana_agent-11.3.0.dist-info → solana_agent-14.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,99 @@
1
+ """
2
+ MongoDB implementation of the agent repository.
3
+ """
4
+ from typing import List, Optional, Any
5
+
6
+ from solana_agent.domains.agents import AIAgent
7
+ from solana_agent.interfaces.repositories.agent import AgentRepository
8
+
9
+
10
+ class MongoAgentRepository(AgentRepository):
11
+ """MongoDB implementation of the AgentRepository interface."""
12
+
13
+ def __init__(self, db_adapter):
14
+ """Initialize the repository with a database adapter."""
15
+ self.db = db_adapter
16
+ self.ai_agents_collection = "agents"
17
+
18
+ # Ensure collections exist
19
+ self.db.create_collection(self.ai_agents_collection)
20
+
21
+ # Create indexes
22
+ self.db.create_index(self.ai_agents_collection,
23
+ [("name", 1)], unique=True)
24
+
25
+ def get_ai_agent_by_name(self, name: str) -> Optional[AIAgent]:
26
+ """Get an AI agent by name.
27
+
28
+ Args:
29
+ name: The name of the AI agent
30
+
31
+ Returns:
32
+ The AI agent or None if not found
33
+ """
34
+ # Query the AI agents collection for a document with matching name
35
+ doc = self.db.find_one(self.ai_agents_collection, {"name": name})
36
+
37
+ # If no document found, return None
38
+ if not doc:
39
+ return None
40
+
41
+ # Convert the document to an AIAgent domain model
42
+ try:
43
+ return AIAgent.model_validate(doc)
44
+ except Exception as e:
45
+ print(f"Error parsing AI agent with name {name}: {e}")
46
+ return None
47
+
48
+ def get_ai_agent(self, name: str) -> Optional[AIAgent]:
49
+ """Get an AI agent by name."""
50
+ doc = self.db.find_one(self.ai_agents_collection, {"name": name})
51
+ if not doc:
52
+ return None
53
+
54
+ return AIAgent.model_validate(doc)
55
+
56
+ def get_all_ai_agents(self) -> List[AIAgent]:
57
+ """Get all AI agents in the system.
58
+
59
+ Returns:
60
+ List of all AI agents
61
+ """
62
+ # Query all documents from the AI agents collection
63
+ docs = self.db.find(self.ai_agents_collection, {})
64
+
65
+ # Convert each document to an AIAgent domain model
66
+ ai_agents = []
67
+ for doc in docs:
68
+ try:
69
+ agent = AIAgent.model_validate(doc)
70
+ ai_agents.append(agent)
71
+ except Exception as e:
72
+ # Log the error but continue processing other agents
73
+ print(f"Error parsing AI agent from database: {e}")
74
+
75
+ return ai_agents
76
+
77
+ def save_ai_agent(self, agent: AIAgent) -> bool:
78
+ """Save an AI agent."""
79
+ doc = agent.model_dump()
80
+
81
+ # Check if agent already exists
82
+ existing = self.db.find_one(
83
+ self.ai_agents_collection, {"name": agent.name})
84
+
85
+ if existing:
86
+ # Update existing agent
87
+ return self.db.update_one(
88
+ self.ai_agents_collection,
89
+ {"name": agent.name},
90
+ {"$set": doc}
91
+ )
92
+ else:
93
+ # Create new agent
94
+ self.db.insert_one(self.ai_agents_collection, doc)
95
+ return True
96
+
97
+ def delete_ai_agent(self, name: str) -> bool:
98
+ """Delete an AI agent."""
99
+ return self.db.delete_one(self.ai_agents_collection, {"name": name})
@@ -0,0 +1,134 @@
1
+ from typing import List, Dict, Any, Optional, Tuple
2
+ from datetime import datetime, timezone
3
+ from zep_cloud.client import AsyncZep as AsyncZepCloud
4
+ from zep_python.client import AsyncZep
5
+ from zep_cloud.types import Message
6
+ from solana_agent.interfaces.providers.memory import MemoryProvider
7
+ from solana_agent.interfaces.providers.data_storage import DataStorageProvider
8
+
9
+
10
+ class MemoryRepository(MemoryProvider):
11
+ """Combined Zep and MongoDB implementation of MemoryProvider."""
12
+
13
+ def __init__(
14
+ self,
15
+ mongo_adapter: DataStorageProvider,
16
+ zep_api_key: Optional[str] = None,
17
+ zep_base_url: Optional[str] = None
18
+ ):
19
+ """Initialize the combined memory provider."""
20
+ # Initialize MongoDB
21
+ self.mongo = mongo_adapter
22
+ self.collection = "conversations"
23
+
24
+ # Ensure MongoDB collection and indexes
25
+ self.mongo.create_collection(self.collection)
26
+ self.mongo.create_index(self.collection, [("user_id", 1)])
27
+ self.mongo.create_index(self.collection, [("timestamp", 1)])
28
+
29
+ # Initialize Zep
30
+ if zep_api_key and not zep_base_url:
31
+ self.zep = AsyncZepCloud(api_key=zep_api_key)
32
+ elif zep_api_key and zep_base_url:
33
+ self.zep = AsyncZep(api_key=zep_api_key, base_url=zep_base_url)
34
+ else:
35
+ self.zep = AsyncZep(base_url="http://localhost:8000")
36
+
37
+ async def store(self, user_id: str, messages: List[Dict[str, Any]]) -> None:
38
+ """Store messages in both Zep and MongoDB."""
39
+ # Store in MongoDB as single document
40
+ try:
41
+ # Extract user and assistant messages
42
+ user_message = next(msg["content"]
43
+ for msg in messages if msg["role"] == "user")
44
+ assistant_message = next(
45
+ msg["content"] for msg in messages if msg["role"] == "assistant")
46
+
47
+ doc = {
48
+ "user_id": user_id,
49
+ "user_message": self._truncate(user_message),
50
+ "assistant_message": self._truncate(assistant_message),
51
+ "timestamp": datetime.now(timezone.utc)
52
+ }
53
+ self.mongo.insert_one(self.collection, doc)
54
+ except Exception as e:
55
+ print(f"MongoDB storage error: {e}")
56
+
57
+ # Store in Zep with role-based format
58
+ try:
59
+ try:
60
+ await self.zep.user.add(user_id=user_id)
61
+ except Exception:
62
+ pass
63
+ try:
64
+ await self.zep.memory.add_session(
65
+ session_id=user_id,
66
+ user_id=user_id,
67
+ )
68
+ except Exception:
69
+ pass
70
+
71
+ zep_messages = [
72
+ Message(
73
+ role=msg["role"],
74
+ role_type=msg["role"],
75
+ content=self._truncate(msg["content"])
76
+ )
77
+ for msg in messages
78
+ ]
79
+ await self.zep.memory.add(session_id=user_id, messages=zep_messages)
80
+ except Exception as e:
81
+ print(f"Zep storage error: {e}")
82
+
83
+ async def retrieve(self, user_id: str) -> str:
84
+ """Retrieve memory context from Zep only."""
85
+ try:
86
+ memory = await self.zep.memory.get(session_id=user_id)
87
+
88
+ return memory.context
89
+
90
+ except Exception as e:
91
+ print(f"Error retrieving Zep memory: {e}")
92
+ return ""
93
+
94
+ async def delete(self, user_id: str) -> None:
95
+ """Delete memory from both systems."""
96
+ try:
97
+ self.mongo.delete_many(
98
+ self.collection,
99
+ {"user_id": user_id}
100
+ )
101
+ except Exception as e:
102
+ print(f"MongoDB deletion error: {e}")
103
+
104
+ try:
105
+ await self.zep.memory.delete(session_id=user_id)
106
+ await self.zep.user.delete(user_id=user_id)
107
+ except Exception as e:
108
+ print(f"Zep deletion error: {e}")
109
+
110
+ def find(
111
+ self,
112
+ collection: str,
113
+ query: Dict,
114
+ sort: Optional[List[Tuple]] = None,
115
+ limit: int = 0,
116
+ skip: int = 0
117
+ ) -> List[Dict]:
118
+ """Find documents matching query."""
119
+ return self.mongo.find(collection, query, sort=sort, limit=limit, skip=skip)
120
+
121
+ def count_documents(self, collection: str, query: Dict) -> int:
122
+ return self.mongo.count_documents(collection, query)
123
+
124
+ def _truncate(self, text: str, limit: int = 2500) -> str:
125
+ """Truncate text to be within limits."""
126
+ if len(text) <= limit:
127
+ return text
128
+
129
+ truncated = text[:limit]
130
+ last_period = truncated.rfind(".")
131
+ if last_period > limit * 0.8:
132
+ return truncated[:last_period + 1]
133
+
134
+ return truncated + "..."
@@ -0,0 +1,6 @@
1
+ """
2
+ Service implementations for the Solana Agent system.
3
+
4
+ These services implement the business logic interfaces defined in
5
+ solana_agent.interfaces.services.
6
+ """
@@ -0,0 +1,320 @@
1
+ """
2
+ Agent service implementation.
3
+
4
+ This service manages AI and human agents, their registration, tool assignments,
5
+ and response generation.
6
+ """
7
+ import datetime as main_datetime
8
+ from datetime import datetime
9
+ import json
10
+ from typing import AsyncGenerator, Dict, List, Optional, Any
11
+
12
+ from solana_agent.interfaces.services.agent import AgentService as AgentServiceInterface
13
+ from solana_agent.interfaces.providers.llm import LLMProvider
14
+ from solana_agent.interfaces.repositories.agent import AgentRepository
15
+ from solana_agent.interfaces.plugins.plugins import ToolRegistry
16
+ from solana_agent.domains.agents import AIAgent, OrganizationMission
17
+
18
+
19
+ class AgentService(AgentServiceInterface):
20
+ """Service for managing agents and generating responses."""
21
+
22
+ def __init__(
23
+ self,
24
+ llm_provider: LLMProvider,
25
+ agent_repository: AgentRepository,
26
+ organization_mission: Optional[OrganizationMission] = None,
27
+ config: Optional[Dict[str, Any]] = None,
28
+ tool_registry: Optional[ToolRegistry] = None,
29
+ ):
30
+ """Initialize the agent service.
31
+
32
+ Args:
33
+ llm_provider: Provider for language model interactions
34
+ agent_repository: Repository for agent data
35
+ organization_mission: Optional organization mission and values
36
+ config: Optional service configuration
37
+ """
38
+ self.llm_provider = llm_provider
39
+ self.agent_repository = agent_repository
40
+ self.organization_mission = organization_mission
41
+ self.config = config or {}
42
+
43
+ # Initialize tool registry with concrete implementation
44
+ if tool_registry:
45
+ self.tool_registry = tool_registry
46
+ else:
47
+ # Import the concrete implementation
48
+ from solana_agent.plugins.registry import ToolRegistry as ConcreteToolRegistry
49
+ self.tool_registry = ConcreteToolRegistry()
50
+
51
+ # Will be set by factory if plugin system is enabled
52
+ self.plugin_manager = None
53
+
54
+ def register_ai_agent(
55
+ self, name: str, instructions: str, specialization: str, model: str = "gpt-4o-mini"
56
+ ) -> None:
57
+ """Register an AI agent with its specialization.
58
+
59
+ Args:
60
+ name: Agent name
61
+ instructions: Agent instructions
62
+ specialization: Agent specialization
63
+ model: LLM model to use
64
+ """
65
+ agent = AIAgent(
66
+ name=name,
67
+ instructions=instructions,
68
+ specialization=specialization,
69
+ model=model
70
+ )
71
+ self.agent_repository.save_ai_agent(agent)
72
+
73
+ def get_agent_system_prompt(self, agent_name: str) -> str:
74
+ """Get the system prompt for an agent.
75
+
76
+ Args:
77
+ agent_name: Agent name
78
+
79
+ Returns:
80
+ System prompt
81
+ """
82
+ agent = self.agent_repository.get_ai_agent_by_name(agent_name)
83
+
84
+ # Build system prompt
85
+ system_prompt = f"You are {agent.name}, an AI assistant with the following instructions:\n\n"
86
+ system_prompt += agent.instructions
87
+
88
+ # add current time
89
+ system_prompt += f"\n\nThe current time is {datetime.now(tz=main_datetime.timezone.utc)}\n\n."
90
+
91
+ # Add mission and values if available
92
+ if self.organization_mission:
93
+ system_prompt += f"\n\nORGANIZATION MISSION:\n{self.organization_mission.mission_statement}"
94
+
95
+ if self.organization_mission.values:
96
+ values_text = "\n".join([
97
+ f"- {value.get('name', '')}: {value.get('description', '')}"
98
+ for value in self.organization_mission.values
99
+ ])
100
+ system_prompt += f"\n\nORGANIZATION VALUES:\n{values_text}"
101
+
102
+ # Add organization goals if available
103
+ if self.organization_mission and self.organization_mission.goals:
104
+ goals_text = "\n".join(
105
+ [f"- {goal}" for goal in self.organization_mission.goals])
106
+ system_prompt += f"\n\nORGANIZATION GOALS:\n{goals_text}"
107
+
108
+ return system_prompt
109
+
110
+ def get_all_ai_agents(self) -> Dict[str, AIAgent]:
111
+ """Get all registered AI agents.
112
+
113
+ Returns:
114
+ Dictionary mapping agent names to agents
115
+ """
116
+ agents = self.agent_repository.get_all_ai_agents()
117
+ return {agent.name: agent for agent in agents}
118
+
119
+ def get_specializations(self) -> Dict[str, str]:
120
+ """Get all registered specializations.
121
+
122
+ Returns:
123
+ Dictionary mapping specialization names to descriptions
124
+ """
125
+ specializations = {}
126
+
127
+ # Gather from AI agents
128
+ ai_agents = self.agent_repository.get_all_ai_agents()
129
+ for agent in ai_agents:
130
+ if agent.specialization:
131
+ specializations[agent.specialization] = f"AI expertise in {agent.specialization}"
132
+
133
+ return specializations
134
+
135
+ def assign_tool_for_agent(self, agent_name: str, tool_name: str) -> bool:
136
+ """Assign a tool to an agent.
137
+
138
+ Args:
139
+ agent_name: Agent name
140
+ tool_name: Tool name
141
+
142
+ Returns:
143
+ True if assignment was successful
144
+ """
145
+ return self.tool_registry.assign_tool_to_agent(agent_name, tool_name)
146
+
147
+ def get_agent_tools(self, agent_name: str) -> List[Dict[str, Any]]:
148
+ """Get tools available to an agent.
149
+
150
+ Args:
151
+ agent_name: Agent name
152
+
153
+ Returns:
154
+ List of tool configurations
155
+ """
156
+ return self.tool_registry.get_agent_tools(agent_name)
157
+
158
+ def execute_tool(self, agent_name: str, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
159
+ """Execute a tool on behalf of an agent.
160
+
161
+ Args:
162
+ agent_name: Agent name
163
+ tool_name: Tool name
164
+ parameters: Tool parameters
165
+
166
+ Returns:
167
+ Tool execution result
168
+ """
169
+ if not self.tool_registry:
170
+ return {"status": "error", "message": "Tool registry not available"}
171
+
172
+ tool = self.tool_registry.get_tool(tool_name)
173
+ if not tool:
174
+ return {"status": "error", "message": f"Tool '{tool_name}' not found"}
175
+
176
+ # Check if agent has access to this tool
177
+ agent_tools = self.tool_registry.get_agent_tools(agent_name)
178
+ if not any(t.get("name") == tool_name for t in agent_tools):
179
+ return {
180
+ "status": "error",
181
+ "message": f"Agent '{agent_name}' doesn't have access to tool '{tool_name}'"
182
+ }
183
+
184
+ try:
185
+ return tool.execute(**parameters)
186
+ except Exception as e:
187
+ return {"status": "error", "message": f"Error executing tool: {str(e)}"}
188
+
189
+ async def generate_response(
190
+ self,
191
+ agent_name: str,
192
+ user_id: str,
193
+ query: str,
194
+ memory_context: str = "",
195
+ **kwargs
196
+ ) -> AsyncGenerator[str, None]: # pragma: no cover
197
+ """Generate a response with tool execution support."""
198
+ agent = self.agent_repository.get_ai_agent_by_name(agent_name)
199
+ if not agent:
200
+ yield f"Agent '{agent_name}' not found."
201
+ return
202
+
203
+ # Get system prompt and add tool instructions
204
+ system_prompt = self.get_agent_system_prompt(agent_name)
205
+ if self.tool_registry:
206
+ tool_usage_prompt = self._get_tool_usage_prompt(agent_name)
207
+ if tool_usage_prompt:
208
+ system_prompt = f"{system_prompt}\n\n{tool_usage_prompt}"
209
+
210
+ # Add memory context
211
+ if memory_context:
212
+ system_prompt += f"\n\n Memory Context: {memory_context}"
213
+
214
+ try:
215
+ json_response = ""
216
+ is_json = False
217
+
218
+ async for chunk in self.llm_provider.generate_text(
219
+ user_id=user_id,
220
+ prompt=query,
221
+ system_prompt=system_prompt,
222
+ model=agent.model,
223
+ needs_search=True, # Enable web search by default
224
+ **kwargs
225
+ ):
226
+ # Check for JSON start
227
+ if chunk.strip().startswith("{"):
228
+ is_json = True
229
+ json_response = chunk
230
+ continue
231
+
232
+ # Collect JSON or yield normal text
233
+ if is_json:
234
+ json_response += chunk
235
+ try:
236
+ # Try to parse complete JSON
237
+ data = json.loads(json_response)
238
+
239
+ # Handle tool call
240
+ if "tool_call" in data:
241
+ tool_data = data["tool_call"]
242
+ tool_name = tool_data.get("name")
243
+ parameters = tool_data.get("parameters", {})
244
+
245
+ if tool_name:
246
+ result = self.execute_tool(
247
+ agent_name, tool_name, parameters)
248
+ if result.get("status") == "success":
249
+ yield result.get("result", "")
250
+ else:
251
+ yield f"I apologize, but I encountered an issue: {result.get('message', 'Unknown error')}"
252
+ break
253
+ else:
254
+ # If JSON but not a tool call, yield as text
255
+ yield json_response
256
+ break
257
+ except json.JSONDecodeError:
258
+ # Not complete JSON yet, keep collecting
259
+ continue
260
+ else:
261
+ yield chunk
262
+
263
+ except Exception as e:
264
+ print(f"Error in generate_response: {str(e)}")
265
+ import traceback
266
+ print(traceback.format_exc())
267
+ yield f"I apologize, but I encountered an error: {str(e)}"
268
+
269
+ def _get_tool_usage_prompt(self, agent_name: str) -> str:
270
+ """Generate JSON-based instructions for tool usage."""
271
+ # Get tools assigned to this agent
272
+ tools = self.get_agent_tools(agent_name)
273
+ if not tools:
274
+ return ""
275
+
276
+ # Get actual tool names
277
+ available_tool_names = [tool.get("name", "") for tool in tools]
278
+ tools_json = json.dumps(tools, indent=2)
279
+
280
+ # Create tool example if search is available
281
+ tool_example = ""
282
+ if "search_internet" in available_tool_names:
283
+ tool_example = """
284
+ For latest news query:
285
+ {
286
+ "tool_call": {
287
+ "name": "search_internet",
288
+ "parameters": {
289
+ "query": "latest Solana blockchain news March 2025"
290
+ }
291
+ }
292
+ }"""
293
+
294
+ return f"""
295
+ AVAILABLE TOOLS:
296
+ {tools_json}
297
+
298
+ TOOL USAGE FORMAT:
299
+ {{
300
+ "tool_call": {{
301
+ "name": "<one_of:{', '.join(available_tool_names)}>",
302
+ "parameters": {{
303
+ // parameters as specified in tool definition above
304
+ }}
305
+ }}
306
+ }}
307
+
308
+ {tool_example if tool_example else ''}
309
+
310
+ RESPONSE RULES:
311
+ 1. For tool usage:
312
+ - Only use tools from the AVAILABLE TOOLS list above
313
+ - Follow the exact parameter format shown in the tool definition
314
+ - Include "March 2025" in any search queries for current information
315
+
316
+ 2. Format Requirements:
317
+ - Return ONLY the JSON object for tool calls
318
+ - No explanation text before or after
319
+ - Use exact tool names as shown in AVAILABLE TOOLS
320
+ """