solana-agent 20.1.2__py3-none-any.whl → 31.4.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 (45) hide show
  1. solana_agent/__init__.py +10 -5
  2. solana_agent/adapters/ffmpeg_transcoder.py +375 -0
  3. solana_agent/adapters/mongodb_adapter.py +15 -2
  4. solana_agent/adapters/openai_adapter.py +679 -0
  5. solana_agent/adapters/openai_realtime_ws.py +1813 -0
  6. solana_agent/adapters/pinecone_adapter.py +543 -0
  7. solana_agent/cli.py +128 -0
  8. solana_agent/client/solana_agent.py +180 -20
  9. solana_agent/domains/agent.py +13 -13
  10. solana_agent/domains/routing.py +18 -8
  11. solana_agent/factories/agent_factory.py +239 -38
  12. solana_agent/guardrails/pii.py +107 -0
  13. solana_agent/interfaces/client/client.py +95 -12
  14. solana_agent/interfaces/guardrails/guardrails.py +26 -0
  15. solana_agent/interfaces/plugins/plugins.py +2 -1
  16. solana_agent/interfaces/providers/__init__.py +0 -0
  17. solana_agent/interfaces/providers/audio.py +40 -0
  18. solana_agent/interfaces/providers/data_storage.py +9 -2
  19. solana_agent/interfaces/providers/llm.py +86 -9
  20. solana_agent/interfaces/providers/memory.py +13 -1
  21. solana_agent/interfaces/providers/realtime.py +212 -0
  22. solana_agent/interfaces/providers/vector_storage.py +53 -0
  23. solana_agent/interfaces/services/agent.py +27 -12
  24. solana_agent/interfaces/services/knowledge_base.py +59 -0
  25. solana_agent/interfaces/services/query.py +41 -8
  26. solana_agent/interfaces/services/routing.py +0 -1
  27. solana_agent/plugins/manager.py +37 -16
  28. solana_agent/plugins/registry.py +34 -19
  29. solana_agent/plugins/tools/__init__.py +0 -5
  30. solana_agent/plugins/tools/auto_tool.py +1 -0
  31. solana_agent/repositories/memory.py +332 -111
  32. solana_agent/services/__init__.py +1 -1
  33. solana_agent/services/agent.py +390 -241
  34. solana_agent/services/knowledge_base.py +768 -0
  35. solana_agent/services/query.py +1858 -153
  36. solana_agent/services/realtime.py +626 -0
  37. solana_agent/services/routing.py +104 -51
  38. solana_agent-31.4.0.dist-info/METADATA +1070 -0
  39. solana_agent-31.4.0.dist-info/RECORD +49 -0
  40. {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info}/WHEEL +1 -1
  41. solana_agent-31.4.0.dist-info/entry_points.txt +3 -0
  42. solana_agent/adapters/llm_adapter.py +0 -160
  43. solana_agent-20.1.2.dist-info/METADATA +0 -464
  44. solana_agent-20.1.2.dist-info/RECORD +0 -35
  45. {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info/licenses}/LICENSE +0 -0
@@ -1,11 +1,16 @@
1
- from typing import List, Dict, Any, Optional, Tuple
1
+ import logging
2
+ from typing import List, Dict, Optional, Tuple, Any
2
3
  from datetime import datetime, timezone
4
+ from copy import deepcopy
5
+
3
6
  from zep_cloud.client import AsyncZep as AsyncZepCloud
4
- from zep_python.client import AsyncZep
5
7
  from zep_cloud.types import Message
8
+
6
9
  from solana_agent.interfaces.providers.memory import MemoryProvider
7
10
  from solana_agent.adapters.mongodb_adapter import MongoDBAdapter
8
11
 
12
+ logger = logging.getLogger(__name__)
13
+
9
14
 
10
15
  class MemoryRepository(MemoryProvider):
11
16
  """Combined Zep and MongoDB implementation of MemoryProvider."""
@@ -14,146 +19,316 @@ class MemoryRepository(MemoryProvider):
14
19
  self,
15
20
  mongo_adapter: Optional[MongoDBAdapter] = None,
16
21
  zep_api_key: Optional[str] = None,
17
- zep_base_url: Optional[str] = None
18
22
  ):
19
- """Initialize the combined memory provider."""
23
+ # Mongo setup
20
24
  if not mongo_adapter:
21
25
  self.mongo = None
22
26
  self.collection = None
27
+ self.captures_collection = "captures"
28
+ self.stream_collection = "conversation_stream"
23
29
  else:
24
- # Initialize MongoDB
25
30
  self.mongo = mongo_adapter
26
31
  self.collection = "conversations"
27
-
28
32
  try:
29
- # Ensure MongoDB collection and indexes
30
33
  self.mongo.create_collection(self.collection)
31
34
  self.mongo.create_index(self.collection, [("user_id", 1)])
32
35
  self.mongo.create_index(self.collection, [("timestamp", 1)])
33
- except Exception as e:
34
- print(f"Error initializing MongoDB: {e}")
35
-
36
- # Initialize Zep
37
- if zep_api_key and not zep_base_url:
38
- self.zep = AsyncZepCloud(api_key=zep_api_key)
39
- elif zep_api_key and zep_base_url:
40
- self.zep = AsyncZep(api_key=zep_api_key, base_url=zep_base_url)
41
- else:
42
- self.zep = None
36
+ except Exception as e: # pragma: no cover
37
+ logger.error(f"Error initializing MongoDB: {e}")
38
+
39
+ try:
40
+ self.captures_collection = "captures"
41
+ self.mongo.create_collection(self.captures_collection)
42
+ # Basic indexes
43
+ self.mongo.create_index(self.captures_collection, [("user_id", 1)])
44
+ self.mongo.create_index(self.captures_collection, [("capture_name", 1)])
45
+ self.mongo.create_index(self.captures_collection, [("agent_name", 1)])
46
+ self.mongo.create_index(self.captures_collection, [("timestamp", 1)])
47
+ # Unique per user/agent/capture combo
48
+ try:
49
+ self.mongo.create_index(
50
+ self.captures_collection,
51
+ [("user_id", 1), ("agent_name", 1), ("capture_name", 1)],
52
+ unique=True,
53
+ )
54
+ except Exception as e: # pragma: no cover
55
+ logger.error(f"Error creating unique index for captures: {e}")
56
+ except Exception as e: # pragma: no cover
57
+ logger.error(f"Error initializing MongoDB captures collection: {e}")
58
+ self.captures_collection = "captures"
59
+
60
+ # Defer stream collection creation to first use to preserve legacy init expectations
61
+ self.stream_collection = "conversation_stream"
62
+
63
+ # Zep setup
64
+ self.zep = AsyncZepCloud(api_key=zep_api_key) if zep_api_key else None
65
+
66
+ # --- Realtime streaming helpers (Mongo only) ---
67
+ async def begin_stream_turn(
68
+ self, user_id: str
69
+ ) -> Optional[str]: # pragma: no cover
70
+ """Begin a realtime turn by creating/returning a turn_id (Mongo only)."""
71
+ if not self.mongo:
72
+ return None
73
+ from uuid import uuid4
74
+
75
+ turn_id = str(uuid4())
76
+ try:
77
+ now = datetime.now(timezone.utc)
78
+ # Ensure stream collection and indexes exist lazily
79
+ try:
80
+ if not self.mongo.collection_exists(self.stream_collection):
81
+ self.mongo.create_collection(self.stream_collection)
82
+ self.mongo.create_index(self.stream_collection, [("user_id", 1)])
83
+ self.mongo.create_index(
84
+ self.stream_collection, [("turn_id", 1)], unique=True
85
+ )
86
+ self.mongo.create_index(self.stream_collection, [("partial", 1)])
87
+ self.mongo.create_index(self.stream_collection, [("timestamp", 1)])
88
+ except Exception: # pragma: no cover
89
+ pass
90
+ self.mongo.insert_one(
91
+ self.stream_collection,
92
+ {
93
+ "user_id": user_id,
94
+ "turn_id": turn_id,
95
+ "user_partial": "",
96
+ "assistant_partial": "",
97
+ "partial": True,
98
+ "timestamp": now,
99
+ "created_at": now,
100
+ },
101
+ )
102
+ return turn_id
103
+ except Exception as e: # pragma: no cover
104
+ logger.error(f"MongoDB begin_stream_turn error: {e}")
105
+ return None
106
+
107
+ async def update_stream_user(
108
+ self, user_id: str, turn_id: str, delta: str
109
+ ) -> None: # pragma: no cover
110
+ if not self.mongo or not delta:
111
+ return
112
+ try:
113
+ doc = self.mongo.find_one(
114
+ self.stream_collection, {"turn_id": turn_id, "user_id": user_id}
115
+ )
116
+ if not doc:
117
+ return
118
+ content = (doc.get("user_partial") or "") + delta
119
+ self.mongo.update_one(
120
+ self.stream_collection,
121
+ {"turn_id": turn_id, "user_id": user_id},
122
+ {
123
+ "$set": {
124
+ "user_partial": content,
125
+ "timestamp": datetime.now(timezone.utc),
126
+ }
127
+ },
128
+ upsert=False,
129
+ )
130
+ except Exception as e: # pragma: no cover
131
+ logger.error(f"MongoDB update_stream_user error: {e}")
132
+
133
+ async def update_stream_assistant( # pragma: no cover
134
+ self, user_id: str, turn_id: str, delta: str
135
+ ) -> None:
136
+ if not self.mongo or not delta:
137
+ return
138
+ try:
139
+ doc = self.mongo.find_one(
140
+ self.stream_collection, {"turn_id": turn_id, "user_id": user_id}
141
+ )
142
+ if not doc:
143
+ return
144
+ content = (doc.get("assistant_partial") or "") + delta
145
+ self.mongo.update_one(
146
+ self.stream_collection,
147
+ {"turn_id": turn_id, "user_id": user_id},
148
+ {
149
+ "$set": {
150
+ "assistant_partial": content,
151
+ "timestamp": datetime.now(timezone.utc),
152
+ }
153
+ },
154
+ upsert=False,
155
+ )
156
+ except Exception as e: # pragma: no cover
157
+ logger.error(f"MongoDB update_stream_assistant error: {e}")
158
+
159
+ async def finalize_stream_turn(
160
+ self, user_id: str, turn_id: str
161
+ ) -> None: # pragma: no cover
162
+ if not self.mongo:
163
+ return
164
+ try:
165
+ doc = self.mongo.find_one(
166
+ self.stream_collection, {"turn_id": turn_id, "user_id": user_id}
167
+ )
168
+ if not doc:
169
+ return
170
+ user_text = doc.get("user_partial", "")
171
+ assistant_text = doc.get("assistant_partial", "")
172
+ now = datetime.now(timezone.utc)
173
+ self.mongo.update_one(
174
+ self.stream_collection,
175
+ {"turn_id": turn_id, "user_id": user_id},
176
+ {"$set": {"partial": False, "timestamp": now, "finalized_at": now}},
177
+ upsert=False,
178
+ )
179
+ # Also persist to conversations collection as a complete turn
180
+ if user_text or assistant_text:
181
+ try:
182
+ self.mongo.insert_one(
183
+ self.collection,
184
+ {
185
+ "user_id": user_id,
186
+ "user_message": user_text,
187
+ "assistant_message": assistant_text,
188
+ "timestamp": now,
189
+ },
190
+ )
191
+ except Exception as e: # pragma: no cover
192
+ logger.error(
193
+ f"MongoDB finalize_stream_turn insert conversations error: {e}"
194
+ )
195
+ except Exception as e: # pragma: no cover
196
+ logger.error(f"MongoDB finalize_stream_turn error: {e}")
43
197
 
44
198
  async def store(self, user_id: str, messages: List[Dict[str, Any]]) -> None:
45
- """Store messages in both Zep and MongoDB."""
46
- if not user_id:
199
+ if not user_id or not isinstance(user_id, str):
47
200
  raise ValueError("User ID cannot be None or empty")
48
201
  if not messages or not isinstance(messages, list):
49
202
  raise ValueError("Messages must be a non-empty list")
50
- if not all(isinstance(msg, dict) and "role" in msg and "content" in msg for msg in messages):
203
+ if not all(
204
+ isinstance(m, dict) and "role" in m and "content" in m for m in messages
205
+ ):
51
206
  raise ValueError(
52
- "All messages must be dictionaries with 'role' and 'content' keys")
53
- for msg in messages:
54
- if msg["role"] not in ["user", "assistant"]:
207
+ "All messages must be dictionaries with 'role' and 'content' keys"
208
+ )
209
+ for m in messages:
210
+ if m["role"] not in ["user", "assistant"]:
55
211
  raise ValueError(
56
- f"Invalid role '{msg['role']}' in message. Only 'user' and 'assistant' roles are accepted.")
212
+ "Invalid role in message. Only 'user' and 'assistant' are accepted."
213
+ )
57
214
 
58
- # Store in MongoDB
215
+ # Persist last user/assistant pair to Mongo
59
216
  if self.mongo and len(messages) >= 2:
60
217
  try:
61
- # Get last user and assistant messages
62
218
  user_msg = None
63
219
  assistant_msg = None
64
- for msg in reversed(messages):
65
- if msg.get("role") == "user" and not user_msg:
66
- user_msg = msg.get("content")
67
- elif msg.get("role") == "assistant" and not assistant_msg:
68
- assistant_msg = msg.get("content")
220
+ for m in reversed(messages):
221
+ if m.get("role") == "user" and not user_msg:
222
+ user_msg = m.get("content")
223
+ elif m.get("role") == "assistant" and not assistant_msg:
224
+ assistant_msg = m.get("content")
69
225
  if user_msg and assistant_msg:
70
226
  break
71
-
72
227
  if user_msg and assistant_msg:
73
- # Store truncated messages
74
- doc = {
75
- "user_id": user_id,
76
- "user_message": self._truncate(user_msg),
77
- "assistant_message": self._truncate(assistant_msg),
78
- "timestamp": datetime.now(timezone.utc)
79
- }
80
- self.mongo.insert_one(self.collection, doc)
81
- except Exception as e:
82
- print(f"MongoDB storage error: {e}")
228
+ self.mongo.insert_one(
229
+ self.collection,
230
+ {
231
+ "user_id": user_id,
232
+ "user_message": user_msg,
233
+ "assistant_message": assistant_msg,
234
+ "timestamp": datetime.now(timezone.utc),
235
+ },
236
+ )
237
+ except Exception as e: # pragma: no cover
238
+ logger.error(f"MongoDB storage error: {e}")
83
239
 
84
- # Store in Zep
240
+ # Zep
85
241
  if not self.zep:
86
242
  return
87
243
 
88
- try:
89
- await self.zep.user.add(user_id=user_id)
90
- except Exception as e:
91
- print(f"Zep user addition error: {e}")
92
-
93
- try:
94
- await self.zep.memory.add_session(session_id=user_id, user_id=user_id)
95
- except Exception as e:
96
- print(f"Zep session creation error: {e}")
97
-
98
- # Convert messages to Zep format
99
- zep_messages = []
100
- for msg in messages:
101
- if "role" in msg and "content" in msg:
102
- zep_msg = Message(
103
- role=msg["role"],
104
- content=msg["content"],
105
- role_type=msg["role"],
106
- )
107
- zep_messages.append(zep_msg)
244
+ zep_messages: List[Message] = []
245
+ for m in messages:
246
+ content = (
247
+ self._truncate(deepcopy(m.get("content"))) if "content" in m else None
248
+ )
249
+ if content is None: # pragma: no cover
250
+ continue
251
+ role_type = "user" if m.get("role") == "user" else "assistant"
252
+ zep_messages.append(Message(content=content, role=role_type))
108
253
 
109
- # Add messages to Zep memory
110
254
  if zep_messages:
111
255
  try:
112
- await self.zep.memory.add(
113
- session_id=user_id,
114
- messages=zep_messages
256
+ await self.zep.thread.add_messages(
257
+ thread_id=user_id, messages=zep_messages
115
258
  )
116
- except Exception as e:
117
- print(f"Zep memory addition error: {e}")
118
-
119
- async def retrieve(self, user_id: str) -> str:
120
- """Retrieve memory context from Zep only."""
121
- if not self.zep:
122
- return ""
259
+ except Exception: # pragma: no cover
260
+ try:
261
+ try:
262
+ await self.zep.user.add(user_id=user_id)
263
+ except Exception as e: # pragma: no cover
264
+ logger.error(f"Zep user addition error: {e}")
265
+ try:
266
+ await self.zep.thread.create(thread_id=user_id, user_id=user_id)
267
+ except Exception as e: # pragma: no cover
268
+ logger.error(f"Zep thread creation error: {e}")
269
+ await self.zep.thread.add_messages(
270
+ thread_id=user_id, messages=zep_messages
271
+ )
272
+ except Exception as e: # pragma: no cover
273
+ logger.error(f"Zep memory addition error: {e}")
123
274
 
275
+ async def retrieve(self, user_id: str) -> str: # pragma: no cover
124
276
  try:
125
- memory = await self.zep.memory.get(session_id=user_id)
126
- if memory is None or not hasattr(memory, 'context') or memory.context is None:
127
- return ""
128
- return memory.context
277
+ # Preferred: Zep user context
278
+ memories = ""
279
+ if self.zep:
280
+ try:
281
+ memory = await self.zep.thread.get_user_context(thread_id=user_id)
282
+ if memory and memory.context:
283
+ memories = memory.context
284
+ except Exception as e: # pragma: no cover
285
+ logger.error(f"Zep retrieval error: {e}")
129
286
 
130
- except Exception as e:
131
- print(f"Error retrieving Zep memory: {e}")
287
+ # Fallback: Build lightweight conversation history from Mongo if available
288
+ if not memories and self.mongo:
289
+ try:
290
+ # Fetch last 10 conversations for this user in ascending time order
291
+ docs = self.mongo.find(
292
+ self.collection,
293
+ {"user_id": user_id},
294
+ sort=[("timestamp", 1)],
295
+ limit=10,
296
+ )
297
+ if docs:
298
+ parts: List[str] = []
299
+ for d in docs:
300
+ u = (d or {}).get("user_message") or ""
301
+ a = (d or {}).get("assistant_message") or ""
302
+ # Only include complete turns to avoid partial/ambiguous history
303
+ if u and a:
304
+ parts.append(f"User: {u}")
305
+ parts.append(f"Assistant: {a}")
306
+ parts.append("") # blank line between turns
307
+ memories = "\n".join(parts).strip()
308
+ except Exception as e: # pragma: no cover
309
+ logger.error(f"Mongo fallback retrieval error: {e}")
310
+
311
+ return memories or ""
312
+ except Exception as e: # pragma: no cover
313
+ logger.error(f"Error retrieving memories: {e}")
132
314
  return ""
133
315
 
134
- async def delete(self, user_id: str) -> None:
135
- """Delete memory from both systems."""
316
+ async def delete(self, user_id: str) -> None: # pragma: no cover
136
317
  if self.mongo:
137
318
  try:
138
- self.mongo.delete_all(
139
- self.collection,
140
- {"user_id": user_id}
141
- )
142
- except Exception as e:
143
- print(f"MongoDB deletion error: {e}")
144
-
319
+ self.mongo.delete_all(self.collection, {"user_id": user_id})
320
+ except Exception as e: # pragma: no cover
321
+ logger.error(f"MongoDB deletion error: {e}")
145
322
  if not self.zep:
146
323
  return
147
-
148
324
  try:
149
- await self.zep.memory.delete(session_id=user_id)
150
- except Exception as e:
151
- print(f"Zep memory deletion error: {e}")
152
-
325
+ await self.zep.thread.delete(thread_id=user_id)
326
+ except Exception as e: # pragma: no cover
327
+ logger.error(f"Zep memory deletion error: {e}")
153
328
  try:
154
329
  await self.zep.user.delete(user_id=user_id)
155
- except Exception as e:
156
- print(f"Zep user deletion error: {e}")
330
+ except Exception as e: # pragma: no cover
331
+ logger.error(f"Zep user deletion error: {e}")
157
332
 
158
333
  def find(
159
334
  self,
@@ -161,39 +336,85 @@ class MemoryRepository(MemoryProvider):
161
336
  query: Dict,
162
337
  sort: Optional[List[Tuple]] = None,
163
338
  limit: int = 0,
164
- skip: int = 0
339
+ skip: int = 0,
165
340
  ) -> List[Dict]: # pragma: no cover
166
- """Find documents in MongoDB."""
167
341
  if not self.mongo:
168
342
  return []
169
-
170
343
  try:
171
344
  return self.mongo.find(collection, query, sort=sort, limit=limit, skip=skip)
172
- except Exception as e:
173
- print(f"MongoDB find error: {e}")
345
+ except Exception as e: # pragma: no cover
346
+ logger.error(f"MongoDB find error: {e}")
174
347
  return []
175
348
 
176
349
  def count_documents(self, collection: str, query: Dict) -> int:
177
- """Count documents in MongoDB."""
178
350
  if not self.mongo:
179
351
  return 0
180
352
  return self.mongo.count_documents(collection, query)
181
353
 
182
354
  def _truncate(self, text: str, limit: int = 2500) -> str:
183
- """Truncate text to be within limits."""
184
355
  if text is None:
185
356
  raise AttributeError("Cannot truncate None text")
186
-
187
357
  if not text:
188
358
  return ""
189
-
190
359
  if len(text) <= limit:
191
360
  return text
192
-
193
- # Try to truncate at last period before limit
194
- last_period = text.rfind('.', 0, limit)
361
+ last_period = text.rfind(".", 0, limit)
195
362
  if last_period > 0:
196
- return text[:last_period + 1]
363
+ return text[: last_period + 1]
364
+ return text[: limit - 3] + "..."
197
365
 
198
- # If no period found, truncate at limit and add ellipsis
199
- return text[:limit] + "..."
366
+ async def save_capture(
367
+ self,
368
+ user_id: str,
369
+ capture_name: str,
370
+ agent_name: Optional[str],
371
+ data: Dict[str, Any],
372
+ schema: Optional[Dict[str, Any]] = None,
373
+ ) -> Optional[str]:
374
+ if not self.mongo: # pragma: no cover
375
+ logger.warning("MongoDB not configured; cannot save capture.")
376
+ return None
377
+ if not user_id or not isinstance(user_id, str):
378
+ raise ValueError("user_id must be a non-empty string")
379
+ if not capture_name or not isinstance(capture_name, str):
380
+ raise ValueError("capture_name must be a non-empty string")
381
+ if not isinstance(data, dict):
382
+ raise ValueError("data must be a dictionary")
383
+
384
+ try:
385
+ now = datetime.now(timezone.utc)
386
+ key = {
387
+ "user_id": user_id,
388
+ "agent_name": agent_name,
389
+ "capture_name": capture_name,
390
+ }
391
+ existing = self.mongo.find_one(self.captures_collection, key)
392
+ merged_data: Dict[str, Any] = {}
393
+ if existing and isinstance(existing.get("data"), dict):
394
+ merged_data.update(existing.get("data", {}))
395
+ merged_data.update(data or {})
396
+ update_doc = {
397
+ "$set": {
398
+ "user_id": user_id,
399
+ "agent_name": agent_name,
400
+ "capture_name": capture_name,
401
+ "data": merged_data,
402
+ "schema": (
403
+ schema
404
+ if schema is not None
405
+ else existing.get("schema")
406
+ if existing
407
+ else {}
408
+ ),
409
+ "timestamp": now,
410
+ },
411
+ "$setOnInsert": {"created_at": now},
412
+ }
413
+ self.mongo.update_one(
414
+ self.captures_collection, key, update_doc, upsert=True
415
+ )
416
+ doc = self.mongo.find_one(self.captures_collection, key)
417
+ return str(doc.get("_id")) if doc and doc.get("_id") else None
418
+ except Exception as e: # pragma: no cover
419
+ logger.error(f"MongoDB save_capture error: {e}")
420
+ return None
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Service implementations for the Solana Agent system.
3
3
 
4
- These services implement the business logic interfaces defined in
4
+ These services implement the business logic interfaces defined in
5
5
  solana_agent.interfaces.services.
6
6
  """