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.
- solana_agent/__init__.py +10 -5
- solana_agent/adapters/ffmpeg_transcoder.py +375 -0
- solana_agent/adapters/mongodb_adapter.py +15 -2
- solana_agent/adapters/openai_adapter.py +679 -0
- solana_agent/adapters/openai_realtime_ws.py +1813 -0
- solana_agent/adapters/pinecone_adapter.py +543 -0
- solana_agent/cli.py +128 -0
- solana_agent/client/solana_agent.py +180 -20
- solana_agent/domains/agent.py +13 -13
- solana_agent/domains/routing.py +18 -8
- solana_agent/factories/agent_factory.py +239 -38
- solana_agent/guardrails/pii.py +107 -0
- solana_agent/interfaces/client/client.py +95 -12
- solana_agent/interfaces/guardrails/guardrails.py +26 -0
- solana_agent/interfaces/plugins/plugins.py +2 -1
- solana_agent/interfaces/providers/__init__.py +0 -0
- solana_agent/interfaces/providers/audio.py +40 -0
- solana_agent/interfaces/providers/data_storage.py +9 -2
- solana_agent/interfaces/providers/llm.py +86 -9
- solana_agent/interfaces/providers/memory.py +13 -1
- solana_agent/interfaces/providers/realtime.py +212 -0
- solana_agent/interfaces/providers/vector_storage.py +53 -0
- solana_agent/interfaces/services/agent.py +27 -12
- solana_agent/interfaces/services/knowledge_base.py +59 -0
- solana_agent/interfaces/services/query.py +41 -8
- solana_agent/interfaces/services/routing.py +0 -1
- solana_agent/plugins/manager.py +37 -16
- solana_agent/plugins/registry.py +34 -19
- solana_agent/plugins/tools/__init__.py +0 -5
- solana_agent/plugins/tools/auto_tool.py +1 -0
- solana_agent/repositories/memory.py +332 -111
- solana_agent/services/__init__.py +1 -1
- solana_agent/services/agent.py +390 -241
- solana_agent/services/knowledge_base.py +768 -0
- solana_agent/services/query.py +1858 -153
- solana_agent/services/realtime.py +626 -0
- solana_agent/services/routing.py +104 -51
- solana_agent-31.4.0.dist-info/METADATA +1070 -0
- solana_agent-31.4.0.dist-info/RECORD +49 -0
- {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info}/WHEEL +1 -1
- solana_agent-31.4.0.dist-info/entry_points.txt +3 -0
- solana_agent/adapters/llm_adapter.py +0 -160
- solana_agent-20.1.2.dist-info/METADATA +0 -464
- solana_agent-20.1.2.dist-info/RECORD +0 -35
- {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
212
|
+
"Invalid role in message. Only 'user' and 'assistant' are accepted."
|
|
213
|
+
)
|
|
57
214
|
|
|
58
|
-
#
|
|
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
|
|
65
|
-
if
|
|
66
|
-
user_msg =
|
|
67
|
-
elif
|
|
68
|
-
assistant_msg =
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
#
|
|
240
|
+
# Zep
|
|
85
241
|
if not self.zep:
|
|
86
242
|
return
|
|
87
243
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
113
|
-
|
|
114
|
-
messages=zep_messages
|
|
256
|
+
await self.zep.thread.add_messages(
|
|
257
|
+
thread_id=user_id, messages=zep_messages
|
|
115
258
|
)
|
|
116
|
-
except Exception
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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.
|
|
150
|
-
except Exception as e:
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|