solana-agent 4.0.3__tar.gz → 5.0.0__tar.gz
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-4.0.3 → solana_agent-5.0.0}/PKG-INFO +15 -2
- {solana_agent-4.0.3 → solana_agent-5.0.0}/README.md +13 -0
- {solana_agent-4.0.3 → solana_agent-5.0.0}/pyproject.toml +2 -2
- {solana_agent-4.0.3 → solana_agent-5.0.0}/solana_agent/ai.py +290 -1
- {solana_agent-4.0.3 → solana_agent-5.0.0}/LICENSE +0 -0
- {solana_agent-4.0.3 → solana_agent-5.0.0}/solana_agent/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: solana-agent
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
4
4
|
Summary: Build self-learning AI Agents
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: ai,openai,ai agents
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
19
|
Requires-Dist: ntplib (>=0.4.0,<0.5.0)
|
|
20
|
-
Requires-Dist: openai (>=1.
|
|
20
|
+
Requires-Dist: openai (>=1.65.1,<2.0.0)
|
|
21
21
|
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
22
22
|
Requires-Dist: pinecone (>=6.0.1,<7.0.0)
|
|
23
23
|
Requires-Dist: pydantic (>=2.10.6,<3.0.0)
|
|
@@ -62,6 +62,12 @@ Unlike traditional AI assistants that forget conversations after each session, S
|
|
|
62
62
|
- Advanced AI memory combining conversational context, knowledge base, and parallel tool calling
|
|
63
63
|
- Create custom tools for extending the Agent's capabilities like further API integrations
|
|
64
64
|
|
|
65
|
+
🤖 **Multi-Agent Swarms**
|
|
66
|
+
- Create specialized agents with different expertise domains
|
|
67
|
+
- Automatic routing of queries to the most appropriate specialist
|
|
68
|
+
- Seamless handoffs between agents for complex multi-domain questions
|
|
69
|
+
- Shared memory and context across the entire agent swarm
|
|
70
|
+
|
|
65
71
|
🔍 **Multi-Source Search and Reasoning**
|
|
66
72
|
- Internet search via Perplexity
|
|
67
73
|
- X (Twitter) search using Grok
|
|
@@ -82,6 +88,13 @@ Unlike traditional AI assistants that forget conversations after each session, S
|
|
|
82
88
|
- Knowledge Base to add domain specific knowledge
|
|
83
89
|
- CSV file uploads to perform document context search
|
|
84
90
|
|
|
91
|
+
🤝 **Intelligent Multi-Agent Systems**
|
|
92
|
+
- First-class support for specialized agent swarms
|
|
93
|
+
- Dynamic inter-agent routing based on query complexity
|
|
94
|
+
- Seamless handoffs with continuous memory preservation
|
|
95
|
+
- Single unified interface for the entire agent network
|
|
96
|
+
- No custom coding required for agent coordination
|
|
97
|
+
|
|
85
98
|
🏢 **Enterprise Focus**
|
|
86
99
|
- Production-ready out of the box in a few lines of code
|
|
87
100
|
- Enterprise-grade deployment options for all components and services
|
|
@@ -32,6 +32,12 @@ Unlike traditional AI assistants that forget conversations after each session, S
|
|
|
32
32
|
- Advanced AI memory combining conversational context, knowledge base, and parallel tool calling
|
|
33
33
|
- Create custom tools for extending the Agent's capabilities like further API integrations
|
|
34
34
|
|
|
35
|
+
🤖 **Multi-Agent Swarms**
|
|
36
|
+
- Create specialized agents with different expertise domains
|
|
37
|
+
- Automatic routing of queries to the most appropriate specialist
|
|
38
|
+
- Seamless handoffs between agents for complex multi-domain questions
|
|
39
|
+
- Shared memory and context across the entire agent swarm
|
|
40
|
+
|
|
35
41
|
🔍 **Multi-Source Search and Reasoning**
|
|
36
42
|
- Internet search via Perplexity
|
|
37
43
|
- X (Twitter) search using Grok
|
|
@@ -52,6 +58,13 @@ Unlike traditional AI assistants that forget conversations after each session, S
|
|
|
52
58
|
- Knowledge Base to add domain specific knowledge
|
|
53
59
|
- CSV file uploads to perform document context search
|
|
54
60
|
|
|
61
|
+
🤝 **Intelligent Multi-Agent Systems**
|
|
62
|
+
- First-class support for specialized agent swarms
|
|
63
|
+
- Dynamic inter-agent routing based on query complexity
|
|
64
|
+
- Seamless handoffs with continuous memory preservation
|
|
65
|
+
- Single unified interface for the entire agent network
|
|
66
|
+
- No custom coding required for agent coordination
|
|
67
|
+
|
|
55
68
|
🏢 **Enterprise Focus**
|
|
56
69
|
- Production-ready out of the box in a few lines of code
|
|
57
70
|
- Enterprise-grade deployment options for all components and services
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "solana-agent"
|
|
3
|
-
version = "
|
|
3
|
+
version = "5.0.0"
|
|
4
4
|
description = "Build self-learning AI Agents"
|
|
5
5
|
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -18,7 +18,7 @@ python_paths = [".", "tests"]
|
|
|
18
18
|
|
|
19
19
|
[tool.poetry.dependencies]
|
|
20
20
|
python = ">=3.9,<4.0"
|
|
21
|
-
openai = "^1.
|
|
21
|
+
openai = "^1.65.1"
|
|
22
22
|
pydantic = "^2.10.6"
|
|
23
23
|
pymongo = "^4.11.1"
|
|
24
24
|
zep-cloud = "^2.4.0"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import datetime
|
|
3
|
+
import traceback
|
|
3
4
|
import ntplib
|
|
4
5
|
import json
|
|
5
6
|
from typing import AsyncGenerator, List, Literal, Dict, Any, Callable
|
|
@@ -1106,4 +1107,292 @@ class AI:
|
|
|
1106
1107
|
return func
|
|
1107
1108
|
|
|
1108
1109
|
|
|
1109
|
-
|
|
1110
|
+
class MultiAgentSystem:
|
|
1111
|
+
"""A multi-agent system that coordinates specialized AI agents with handoff capabilities."""
|
|
1112
|
+
|
|
1113
|
+
def __init__(self, database: MongoDatabase, router_model: str = "gpt-4o"):
|
|
1114
|
+
"""Initialize the multi-agent system with a shared database.
|
|
1115
|
+
|
|
1116
|
+
Args:
|
|
1117
|
+
database (MongoDatabase): Shared MongoDB database instance
|
|
1118
|
+
router_model (str, optional): Model to use for routing decisions. Defaults to "gpt-4o".
|
|
1119
|
+
"""
|
|
1120
|
+
self.agents = {} # name -> AI instance
|
|
1121
|
+
self.specializations = {} # name -> description
|
|
1122
|
+
self.database = database
|
|
1123
|
+
self.router_model = router_model
|
|
1124
|
+
|
|
1125
|
+
# Ensure handoffs collection exists
|
|
1126
|
+
if "handoffs" not in self.database.db.list_collection_names():
|
|
1127
|
+
self.database.db.create_collection("handoffs")
|
|
1128
|
+
self.handoffs = self.database.db["handoffs"]
|
|
1129
|
+
|
|
1130
|
+
print(
|
|
1131
|
+
f"MultiAgentSystem initialized with router model: {router_model}")
|
|
1132
|
+
|
|
1133
|
+
def register(self, name: str, agent: AI, specialization: str):
|
|
1134
|
+
"""Register a specialized agent with the multi-agent system."""
|
|
1135
|
+
# Add the agent to the system first
|
|
1136
|
+
self.agents[name] = agent
|
|
1137
|
+
self.specializations[name] = specialization
|
|
1138
|
+
|
|
1139
|
+
print(
|
|
1140
|
+
f"Registered agent: {name}, specialization: {specialization[:50]}...")
|
|
1141
|
+
print(f"Current agents: {list(self.agents.keys())}")
|
|
1142
|
+
|
|
1143
|
+
# We need to refresh handoff tools for ALL agents whenever a new one is registered
|
|
1144
|
+
self._update_all_agent_tools()
|
|
1145
|
+
|
|
1146
|
+
def _update_all_agent_tools(self):
|
|
1147
|
+
"""Update all agents with current handoff capabilities."""
|
|
1148
|
+
# For each registered agent, update its handoff tool
|
|
1149
|
+
for agent_name, agent in self.agents.items():
|
|
1150
|
+
# Get other agents that this agent can hand off to
|
|
1151
|
+
available_targets = [
|
|
1152
|
+
name for name in self.agents.keys() if name != agent_name
|
|
1153
|
+
]
|
|
1154
|
+
|
|
1155
|
+
# First remove any existing handoff tool if present
|
|
1156
|
+
agent._tools = [
|
|
1157
|
+
t for t in agent._tools if t["function"]["name"] != "request_handoff"
|
|
1158
|
+
]
|
|
1159
|
+
|
|
1160
|
+
# Create handoff tool with explicit naming requirements
|
|
1161
|
+
def create_handoff_tool(current_agent_name, available_targets_list):
|
|
1162
|
+
def request_handoff(target_agent: str, reason: str) -> str:
|
|
1163
|
+
"""Request an immediate handoff to another specialized agent.
|
|
1164
|
+
This is an INTERNAL SYSTEM TOOL. The user will NOT see your reasoning about the handoff.
|
|
1165
|
+
Use this tool IMMEDIATELY when a query is outside your expertise.
|
|
1166
|
+
|
|
1167
|
+
Args:
|
|
1168
|
+
target_agent (str): Name of agent to transfer to. MUST be one of: {', '.join(available_targets_list)}.
|
|
1169
|
+
DO NOT INVENT NEW NAMES OR VARIATIONS. Use EXACTLY one of these names.
|
|
1170
|
+
reason (str): Brief explanation of why this question requires the specialist
|
|
1171
|
+
|
|
1172
|
+
Returns:
|
|
1173
|
+
str: Empty string - the handoff is handled internally
|
|
1174
|
+
"""
|
|
1175
|
+
# Prevent self-handoffs
|
|
1176
|
+
if target_agent == current_agent_name:
|
|
1177
|
+
print(
|
|
1178
|
+
f"[HANDOFF ERROR] Agent {current_agent_name} attempted to hand off to itself"
|
|
1179
|
+
)
|
|
1180
|
+
if available_targets_list:
|
|
1181
|
+
target_agent = available_targets_list[0]
|
|
1182
|
+
print(
|
|
1183
|
+
f"[HANDOFF CORRECTION] Redirecting to {target_agent} instead"
|
|
1184
|
+
)
|
|
1185
|
+
else:
|
|
1186
|
+
print(
|
|
1187
|
+
"[HANDOFF ERROR] No other agents available to hand off to"
|
|
1188
|
+
)
|
|
1189
|
+
return ""
|
|
1190
|
+
|
|
1191
|
+
# Validate target agent exists
|
|
1192
|
+
if target_agent not in self.agents:
|
|
1193
|
+
print(
|
|
1194
|
+
f"[HANDOFF WARNING] Invalid target '{target_agent}'")
|
|
1195
|
+
if available_targets_list:
|
|
1196
|
+
original_target = target_agent
|
|
1197
|
+
target_agent = available_targets_list[0]
|
|
1198
|
+
print(
|
|
1199
|
+
f"[HANDOFF CORRECTION] Redirecting from '{original_target}' to '{target_agent}'"
|
|
1200
|
+
)
|
|
1201
|
+
else:
|
|
1202
|
+
print(
|
|
1203
|
+
"[HANDOFF ERROR] No valid target agents available")
|
|
1204
|
+
return ""
|
|
1205
|
+
|
|
1206
|
+
print(
|
|
1207
|
+
f"[HANDOFF TOOL CALLED] {current_agent_name} -> {target_agent}: {reason}"
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
# Set handoff info in the agent instance for processing
|
|
1211
|
+
agent._handoff_info = {
|
|
1212
|
+
"target": target_agent, "reason": reason}
|
|
1213
|
+
|
|
1214
|
+
# Return empty string - the actual handoff happens in the process method
|
|
1215
|
+
return ""
|
|
1216
|
+
|
|
1217
|
+
return request_handoff
|
|
1218
|
+
|
|
1219
|
+
# Use the factory to create a properly-bound tool function
|
|
1220
|
+
handoff_tool = create_handoff_tool(agent_name, available_targets)
|
|
1221
|
+
|
|
1222
|
+
# Initialize handoff info attribute
|
|
1223
|
+
agent._handoff_info = None
|
|
1224
|
+
|
|
1225
|
+
# Add the updated handoff tool with proper closure
|
|
1226
|
+
agent.add_tool(handoff_tool)
|
|
1227
|
+
|
|
1228
|
+
# Add critical handoff instructions to the agent's base instructions
|
|
1229
|
+
handoff_examples = "\n".join(
|
|
1230
|
+
[
|
|
1231
|
+
f" - `{name}` ({self.specializations[name][:40]}...)"
|
|
1232
|
+
for name in available_targets
|
|
1233
|
+
]
|
|
1234
|
+
)
|
|
1235
|
+
handoff_instructions = f"""
|
|
1236
|
+
STRICT HANDOFF GUIDANCE:
|
|
1237
|
+
1. You must use ONLY the EXACT agent names listed below for handoffs:
|
|
1238
|
+
{handoff_examples}
|
|
1239
|
+
|
|
1240
|
+
2. DO NOT INVENT, MODIFY, OR CREATE NEW AGENT NAMES like "Smart Contract Developer" or "Technical Expert"
|
|
1241
|
+
|
|
1242
|
+
3. For technical implementation questions, use "developer" (not variations like "developer expert" or "tech specialist")
|
|
1243
|
+
|
|
1244
|
+
4. ONLY these EXACT agent names will work for handoffs: {', '.join(available_targets)}
|
|
1245
|
+
"""
|
|
1246
|
+
|
|
1247
|
+
# Update agent instructions with handoff guidance
|
|
1248
|
+
agent._instructions = agent._instructions + "\n\n" + handoff_instructions
|
|
1249
|
+
|
|
1250
|
+
print(
|
|
1251
|
+
f"Updated handoff capabilities for {agent_name} with targets: {available_targets}"
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
async def process(self, user_id: str, user_text: str) -> AsyncGenerator[str, None]:
|
|
1255
|
+
"""Process the user request with appropriate agent and handle handoffs."""
|
|
1256
|
+
try:
|
|
1257
|
+
# Check if any agents are registered
|
|
1258
|
+
if not self.agents:
|
|
1259
|
+
yield "Error: No agents are registered with the system. Please register at least one agent first."
|
|
1260
|
+
return
|
|
1261
|
+
|
|
1262
|
+
# Get routing decision
|
|
1263
|
+
first_agent = next(iter(self.agents.values()))
|
|
1264
|
+
agent_name = await self._get_routing_decision(first_agent, user_text)
|
|
1265
|
+
current_agent = self.agents[agent_name]
|
|
1266
|
+
print(f"Starting conversation with agent: {agent_name}")
|
|
1267
|
+
|
|
1268
|
+
# Initialize a flag for handoff detection
|
|
1269
|
+
handoff_detected = False
|
|
1270
|
+
response_started = False
|
|
1271
|
+
|
|
1272
|
+
# Reset handoff info for this interaction
|
|
1273
|
+
current_agent._handoff_info = None
|
|
1274
|
+
|
|
1275
|
+
# Process initial agent's response
|
|
1276
|
+
async for chunk in current_agent.text(user_id, user_text):
|
|
1277
|
+
# Check for handoff after each chunk
|
|
1278
|
+
if current_agent._handoff_info and not handoff_detected:
|
|
1279
|
+
handoff_detected = True
|
|
1280
|
+
target_name = current_agent._handoff_info["target"]
|
|
1281
|
+
target_agent = self.agents[target_name]
|
|
1282
|
+
reason = current_agent._handoff_info["reason"]
|
|
1283
|
+
|
|
1284
|
+
# Record handoff without waiting
|
|
1285
|
+
asyncio.create_task(
|
|
1286
|
+
self._record_handoff(
|
|
1287
|
+
user_id, agent_name, target_name, reason, user_text
|
|
1288
|
+
)
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
# Process with target agent
|
|
1292
|
+
print(f"[HANDOFF] Forwarding to {target_name}")
|
|
1293
|
+
handoff_query = f"""
|
|
1294
|
+
Answer this ENTIRE question completely from scratch:
|
|
1295
|
+
|
|
1296
|
+
{user_text}
|
|
1297
|
+
|
|
1298
|
+
IMPORTANT INSTRUCTIONS:
|
|
1299
|
+
1. Address ALL aspects of the question comprehensively
|
|
1300
|
+
2. Organize your response in a logical, structured manner
|
|
1301
|
+
3. Include both explanations AND implementations as needed
|
|
1302
|
+
4. Do not mention any handoff or that you're continuing from another agent
|
|
1303
|
+
5. Answer as if you are addressing the complete question from the beginning
|
|
1304
|
+
6. Consider any relevant context from previous conversation
|
|
1305
|
+
"""
|
|
1306
|
+
|
|
1307
|
+
# If we've already started returning some text, add a separator
|
|
1308
|
+
if response_started:
|
|
1309
|
+
yield "\n\n---\n\n"
|
|
1310
|
+
|
|
1311
|
+
# Stream directly from target agent
|
|
1312
|
+
async for new_chunk in target_agent.text(user_id, handoff_query):
|
|
1313
|
+
yield new_chunk
|
|
1314
|
+
# Force immediate delivery of each chunk
|
|
1315
|
+
await asyncio.sleep(0)
|
|
1316
|
+
return
|
|
1317
|
+
else:
|
|
1318
|
+
# Only yield content if no handoff has been detected
|
|
1319
|
+
if not handoff_detected:
|
|
1320
|
+
response_started = True
|
|
1321
|
+
yield chunk
|
|
1322
|
+
await asyncio.sleep(0) # Force immediate delivery
|
|
1323
|
+
|
|
1324
|
+
except Exception as e:
|
|
1325
|
+
print(f"Error in multi-agent processing: {str(e)}")
|
|
1326
|
+
print(traceback.format_exc())
|
|
1327
|
+
yield "\n\nI apologize for the technical difficulty.\n\n"
|
|
1328
|
+
|
|
1329
|
+
async def _get_routing_decision(self, agent, user_text):
|
|
1330
|
+
"""Get routing decision in parallel to reduce latency."""
|
|
1331
|
+
enhanced_prompt = f"""
|
|
1332
|
+
Analyze this user query carefully to determine the MOST APPROPRIATE specialist.
|
|
1333
|
+
|
|
1334
|
+
User query: "{user_text}"
|
|
1335
|
+
|
|
1336
|
+
Available specialists:
|
|
1337
|
+
{json.dumps(self.specializations, indent=2)}
|
|
1338
|
+
|
|
1339
|
+
CRITICAL ROUTING INSTRUCTIONS:
|
|
1340
|
+
1. For compound questions with multiple aspects spanning different domains,
|
|
1341
|
+
choose the specialist who should address the CONCEPTUAL or EDUCATIONAL aspects first.
|
|
1342
|
+
|
|
1343
|
+
2. Choose implementation specialists (technical, development, coding) only when
|
|
1344
|
+
the query is PURELY about implementation with no conceptual explanation needed.
|
|
1345
|
+
|
|
1346
|
+
3. When a query involves a SEQUENCE (like "explain X and then do Y"),
|
|
1347
|
+
prioritize the specialist handling the FIRST part of the sequence.
|
|
1348
|
+
|
|
1349
|
+
Return ONLY the name of the single most appropriate specialist.
|
|
1350
|
+
"""
|
|
1351
|
+
|
|
1352
|
+
# Route to appropriate agent
|
|
1353
|
+
router_response = agent._client.chat.completions.create(
|
|
1354
|
+
model=self.router_model,
|
|
1355
|
+
messages=[
|
|
1356
|
+
{"role": "system", "content": enhanced_prompt},
|
|
1357
|
+
{"role": "user", "content": user_text},
|
|
1358
|
+
],
|
|
1359
|
+
temperature=0.2,
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
# Extract the selected agent
|
|
1363
|
+
raw_response = router_response.choices[0].message.content.strip()
|
|
1364
|
+
print(f"Router model raw response: '{raw_response}'")
|
|
1365
|
+
|
|
1366
|
+
return self._match_agent_name(raw_response)
|
|
1367
|
+
|
|
1368
|
+
async def _record_handoff(self, user_id, from_agent, to_agent, reason, query):
|
|
1369
|
+
"""Record handoff in database without blocking the main flow."""
|
|
1370
|
+
self.handoffs.insert_one(
|
|
1371
|
+
{
|
|
1372
|
+
"user_id": user_id,
|
|
1373
|
+
"from_agent": from_agent,
|
|
1374
|
+
"to_agent": to_agent,
|
|
1375
|
+
"reason": reason,
|
|
1376
|
+
"query": query,
|
|
1377
|
+
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
|
1378
|
+
}
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
def _match_agent_name(self, raw_response):
|
|
1382
|
+
"""Match router response to an actual agent name."""
|
|
1383
|
+
# Exact match (priority)
|
|
1384
|
+
if raw_response in self.agents:
|
|
1385
|
+
return raw_response
|
|
1386
|
+
|
|
1387
|
+
# Case-insensitive match
|
|
1388
|
+
for name in self.agents:
|
|
1389
|
+
if name.lower() == raw_response.lower():
|
|
1390
|
+
return name
|
|
1391
|
+
|
|
1392
|
+
# Partial match
|
|
1393
|
+
for name in self.agents:
|
|
1394
|
+
if name.lower() in raw_response.lower():
|
|
1395
|
+
return name
|
|
1396
|
+
|
|
1397
|
+
# Fallback to first agent
|
|
1398
|
+
return list(self.agents.keys())[0]
|
|
File without changes
|
|
File without changes
|