solana-agent 31.1.0__py3-none-any.whl → 31.1.2__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/factories/agent_factory.py +7 -0
- solana_agent/services/query.py +396 -210
- solana_agent/services/routing.py +19 -13
- {solana_agent-31.1.0.dist-info → solana_agent-31.1.2.dist-info}/METADATA +6 -6
- {solana_agent-31.1.0.dist-info → solana_agent-31.1.2.dist-info}/RECORD +8 -8
- {solana_agent-31.1.0.dist-info → solana_agent-31.1.2.dist-info}/LICENSE +0 -0
- {solana_agent-31.1.0.dist-info → solana_agent-31.1.2.dist-info}/WHEEL +0 -0
- {solana_agent-31.1.0.dist-info → solana_agent-31.1.2.dist-info}/entry_points.txt +0 -0
@@ -186,9 +186,16 @@ class SolanaAgentFactory:
|
|
186
186
|
)
|
187
187
|
|
188
188
|
# Create routing service
|
189
|
+
# Optional routing model override (use small, cheap model by default in service)
|
190
|
+
routing_model = (
|
191
|
+
config.get("openai", {}).get("routing_model")
|
192
|
+
if isinstance(config.get("openai"), dict)
|
193
|
+
else None
|
194
|
+
)
|
189
195
|
routing_service = RoutingService(
|
190
196
|
llm_provider=llm_adapter,
|
191
197
|
agent_service=agent_service,
|
198
|
+
model=routing_model,
|
192
199
|
)
|
193
200
|
|
194
201
|
# Debug the agent service tool registry
|
solana_agent/services/query.py
CHANGED
@@ -7,6 +7,7 @@ clean separation of concerns.
|
|
7
7
|
"""
|
8
8
|
|
9
9
|
import logging
|
10
|
+
import re
|
10
11
|
from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Type, Union
|
11
12
|
|
12
13
|
from pydantic import BaseModel
|
@@ -22,9 +23,7 @@ from solana_agent.interfaces.providers.memory import (
|
|
22
23
|
from solana_agent.interfaces.services.knowledge_base import (
|
23
24
|
KnowledgeBaseService as KnowledgeBaseInterface,
|
24
25
|
)
|
25
|
-
from solana_agent.interfaces.guardrails.guardrails import
|
26
|
-
InputGuardrail,
|
27
|
-
)
|
26
|
+
from solana_agent.interfaces.guardrails.guardrails import InputGuardrail
|
28
27
|
|
29
28
|
from solana_agent.services.agent import AgentService
|
30
29
|
from solana_agent.services.routing import RoutingService
|
@@ -44,16 +43,7 @@ class QueryService(QueryServiceInterface):
|
|
44
43
|
kb_results_count: int = 3,
|
45
44
|
input_guardrails: List[InputGuardrail] = None,
|
46
45
|
):
|
47
|
-
"""Initialize the query service.
|
48
|
-
|
49
|
-
Args:
|
50
|
-
agent_service: Service for AI agent management
|
51
|
-
routing_service: Service for routing queries to appropriate agents
|
52
|
-
memory_provider: Optional provider for memory storage and retrieval
|
53
|
-
knowledge_base: Optional provider for knowledge base interactions
|
54
|
-
kb_results_count: Number of results to retrieve from knowledge base
|
55
|
-
input_guardrails: List of input guardrail instances
|
56
|
-
"""
|
46
|
+
"""Initialize the query service."""
|
57
47
|
self.agent_service = agent_service
|
58
48
|
self.routing_service = routing_service
|
59
49
|
self.memory_provider = memory_provider
|
@@ -92,26 +82,9 @@ class QueryService(QueryServiceInterface):
|
|
92
82
|
capture_schema: Optional[Dict[str, Any]] = None,
|
93
83
|
capture_name: Optional[str] = None,
|
94
84
|
) -> AsyncGenerator[Union[str, bytes, BaseModel], None]: # pragma: no cover
|
95
|
-
"""Process the user request
|
96
|
-
|
97
|
-
Args:
|
98
|
-
user_id: User ID
|
99
|
-
query: Text query or audio bytes
|
100
|
-
images: Optional list of image URLs (str) or image bytes.
|
101
|
-
output_format: Response format ("text" or "audio")
|
102
|
-
audio_voice: Voice for TTS (text-to-speech)
|
103
|
-
audio_instructions: Audio voice instructions
|
104
|
-
audio_output_format: Audio output format
|
105
|
-
audio_input_format: Audio input format
|
106
|
-
prompt: Optional prompt for the agent
|
107
|
-
router: Optional routing service for processing
|
108
|
-
output_model: Optional Pydantic model for structured output
|
109
|
-
|
110
|
-
Yields:
|
111
|
-
Response chunks (text strings or audio bytes)
|
112
|
-
"""
|
85
|
+
"""Process the user request and generate a response."""
|
113
86
|
try:
|
114
|
-
#
|
87
|
+
# 1) Transcribe audio or accept text
|
115
88
|
user_text = ""
|
116
89
|
if not isinstance(query, str):
|
117
90
|
logger.info(
|
@@ -128,123 +101,405 @@ class QueryService(QueryServiceInterface):
|
|
128
101
|
user_text = query
|
129
102
|
logger.info(f"Received text input length: {len(user_text)}")
|
130
103
|
|
131
|
-
#
|
104
|
+
# 2) Input guardrails
|
132
105
|
original_text = user_text
|
133
|
-
processed_text = user_text
|
134
106
|
for guardrail in self.input_guardrails:
|
135
107
|
try:
|
136
|
-
|
137
|
-
logger.debug(
|
138
|
-
f"Applied input guardrail: {guardrail.__class__.__name__}"
|
139
|
-
)
|
108
|
+
user_text = await guardrail.process(user_text)
|
140
109
|
except Exception as e:
|
141
|
-
logger.error
|
142
|
-
|
143
|
-
exc_info=True,
|
144
|
-
)
|
145
|
-
if processed_text != original_text:
|
110
|
+
logger.debug(f"Guardrail error: {e}")
|
111
|
+
if user_text != original_text:
|
146
112
|
logger.info(
|
147
|
-
f"Input guardrails modified user text. Original length: {len(original_text)}, New length: {len(
|
113
|
+
f"Input guardrails modified user text. Original length: {len(original_text)}, New length: {len(user_text)}"
|
148
114
|
)
|
149
|
-
user_text = processed_text # Use the processed text going forward
|
150
|
-
# --- End Apply Input Guardrails ---
|
151
115
|
|
152
|
-
#
|
153
|
-
|
154
|
-
if not images and user_text.strip().lower() in [
|
155
|
-
"test",
|
156
|
-
"hello",
|
116
|
+
# 3) Greetings shortcut
|
117
|
+
if not images and user_text.strip().lower() in {
|
157
118
|
"hi",
|
119
|
+
"hello",
|
158
120
|
"hey",
|
159
121
|
"ping",
|
160
|
-
|
161
|
-
|
162
|
-
|
122
|
+
"test",
|
123
|
+
}:
|
124
|
+
greeting = "Hello! How can I help you today?"
|
163
125
|
if output_format == "audio":
|
164
126
|
async for chunk in self.agent_service.llm_provider.tts(
|
165
|
-
text=
|
127
|
+
text=greeting,
|
166
128
|
voice=audio_voice,
|
167
129
|
response_format=audio_output_format,
|
168
130
|
instructions=audio_instructions,
|
169
131
|
):
|
170
132
|
yield chunk
|
171
133
|
else:
|
172
|
-
yield
|
173
|
-
|
174
|
-
# Store simple interaction in memory (using processed user_text)
|
134
|
+
yield greeting
|
175
135
|
if self.memory_provider:
|
176
|
-
await self._store_conversation(user_id,
|
136
|
+
await self._store_conversation(user_id, original_text, greeting)
|
177
137
|
return
|
178
138
|
|
179
|
-
#
|
139
|
+
# 4) Memory context (conversation history)
|
180
140
|
memory_context = ""
|
181
141
|
if self.memory_provider:
|
182
142
|
try:
|
183
143
|
memory_context = await self.memory_provider.retrieve(user_id)
|
184
|
-
|
185
|
-
|
186
|
-
)
|
187
|
-
except Exception as e:
|
188
|
-
logger.error(f"Error retrieving memory context: {e}", exc_info=True)
|
144
|
+
except Exception:
|
145
|
+
memory_context = ""
|
189
146
|
|
190
|
-
#
|
147
|
+
# 5) Knowledge base context
|
191
148
|
kb_context = ""
|
192
149
|
if self.knowledge_base:
|
193
150
|
try:
|
194
|
-
# Use processed user_text for KB query
|
195
151
|
kb_results = await self.knowledge_base.query(
|
196
152
|
query_text=user_text,
|
197
153
|
top_k=self.kb_results_count,
|
198
154
|
include_content=True,
|
199
|
-
include_metadata=False,
|
155
|
+
include_metadata=False,
|
200
156
|
)
|
201
|
-
|
202
157
|
if kb_results:
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
158
|
+
kb_lines = [
|
159
|
+
"**KNOWLEDGE BASE (CRITICAL: MAKE THIS INFORMATION THE TOP PRIORITY):**"
|
160
|
+
]
|
161
|
+
for i, r in enumerate(kb_results, 1):
|
162
|
+
kb_lines.append(f"[{i}] {r.get('content', '').strip()}\n")
|
163
|
+
kb_context = "\n".join(kb_lines)
|
164
|
+
except Exception:
|
165
|
+
kb_context = ""
|
166
|
+
|
167
|
+
# 6) Route query (and fetch previous assistant message)
|
168
|
+
agent_name = "default"
|
169
|
+
prev_assistant = ""
|
170
|
+
routing_input = user_text
|
171
|
+
if self.memory_provider:
|
172
|
+
try:
|
173
|
+
prev_docs = self.memory_provider.find(
|
174
|
+
collection="conversations",
|
175
|
+
query={"user_id": user_id},
|
176
|
+
sort=[("timestamp", -1)],
|
177
|
+
limit=1,
|
178
|
+
)
|
179
|
+
if prev_docs:
|
180
|
+
prev_user_msg = (prev_docs[0] or {}).get(
|
181
|
+
"user_message", ""
|
182
|
+
) or ""
|
183
|
+
prev_assistant = (prev_docs[0] or {}).get(
|
184
|
+
"assistant_message", ""
|
185
|
+
) or ""
|
186
|
+
if prev_user_msg:
|
187
|
+
routing_input = (
|
188
|
+
f"previous_user_message: {prev_user_msg}\n"
|
189
|
+
f"current_user_message: {user_text}"
|
190
|
+
)
|
191
|
+
except Exception:
|
192
|
+
pass
|
217
193
|
try:
|
218
|
-
# Use processed user_text for routing (images generally don't affect routing logic here)
|
219
194
|
if router:
|
220
|
-
agent_name = await router.route_query(
|
195
|
+
agent_name = await router.route_query(routing_input)
|
221
196
|
else:
|
222
|
-
agent_name = await self.routing_service.route_query(
|
223
|
-
|
197
|
+
agent_name = await self.routing_service.route_query(routing_input)
|
198
|
+
except Exception:
|
199
|
+
agent_name = "default"
|
200
|
+
|
201
|
+
# 7) Captured data context + incremental save using previous assistant message
|
202
|
+
capture_context = ""
|
203
|
+
# Two completion flags:
|
204
|
+
required_complete = False
|
205
|
+
form_complete = False # required + optional
|
206
|
+
|
207
|
+
# Helpers
|
208
|
+
def _non_empty(v: Any) -> bool:
|
209
|
+
if v is None:
|
210
|
+
return False
|
211
|
+
if isinstance(v, str):
|
212
|
+
s = v.strip().lower()
|
213
|
+
return s not in {"", "null", "none", "n/a", "na", "undefined", "."}
|
214
|
+
if isinstance(v, (list, dict, tuple, set)):
|
215
|
+
return len(v) > 0
|
216
|
+
return True
|
217
|
+
|
218
|
+
def _parse_numbers_list(s: str) -> List[str]:
|
219
|
+
nums = re.findall(r"\b(\d+)\b", s)
|
220
|
+
seen, out = set(), []
|
221
|
+
for n in nums:
|
222
|
+
if n not in seen:
|
223
|
+
seen.add(n)
|
224
|
+
out.append(n)
|
225
|
+
return out
|
226
|
+
|
227
|
+
def _extract_numbered_options(text: str) -> Dict[str, str]:
|
228
|
+
"""Parse previous assistant message for lines like:
|
229
|
+
'1) Foo', '1. Foo', '- 1) Foo', '* 1. Foo' -> {'1': 'Foo'}"""
|
230
|
+
options: Dict[str, str] = {}
|
231
|
+
if not text:
|
232
|
+
return options
|
233
|
+
for raw in text.splitlines():
|
234
|
+
line = raw.strip()
|
235
|
+
if not line:
|
236
|
+
continue
|
237
|
+
m = re.match(r"^(?:[-*]\s*)?(\d+)[\.)]?\s+(.*)$", line)
|
238
|
+
if m:
|
239
|
+
idx, label = m.group(1), m.group(2).strip().rstrip()
|
240
|
+
if len(label) >= 1:
|
241
|
+
options[idx] = label
|
242
|
+
return options
|
243
|
+
|
244
|
+
def _detect_field_from_prev_question(
|
245
|
+
prev_text: str, schema: Optional[Dict[str, Any]]
|
246
|
+
) -> Optional[str]:
|
247
|
+
if not prev_text or not isinstance(schema, dict):
|
248
|
+
return None
|
249
|
+
t = prev_text.lower()
|
250
|
+
patterns = [
|
251
|
+
("ideas", ["which ideas attract you", "ideas"]),
|
252
|
+
("description", ["please describe yourself", "describe yourself"]),
|
253
|
+
("myself", ["tell us about yourself", "about yourself"]),
|
254
|
+
("questions", ["do you have any questions"]),
|
255
|
+
("rating", ["rating", "1 to 5", "how satisfied", "how happy"]),
|
256
|
+
("email", ["email"]),
|
257
|
+
("phone", ["phone"]),
|
258
|
+
("name", ["name"]),
|
259
|
+
("city", ["city"]),
|
260
|
+
("state", ["state"]),
|
261
|
+
]
|
262
|
+
candidates = set((schema.get("properties") or {}).keys())
|
263
|
+
for field, keys in patterns:
|
264
|
+
if field in candidates and any(key in t for key in keys):
|
265
|
+
return field
|
266
|
+
for field in candidates:
|
267
|
+
if field in t:
|
268
|
+
return field
|
269
|
+
return None
|
270
|
+
|
271
|
+
# Resolve active capture from args or agent config
|
272
|
+
active_capture_name = capture_name
|
273
|
+
active_capture_schema = capture_schema
|
274
|
+
if not active_capture_name or not active_capture_schema:
|
275
|
+
try:
|
276
|
+
cap_cfg = self.agent_service.get_agent_capture(agent_name)
|
277
|
+
if cap_cfg:
|
278
|
+
active_capture_name = active_capture_name or cap_cfg.get("name")
|
279
|
+
active_capture_schema = active_capture_schema or cap_cfg.get(
|
280
|
+
"schema"
|
281
|
+
)
|
282
|
+
except Exception:
|
283
|
+
pass
|
284
|
+
|
285
|
+
latest_by_name: Dict[str, Dict[str, Any]] = {}
|
286
|
+
if self.memory_provider:
|
287
|
+
try:
|
288
|
+
docs = self.memory_provider.find(
|
289
|
+
collection="captures",
|
290
|
+
query={"user_id": user_id},
|
291
|
+
sort=[("timestamp", -1)],
|
292
|
+
limit=100,
|
293
|
+
)
|
294
|
+
for d in docs or []:
|
295
|
+
name = (d or {}).get("capture_name")
|
296
|
+
if not name or name in latest_by_name:
|
297
|
+
continue
|
298
|
+
latest_by_name[name] = {
|
299
|
+
"data": (d or {}).get("data", {}) or {},
|
300
|
+
"mode": (d or {}).get("mode", "once"),
|
301
|
+
"agent": (d or {}).get("agent_name"),
|
302
|
+
}
|
303
|
+
except Exception:
|
304
|
+
pass
|
305
|
+
|
306
|
+
# Incremental save: use prev_assistant's numbered list to map numeric reply -> labels
|
307
|
+
incremental: Dict[str, Any] = {}
|
308
|
+
try:
|
309
|
+
if (
|
310
|
+
self.memory_provider
|
311
|
+
and active_capture_name
|
312
|
+
and isinstance(active_capture_schema, dict)
|
313
|
+
):
|
314
|
+
props = (active_capture_schema or {}).get("properties", {})
|
315
|
+
required_fields = list(
|
316
|
+
(active_capture_schema or {}).get("required", []) or []
|
317
|
+
)
|
318
|
+
all_fields = list(props.keys())
|
319
|
+
optional_fields = [
|
320
|
+
f for f in all_fields if f not in set(required_fields)
|
321
|
+
]
|
322
|
+
|
323
|
+
active_data_existing = (
|
324
|
+
latest_by_name.get(active_capture_name, {}) or {}
|
325
|
+
).get("data", {}) or {}
|
326
|
+
|
327
|
+
def _missing(fields: List[str]) -> List[str]:
|
328
|
+
return [
|
329
|
+
f
|
330
|
+
for f in fields
|
331
|
+
if not _non_empty(active_data_existing.get(f))
|
332
|
+
]
|
333
|
+
|
334
|
+
missing_required = _missing(required_fields)
|
335
|
+
missing_optional = _missing(optional_fields)
|
336
|
+
|
337
|
+
target_field: Optional[str] = _detect_field_from_prev_question(
|
338
|
+
prev_assistant, active_capture_schema
|
339
|
+
)
|
340
|
+
if not target_field:
|
341
|
+
# If exactly one required missing, target it; else if none required missing and exactly one optional missing, target it.
|
342
|
+
if len(missing_required) == 1:
|
343
|
+
target_field = missing_required[0]
|
344
|
+
elif len(missing_required) == 0 and len(missing_optional) == 1:
|
345
|
+
target_field = missing_optional[0]
|
346
|
+
|
347
|
+
if target_field and target_field in props:
|
348
|
+
f_schema = props.get(target_field, {}) or {}
|
349
|
+
f_type = f_schema.get("type")
|
350
|
+
number_to_label = _extract_numbered_options(prev_assistant)
|
351
|
+
|
352
|
+
if number_to_label:
|
353
|
+
nums = _parse_numbers_list(user_text)
|
354
|
+
labels = [
|
355
|
+
number_to_label[n] for n in nums if n in number_to_label
|
356
|
+
]
|
357
|
+
if labels:
|
358
|
+
if f_type == "array":
|
359
|
+
incremental[target_field] = labels
|
360
|
+
else:
|
361
|
+
incremental[target_field] = labels[0]
|
362
|
+
|
363
|
+
if target_field not in incremental:
|
364
|
+
if f_type == "number":
|
365
|
+
m = re.search(r"\b([0-9]+(?:\.[0-9]+)?)\b", user_text)
|
366
|
+
if m:
|
367
|
+
try:
|
368
|
+
incremental[target_field] = float(m.group(1))
|
369
|
+
except Exception:
|
370
|
+
pass
|
371
|
+
elif f_type == "array":
|
372
|
+
parts = [
|
373
|
+
p.strip()
|
374
|
+
for p in re.split(r"[,\n;]+", user_text)
|
375
|
+
if p.strip()
|
376
|
+
]
|
377
|
+
if parts:
|
378
|
+
incremental[target_field] = parts
|
379
|
+
else:
|
380
|
+
if user_text.strip():
|
381
|
+
incremental[target_field] = user_text.strip()
|
382
|
+
|
383
|
+
if incremental:
|
384
|
+
cleaned = {
|
385
|
+
k: v for k, v in incremental.items() if _non_empty(v)
|
386
|
+
}
|
387
|
+
if cleaned:
|
388
|
+
try:
|
389
|
+
await self.memory_provider.save_capture(
|
390
|
+
user_id=user_id,
|
391
|
+
capture_name=active_capture_name,
|
392
|
+
agent_name=agent_name,
|
393
|
+
data=cleaned,
|
394
|
+
schema=active_capture_schema,
|
395
|
+
)
|
396
|
+
except Exception as se:
|
397
|
+
logger.error(f"Error saving incremental capture: {se}")
|
398
|
+
|
224
399
|
except Exception as e:
|
225
|
-
logger.
|
226
|
-
|
227
|
-
|
400
|
+
logger.debug(f"Incremental extraction skipped: {e}")
|
401
|
+
|
402
|
+
# Build capture context, merging in incremental immediately (avoid read lag)
|
403
|
+
def _get_active_data(name: Optional[str]) -> Dict[str, Any]:
|
404
|
+
if not name:
|
405
|
+
return {}
|
406
|
+
base = (latest_by_name.get(name, {}) or {}).get("data", {}) or {}
|
407
|
+
if incremental:
|
408
|
+
base = {**base, **incremental}
|
409
|
+
return base
|
410
|
+
|
411
|
+
lines: List[str] = []
|
412
|
+
if active_capture_name and isinstance(active_capture_schema, dict):
|
413
|
+
props = (active_capture_schema or {}).get("properties", {})
|
414
|
+
required_fields = list(
|
415
|
+
(active_capture_schema or {}).get("required", []) or []
|
228
416
|
)
|
417
|
+
all_fields = list(props.keys())
|
418
|
+
optional_fields = [
|
419
|
+
f for f in all_fields if f not in set(required_fields)
|
420
|
+
]
|
421
|
+
|
422
|
+
active_data = _get_active_data(active_capture_name)
|
423
|
+
|
424
|
+
def _missing_from(data: Dict[str, Any], fields: List[str]) -> List[str]:
|
425
|
+
return [f for f in fields if not _non_empty(data.get(f))]
|
229
426
|
|
230
|
-
|
427
|
+
missing_required = _missing_from(active_data, required_fields)
|
428
|
+
missing_optional = _missing_from(active_data, optional_fields)
|
429
|
+
|
430
|
+
required_complete = (
|
431
|
+
len(missing_required) == 0 and len(required_fields) > 0
|
432
|
+
)
|
433
|
+
form_complete = required_complete and len(missing_optional) == 0
|
434
|
+
|
435
|
+
lines.append(
|
436
|
+
"CAPTURED FORM STATE (Authoritative; do not re-ask filled values):"
|
437
|
+
)
|
438
|
+
lines.append(f"- form_name: {active_capture_name}")
|
439
|
+
|
440
|
+
if active_data:
|
441
|
+
pairs = [
|
442
|
+
f"{k}: {v}" for k, v in active_data.items() if _non_empty(v)
|
443
|
+
]
|
444
|
+
lines.append(
|
445
|
+
f"- filled_fields: {', '.join(pairs) if pairs else '(none)'}"
|
446
|
+
)
|
447
|
+
else:
|
448
|
+
lines.append("- filled_fields: (none)")
|
449
|
+
|
450
|
+
lines.append(
|
451
|
+
f"- missing_required_fields: {', '.join(missing_required) if missing_required else '(none)'}"
|
452
|
+
)
|
453
|
+
lines.append(
|
454
|
+
f"- missing_optional_fields: {', '.join(missing_optional) if missing_optional else '(none)'}"
|
455
|
+
)
|
456
|
+
lines.append("")
|
457
|
+
|
458
|
+
if latest_by_name:
|
459
|
+
lines.append("OTHER CAPTURED USER DATA (for reference):")
|
460
|
+
for cname, info in latest_by_name.items():
|
461
|
+
if cname == active_capture_name:
|
462
|
+
continue
|
463
|
+
data = info.get("data", {}) or {}
|
464
|
+
if data:
|
465
|
+
pairs = [f"{k}: {v}" for k, v in data.items() if _non_empty(v)]
|
466
|
+
lines.append(
|
467
|
+
f"- {cname}: {', '.join(pairs) if pairs else '(none)'}"
|
468
|
+
)
|
469
|
+
else:
|
470
|
+
lines.append(f"- {cname}: (none)")
|
471
|
+
|
472
|
+
if lines:
|
473
|
+
capture_context = "\n".join(lines) + "\n\n"
|
474
|
+
|
475
|
+
# Merge contexts + flow rules
|
231
476
|
combined_context = ""
|
477
|
+
if capture_context:
|
478
|
+
combined_context += capture_context
|
232
479
|
if memory_context:
|
233
|
-
combined_context += f"CONVERSATION HISTORY (Use for
|
480
|
+
combined_context += f"CONVERSATION HISTORY (Use for continuity; not authoritative for facts):\n{memory_context}\n\n"
|
234
481
|
if kb_context:
|
235
|
-
combined_context +=
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
482
|
+
combined_context += kb_context + "\n"
|
483
|
+
if combined_context:
|
484
|
+
combined_context += (
|
485
|
+
"PRIORITIZATION GUIDE:\n"
|
486
|
+
"- Prefer Captured User Data for user-specific fields.\n"
|
487
|
+
"- Prefer KB/tools for facts.\n"
|
488
|
+
"- History is for tone and continuity.\n\n"
|
489
|
+
"FORM FLOW RULES:\n"
|
490
|
+
"- Ask exactly one field per turn.\n"
|
491
|
+
"- If any required fields are missing, ask the next missing required field.\n"
|
492
|
+
"- If all required fields are filled but optional fields are missing, ask the next missing optional field.\n"
|
493
|
+
"- Do NOT re-ask or verify values present in Captured User Data (auto-saved, authoritative).\n"
|
494
|
+
"- Do NOT provide summaries until no required or optional fields are missing.\n\n"
|
495
|
+
)
|
240
496
|
|
241
|
-
#
|
242
|
-
# Pass the processed user_text and images to the agent service
|
497
|
+
# 8) Generate response
|
243
498
|
if output_format == "audio":
|
244
499
|
async for audio_chunk in self.agent_service.generate_response(
|
245
500
|
agent_name=agent_name,
|
246
501
|
user_id=user_id,
|
247
|
-
query=user_text,
|
502
|
+
query=user_text,
|
248
503
|
images=images,
|
249
504
|
memory_context=combined_context,
|
250
505
|
output_format="audio",
|
@@ -254,20 +509,17 @@ class QueryService(QueryServiceInterface):
|
|
254
509
|
prompt=prompt,
|
255
510
|
):
|
256
511
|
yield audio_chunk
|
257
|
-
|
258
|
-
# Store conversation using processed user_text
|
259
|
-
# Note: Storing images in history is not directly supported by current memory provider interface
|
260
512
|
if self.memory_provider:
|
261
513
|
await self._store_conversation(
|
262
514
|
user_id=user_id,
|
263
|
-
user_message=user_text,
|
515
|
+
user_message=user_text,
|
264
516
|
assistant_message=self.agent_service.last_text_response,
|
265
517
|
)
|
266
518
|
else:
|
267
519
|
full_text_response = ""
|
268
|
-
# If capture_schema is provided, we run a structured output pass first
|
269
520
|
capture_data: Optional[BaseModel] = None
|
270
|
-
|
521
|
+
|
522
|
+
# Resolve agent capture if not provided
|
271
523
|
if not capture_schema or not capture_name:
|
272
524
|
try:
|
273
525
|
cap = self.agent_service.get_agent_capture(agent_name)
|
@@ -277,9 +529,9 @@ class QueryService(QueryServiceInterface):
|
|
277
529
|
except Exception:
|
278
530
|
pass
|
279
531
|
|
280
|
-
|
532
|
+
# Only run final structured output when no required or optional fields are missing
|
533
|
+
if capture_schema and capture_name and form_complete:
|
281
534
|
try:
|
282
|
-
# Build a dynamic Pydantic model from JSON schema
|
283
535
|
DynamicModel = self._build_model_from_json_schema(
|
284
536
|
capture_name, capture_schema
|
285
537
|
)
|
@@ -293,14 +545,13 @@ class QueryService(QueryServiceInterface):
|
|
293
545
|
prompt=(
|
294
546
|
(
|
295
547
|
prompt
|
296
|
-
+ "\n\
|
548
|
+
+ "\n\nUsing the captured user data above, return only the JSON for the requested schema. Do not invent values."
|
297
549
|
)
|
298
550
|
if prompt
|
299
|
-
else "
|
551
|
+
else "Using the captured user data above, return only the JSON for the requested schema. Do not invent values."
|
300
552
|
),
|
301
553
|
output_model=DynamicModel,
|
302
554
|
):
|
303
|
-
# This yields a pydantic model instance
|
304
555
|
capture_data = result # type: ignore
|
305
556
|
break
|
306
557
|
except Exception as e:
|
@@ -309,8 +560,8 @@ class QueryService(QueryServiceInterface):
|
|
309
560
|
async for chunk in self.agent_service.generate_response(
|
310
561
|
agent_name=agent_name,
|
311
562
|
user_id=user_id,
|
312
|
-
query=user_text,
|
313
|
-
images=images,
|
563
|
+
query=user_text,
|
564
|
+
images=images,
|
314
565
|
memory_context=combined_context,
|
315
566
|
output_format="text",
|
316
567
|
prompt=prompt,
|
@@ -320,16 +571,14 @@ class QueryService(QueryServiceInterface):
|
|
320
571
|
if output_model is None:
|
321
572
|
full_text_response += chunk
|
322
573
|
|
323
|
-
# Store conversation using processed user_text
|
324
|
-
# Note: Storing images in history is not directly supported by current memory provider interface
|
325
574
|
if self.memory_provider and full_text_response:
|
326
575
|
await self._store_conversation(
|
327
576
|
user_id=user_id,
|
328
|
-
user_message=user_text,
|
577
|
+
user_message=user_text,
|
329
578
|
assistant_message=full_text_response,
|
330
579
|
)
|
331
580
|
|
332
|
-
#
|
581
|
+
# Save final capture data if the model returned it
|
333
582
|
if (
|
334
583
|
self.memory_provider
|
335
584
|
and capture_schema
|
@@ -337,11 +586,10 @@ class QueryService(QueryServiceInterface):
|
|
337
586
|
and capture_data is not None
|
338
587
|
):
|
339
588
|
try:
|
340
|
-
# pydantic v2: model_dump
|
341
589
|
data_dict = (
|
342
|
-
capture_data.model_dump()
|
590
|
+
capture_data.model_dump()
|
343
591
|
if hasattr(capture_data, "model_dump")
|
344
|
-
else capture_data.dict()
|
592
|
+
else capture_data.dict()
|
345
593
|
)
|
346
594
|
await self.memory_provider.save_capture(
|
347
595
|
user_id=user_id,
|
@@ -371,52 +619,29 @@ class QueryService(QueryServiceInterface):
|
|
371
619
|
yield chunk
|
372
620
|
except Exception as tts_e:
|
373
621
|
logger.error(f"Error during TTS for error message: {tts_e}")
|
374
|
-
# Fallback to yielding text error if TTS fails
|
375
622
|
yield error_msg + f" (TTS Error: {tts_e})"
|
376
623
|
else:
|
377
624
|
yield error_msg
|
378
625
|
|
379
626
|
async def delete_user_history(self, user_id: str) -> None:
|
380
|
-
"""Delete all conversation history for a user.
|
381
|
-
|
382
|
-
Args:
|
383
|
-
user_id: User ID
|
384
|
-
"""
|
627
|
+
"""Delete all conversation history for a user."""
|
385
628
|
if self.memory_provider:
|
386
629
|
try:
|
387
630
|
await self.memory_provider.delete(user_id)
|
388
|
-
logger.info(f"Deleted conversation history for user: {user_id}")
|
389
631
|
except Exception as e:
|
390
|
-
logger.error(
|
391
|
-
f"Error deleting user history for {user_id}: {e}", exc_info=True
|
392
|
-
)
|
632
|
+
logger.error(f"Error deleting user history for {user_id}: {e}")
|
393
633
|
else:
|
394
|
-
logger.
|
395
|
-
"Attempted to delete user history, but no memory provider is configured."
|
396
|
-
)
|
634
|
+
logger.debug("No memory provider; skip delete_user_history")
|
397
635
|
|
398
636
|
async def get_user_history(
|
399
637
|
self,
|
400
638
|
user_id: str,
|
401
639
|
page_num: int = 1,
|
402
640
|
page_size: int = 20,
|
403
|
-
sort_order: str = "desc",
|
641
|
+
sort_order: str = "desc",
|
404
642
|
) -> Dict[str, Any]:
|
405
|
-
"""Get paginated message history for a user.
|
406
|
-
|
407
|
-
Args:
|
408
|
-
user_id: User ID
|
409
|
-
page_num: Page number (starting from 1)
|
410
|
-
page_size: Number of messages per page
|
411
|
-
sort_order: Sort order ("asc" or "desc")
|
412
|
-
|
413
|
-
Returns:
|
414
|
-
Dictionary with paginated results and metadata.
|
415
|
-
"""
|
643
|
+
"""Get paginated message history for a user."""
|
416
644
|
if not self.memory_provider:
|
417
|
-
logger.warning(
|
418
|
-
"Attempted to get user history, but no memory provider is configured."
|
419
|
-
)
|
420
645
|
return {
|
421
646
|
"data": [],
|
422
647
|
"total": 0,
|
@@ -425,20 +650,13 @@ class QueryService(QueryServiceInterface):
|
|
425
650
|
"total_pages": 0,
|
426
651
|
"error": "Memory provider not available",
|
427
652
|
}
|
428
|
-
|
429
653
|
try:
|
430
|
-
# Calculate skip and limit for pagination
|
431
654
|
skip = (page_num - 1) * page_size
|
432
|
-
|
433
|
-
# Get total count of documents
|
434
655
|
total = self.memory_provider.count_documents(
|
435
656
|
collection="conversations", query={"user_id": user_id}
|
436
657
|
)
|
437
|
-
|
438
|
-
# Calculate total pages
|
439
658
|
total_pages = (total + page_size - 1) // page_size if total > 0 else 0
|
440
659
|
|
441
|
-
# Get paginated results
|
442
660
|
conversations = self.memory_provider.find(
|
443
661
|
collection="conversations",
|
444
662
|
query={"user_id": user_id},
|
@@ -447,39 +665,27 @@ class QueryService(QueryServiceInterface):
|
|
447
665
|
limit=page_size,
|
448
666
|
)
|
449
667
|
|
450
|
-
|
451
|
-
formatted_conversations = []
|
668
|
+
formatted: List[Dict[str, Any]] = []
|
452
669
|
for conv in conversations:
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
else None
|
457
|
-
)
|
458
|
-
# Assuming the stored format matches what _store_conversation saves
|
459
|
-
# (which currently only stores text messages)
|
460
|
-
formatted_conversations.append(
|
670
|
+
ts = conv.get("timestamp")
|
671
|
+
ts_epoch = int(ts.timestamp()) if ts else None
|
672
|
+
formatted.append(
|
461
673
|
{
|
462
674
|
"id": str(conv.get("_id")),
|
463
|
-
"user_message": conv.get("user_message"),
|
464
|
-
"assistant_message": conv.get(
|
465
|
-
|
466
|
-
), # Or how it's stored
|
467
|
-
"timestamp": timestamp,
|
675
|
+
"user_message": conv.get("user_message"),
|
676
|
+
"assistant_message": conv.get("assistant_message"),
|
677
|
+
"timestamp": ts_epoch,
|
468
678
|
}
|
469
679
|
)
|
470
680
|
|
471
|
-
logger.info(
|
472
|
-
f"Retrieved page {page_num}/{total_pages} of history for user {user_id}"
|
473
|
-
)
|
474
681
|
return {
|
475
|
-
"data":
|
682
|
+
"data": formatted,
|
476
683
|
"total": total,
|
477
684
|
"page": page_num,
|
478
685
|
"page_size": page_size,
|
479
686
|
"total_pages": total_pages,
|
480
687
|
"error": None,
|
481
688
|
}
|
482
|
-
|
483
689
|
except Exception as e:
|
484
690
|
import traceback
|
485
691
|
|
@@ -498,48 +704,29 @@ class QueryService(QueryServiceInterface):
|
|
498
704
|
async def _store_conversation(
|
499
705
|
self, user_id: str, user_message: str, assistant_message: str
|
500
706
|
) -> None:
|
501
|
-
"""Store conversation history in memory provider.
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
# doesn't explicitly handle image data storage in history.
|
512
|
-
await self.memory_provider.store(
|
513
|
-
user_id,
|
514
|
-
[
|
515
|
-
{"role": "user", "content": user_message},
|
516
|
-
{"role": "assistant", "content": assistant_message},
|
517
|
-
],
|
518
|
-
)
|
519
|
-
logger.info(f"Stored conversation for user {user_id}")
|
520
|
-
except Exception as e:
|
521
|
-
logger.error(
|
522
|
-
f"Error storing conversation for user {user_id}: {e}", exc_info=True
|
523
|
-
)
|
524
|
-
else:
|
525
|
-
logger.debug(
|
526
|
-
"Memory provider not configured, skipping conversation storage."
|
707
|
+
"""Store conversation history in memory provider."""
|
708
|
+
if not self.memory_provider:
|
709
|
+
return
|
710
|
+
try:
|
711
|
+
await self.memory_provider.store(
|
712
|
+
user_id,
|
713
|
+
[
|
714
|
+
{"role": "user", "content": user_message},
|
715
|
+
{"role": "assistant", "content": assistant_message},
|
716
|
+
],
|
527
717
|
)
|
718
|
+
except Exception as e:
|
719
|
+
logger.error(f"Store conversation error for {user_id}: {e}")
|
528
720
|
|
529
721
|
def _build_model_from_json_schema(
|
530
722
|
self, name: str, schema: Dict[str, Any]
|
531
723
|
) -> Type[BaseModel]:
|
532
|
-
"""Create a Pydantic model dynamically from a JSON Schema subset.
|
533
|
-
|
534
|
-
Supports 'type' string, integer, number, boolean, object (flat), array (of simple types),
|
535
|
-
required fields, and default values. Nested objects/arrays can be extended later.
|
536
|
-
"""
|
724
|
+
"""Create a Pydantic model dynamically from a JSON Schema subset."""
|
537
725
|
from pydantic import create_model
|
538
726
|
|
539
727
|
def py_type(js: Dict[str, Any]):
|
540
728
|
t = js.get("type")
|
541
729
|
if isinstance(t, list):
|
542
|
-
# handle ["null", "string"] => Optional[str]
|
543
730
|
non_null = [x for x in t if x != "null"]
|
544
731
|
if not non_null:
|
545
732
|
return Optional[Any]
|
@@ -557,13 +744,12 @@ class QueryService(QueryServiceInterface):
|
|
557
744
|
items = js.get("items", {"type": "string"})
|
558
745
|
return List[py_type(items)]
|
559
746
|
if t == "object":
|
560
|
-
# For now, represent as Dict[str, Any]
|
561
747
|
return Dict[str, Any]
|
562
748
|
return Any
|
563
749
|
|
564
750
|
properties: Dict[str, Any] = schema.get("properties", {})
|
565
751
|
required = set(schema.get("required", []))
|
566
|
-
fields = {}
|
752
|
+
fields: Dict[str, Any] = {}
|
567
753
|
for field_name, field_schema in properties.items():
|
568
754
|
typ = py_type(field_schema)
|
569
755
|
default = field_schema.get("default")
|
@@ -572,5 +758,5 @@ class QueryService(QueryServiceInterface):
|
|
572
758
|
else:
|
573
759
|
fields[field_name] = (typ, default)
|
574
760
|
|
575
|
-
Model = create_model(name, **fields)
|
761
|
+
Model = create_model(name, **fields)
|
576
762
|
return Model
|
solana_agent/services/routing.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
"""
|
2
2
|
Routing service implementation.
|
3
3
|
|
4
|
-
This service manages query routing to appropriate agents based
|
5
|
-
|
4
|
+
This service manages query routing to appropriate agents using an LLM-based
|
5
|
+
analysis. It defaults to a small, low-cost model for routing to minimize
|
6
|
+
overhead while maintaining quality.
|
6
7
|
"""
|
7
8
|
|
8
9
|
import logging
|
@@ -28,7 +29,7 @@ class RoutingService(RoutingServiceInterface):
|
|
28
29
|
api_key: Optional[str] = None,
|
29
30
|
base_url: Optional[str] = None,
|
30
31
|
model: Optional[str] = None,
|
31
|
-
):
|
32
|
+
) -> None:
|
32
33
|
"""Initialize the routing service.
|
33
34
|
|
34
35
|
Args:
|
@@ -39,7 +40,10 @@ class RoutingService(RoutingServiceInterface):
|
|
39
40
|
self.agent_service = agent_service
|
40
41
|
self.api_key = api_key
|
41
42
|
self.base_url = base_url
|
42
|
-
|
43
|
+
# Use a small, cheap model for routing unless explicitly provided
|
44
|
+
self.model = model or "gpt-4.1-mini"
|
45
|
+
# Simple sticky session: remember last routed agent in-process
|
46
|
+
self._last_agent = None
|
43
47
|
|
44
48
|
async def _analyze_query(self, query: str) -> Dict[str, Any]:
|
45
49
|
"""Analyze a query to determine routing information.
|
@@ -82,9 +86,6 @@ class RoutingService(RoutingServiceInterface):
|
|
82
86
|
2. Any secondary agents that might be helpful (must be from the listed agents)
|
83
87
|
3. The complexity level (1-5, where 5 is most complex)
|
84
88
|
4. Any key topics or technologies mentioned
|
85
|
-
|
86
|
-
Think carefully about whether the query is more technical/development-focused or more
|
87
|
-
financial/market-focused to match with the appropriate agent.
|
88
89
|
"""
|
89
90
|
|
90
91
|
try:
|
@@ -131,18 +132,23 @@ class RoutingService(RoutingServiceInterface):
|
|
131
132
|
if len(agents) == 1:
|
132
133
|
agent_name = next(iter(agents.keys()))
|
133
134
|
logger.info(f"Only one agent available: {agent_name}") # Use logger.info
|
135
|
+
self._last_agent = agent_name
|
134
136
|
return agent_name
|
135
137
|
|
136
|
-
#
|
137
|
-
|
138
|
+
# Short reply bypass and default stickiness
|
139
|
+
short = query.strip().lower()
|
140
|
+
short_replies = {"", "yes", "no", "ok", "k", "y", "n", "1", "0"}
|
141
|
+
if short in short_replies and self._last_agent:
|
142
|
+
return self._last_agent
|
138
143
|
|
139
|
-
#
|
144
|
+
# Always analyze with a small model to select the best agent
|
145
|
+
analysis = await self._analyze_query(query)
|
140
146
|
best_agent = await self._find_best_ai_agent(
|
141
147
|
analysis["primary_specialization"], analysis["secondary_specializations"]
|
142
148
|
)
|
143
|
-
|
144
|
-
|
145
|
-
return
|
149
|
+
chosen = best_agent or next(iter(agents.keys()))
|
150
|
+
self._last_agent = chosen
|
151
|
+
return chosen
|
146
152
|
|
147
153
|
async def _find_best_ai_agent(
|
148
154
|
self,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: solana-agent
|
3
|
-
Version: 31.1.
|
3
|
+
Version: 31.1.2
|
4
4
|
Summary: AI Agents for Solana
|
5
5
|
License: MIT
|
6
6
|
Keywords: solana,solana ai,solana agent,ai,ai agent,ai agents
|
@@ -14,11 +14,11 @@ Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
16
16
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
17
|
-
Requires-Dist: instructor (==1.
|
17
|
+
Requires-Dist: instructor (==1.11.2)
|
18
18
|
Requires-Dist: llama-index-core (==0.13.3)
|
19
19
|
Requires-Dist: llama-index-embeddings-openai (==0.5.0)
|
20
|
-
Requires-Dist: logfire (==4.3.
|
21
|
-
Requires-Dist: openai (==1.
|
20
|
+
Requires-Dist: logfire (==4.3.6)
|
21
|
+
Requires-Dist: openai (==1.102.0)
|
22
22
|
Requires-Dist: pillow (==11.3.0)
|
23
23
|
Requires-Dist: pinecone[asyncio] (==7.3.0)
|
24
24
|
Requires-Dist: pydantic (>=2)
|
@@ -26,8 +26,8 @@ Requires-Dist: pymongo (==4.14.1)
|
|
26
26
|
Requires-Dist: pypdf (==6.0.0)
|
27
27
|
Requires-Dist: rich (>=13,<14.0)
|
28
28
|
Requires-Dist: scrubadub (==2.0.1)
|
29
|
-
Requires-Dist: typer (==0.
|
30
|
-
Requires-Dist: zep-cloud (==3.4.
|
29
|
+
Requires-Dist: typer (==0.17.3)
|
30
|
+
Requires-Dist: zep-cloud (==3.4.3)
|
31
31
|
Project-URL: Documentation, https://docs.solana-agent.com
|
32
32
|
Project-URL: Homepage, https://solana-agent.com
|
33
33
|
Project-URL: Repository, https://github.com/truemagic-coder/solana-agent
|
@@ -10,7 +10,7 @@ solana_agent/domains/__init__.py,sha256=HiC94wVPRy-QDJSSRywCRrhrFfTBeHjfi5z-QfZv
|
|
10
10
|
solana_agent/domains/agent.py,sha256=8pAi1-kIgzFNANt3dyQjw-1zbThcNdpEllbAGWi79uI,2841
|
11
11
|
solana_agent/domains/routing.py,sha256=1yR4IswGcmREGgbOOI6TKCfuM7gYGOhQjLkBqnZ-rNo,582
|
12
12
|
solana_agent/factories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
-
solana_agent/factories/agent_factory.py,sha256=
|
13
|
+
solana_agent/factories/agent_factory.py,sha256=i20C57hxF7juWuYy9lru28OwCC-xD4g5UhzmQtPe474,14671
|
14
14
|
solana_agent/guardrails/pii.py,sha256=FCz1IC3mmkr41QFFf5NaC0fwJrVkwFsxgyOCS2POO5I,4428
|
15
15
|
solana_agent/interfaces/__init__.py,sha256=IQs1WIM1FeKP1-kY2FEfyhol_dB-I-VAe2rD6jrVF6k,355
|
16
16
|
solana_agent/interfaces/client/client.py,sha256=9hg35-hp_CI-WVGOXehBE1ZCKYahLmbeAvtQOYmML4o,3245
|
@@ -34,10 +34,10 @@ solana_agent/repositories/memory.py,sha256=F46vZ-Uhj7PX2uFGCRKYsZ8JLmKteMN1d30qG
|
|
34
34
|
solana_agent/services/__init__.py,sha256=iko0c2MlF8b_SA_nuBGFllr2E3g_JowOrOzGcnU9tkA,162
|
35
35
|
solana_agent/services/agent.py,sha256=dotuINMtW3TQDLq2eNM5r1cAUwhzxbHBotw8p5CLsYU,20983
|
36
36
|
solana_agent/services/knowledge_base.py,sha256=ZvOPrSmcNDgUzz4bJIQ4LeRl9vMZiK9hOfs71IpB7Bk,32735
|
37
|
-
solana_agent/services/query.py,sha256=
|
38
|
-
solana_agent/services/routing.py,sha256=
|
39
|
-
solana_agent-31.1.
|
40
|
-
solana_agent-31.1.
|
41
|
-
solana_agent-31.1.
|
42
|
-
solana_agent-31.1.
|
43
|
-
solana_agent-31.1.
|
37
|
+
solana_agent/services/query.py,sha256=bmam8rvpWFEGg1uVABOqd1X2GRqRNwBu7JCCzZ93iIE,32328
|
38
|
+
solana_agent/services/routing.py,sha256=hsHe8HSGO_xFc0A17WIOGTidLTfLSfApQw3l2HHqkLo,7614
|
39
|
+
solana_agent-31.1.2.dist-info/LICENSE,sha256=BnSRc-NSFuyF2s496l_4EyrwAP6YimvxWcjPiJ0J7g4,1057
|
40
|
+
solana_agent-31.1.2.dist-info/METADATA,sha256=ic6JDKmY4bhO78QgX7sjtVV-BVuuxg2KR4sI58hSWP4,30013
|
41
|
+
solana_agent-31.1.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
42
|
+
solana_agent-31.1.2.dist-info/entry_points.txt,sha256=-AuT_mfqk8dlZ0pHuAjx1ouAWpTRjpqvEUa6YV3lmc0,53
|
43
|
+
solana_agent-31.1.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|