solana-agent 29.2.0__py3-none-any.whl → 29.2.1__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/adapters/openai_adapter.py +12 -10
- solana_agent/interfaces/providers/llm.py +5 -1
- solana_agent/services/agent.py +72 -561
- {solana_agent-29.2.0.dist-info → solana_agent-29.2.1.dist-info}/METADATA +1 -3
- {solana_agent-29.2.0.dist-info → solana_agent-29.2.1.dist-info}/RECORD +8 -8
- {solana_agent-29.2.0.dist-info → solana_agent-29.2.1.dist-info}/LICENSE +0 -0
- {solana_agent-29.2.0.dist-info → solana_agent-29.2.1.dist-info}/WHEEL +0 -0
- {solana_agent-29.2.0.dist-info → solana_agent-29.2.1.dist-info}/entry_points.txt +0 -0
@@ -163,8 +163,10 @@ class OpenAIAdapter(LLMProvider):
|
|
163
163
|
api_key: Optional[str] = None,
|
164
164
|
base_url: Optional[str] = None,
|
165
165
|
model: Optional[str] = None,
|
166
|
-
|
167
|
-
|
166
|
+
functions: Optional[List[Dict[str, Any]]] = None,
|
167
|
+
function_call: Optional[Union[str, Dict[str, Any]]] = None,
|
168
|
+
) -> Any: # pragma: no cover
|
169
|
+
"""Generate text or function call from OpenAI models."""
|
168
170
|
messages = []
|
169
171
|
if system_prompt:
|
170
172
|
messages.append({"role": "system", "content": system_prompt})
|
@@ -174,6 +176,10 @@ class OpenAIAdapter(LLMProvider):
|
|
174
176
|
"messages": messages,
|
175
177
|
"model": model or self.text_model,
|
176
178
|
}
|
179
|
+
if functions:
|
180
|
+
request_params["functions"] = functions
|
181
|
+
if function_call:
|
182
|
+
request_params["function_call"] = function_call
|
177
183
|
|
178
184
|
if api_key and base_url:
|
179
185
|
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
@@ -185,17 +191,13 @@ class OpenAIAdapter(LLMProvider):
|
|
185
191
|
|
186
192
|
try:
|
187
193
|
response = await client.chat.completions.create(**request_params)
|
188
|
-
|
189
|
-
|
190
|
-
else:
|
191
|
-
logger.warning("Received non-streaming response with no content.")
|
192
|
-
return ""
|
193
|
-
except OpenAIError as e: # Catch specific OpenAI errors
|
194
|
+
return response
|
195
|
+
except OpenAIError as e:
|
194
196
|
logger.error(f"OpenAI API error during text generation: {e}")
|
195
|
-
return
|
197
|
+
return None
|
196
198
|
except Exception as e:
|
197
199
|
logger.exception(f"Error in generate_text: {e}")
|
198
|
-
return
|
200
|
+
return None
|
199
201
|
|
200
202
|
def _calculate_gpt41_image_cost(self, width: int, height: int, model: str) -> int:
|
201
203
|
"""Calculates the token cost for an image with GPT-4.1 models."""
|
@@ -1,6 +1,8 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
2
|
from typing import (
|
3
|
+
Any,
|
3
4
|
AsyncGenerator,
|
5
|
+
Dict,
|
4
6
|
List,
|
5
7
|
Literal,
|
6
8
|
Optional,
|
@@ -26,7 +28,9 @@ class LLMProvider(ABC):
|
|
26
28
|
api_key: Optional[str] = None,
|
27
29
|
base_url: Optional[str] = None,
|
28
30
|
model: Optional[str] = None,
|
29
|
-
|
31
|
+
functions: Optional[List[Dict[str, Any]]] = None,
|
32
|
+
function_call: Optional[Union[str, Dict[str, Any]]] = None,
|
33
|
+
) -> Any:
|
30
34
|
"""Generate text from the language model."""
|
31
35
|
pass
|
32
36
|
|
solana_agent/services/agent.py
CHANGED
@@ -5,7 +5,6 @@ This service manages AI and human agents, their registration, tool assignments,
|
|
5
5
|
and response generation.
|
6
6
|
"""
|
7
7
|
|
8
|
-
import asyncio
|
9
8
|
import datetime as main_datetime
|
10
9
|
from datetime import datetime
|
11
10
|
import json
|
@@ -205,137 +204,6 @@ class AgentService(AgentServiceInterface):
|
|
205
204
|
)
|
206
205
|
return {"status": "error", "message": f"Error executing tool: {str(e)}"}
|
207
206
|
|
208
|
-
# --- Helper function to recursively substitute placeholders ---
|
209
|
-
def _substitute_placeholders(self, data: Any, results_map: Dict[str, str]) -> Any:
|
210
|
-
"""Recursively substitutes placeholders like {{tool_name.result}} or {output_of_tool_name} in strings."""
|
211
|
-
if isinstance(data, str):
|
212
|
-
# Regex to find placeholders like {{tool_name.result}} or {output_of_tool_name}
|
213
|
-
placeholder_pattern = re.compile(
|
214
|
-
r"\{\{(?P<name1>[a-zA-Z0-9_]+)\.result\}\}|\{output_of_(?P<name2>[a-zA-Z0-9_]+)\}"
|
215
|
-
)
|
216
|
-
|
217
|
-
def replace_match(match):
|
218
|
-
tool_name = match.group("name1") or match.group("name2")
|
219
|
-
if tool_name and tool_name in results_map:
|
220
|
-
logger.debug(f"Substituting placeholder for '{tool_name}'")
|
221
|
-
return results_map[tool_name]
|
222
|
-
else:
|
223
|
-
# If placeholder not found, leave it as is but log warning
|
224
|
-
logger.warning(
|
225
|
-
f"Could not find result for placeholder tool '{tool_name}'. Leaving placeholder."
|
226
|
-
)
|
227
|
-
return match.group(0) # Return original placeholder
|
228
|
-
|
229
|
-
# Use re.sub with the replacement function
|
230
|
-
return placeholder_pattern.sub(replace_match, data)
|
231
|
-
elif isinstance(data, dict):
|
232
|
-
# Recursively process dictionary values
|
233
|
-
return {
|
234
|
-
k: self._substitute_placeholders(v, results_map)
|
235
|
-
for k, v in data.items()
|
236
|
-
}
|
237
|
-
elif isinstance(data, list):
|
238
|
-
# Recursively process list items
|
239
|
-
return [self._substitute_placeholders(item, results_map) for item in data]
|
240
|
-
else:
|
241
|
-
# Return non-string/dict/list types as is
|
242
|
-
return data
|
243
|
-
|
244
|
-
# --- Helper to parse tool calls ---
|
245
|
-
def _parse_tool_calls(self, text: str) -> List[Dict[str, Any]]:
|
246
|
-
"""Parses all [TOOL]...[/TOOL] blocks in the text."""
|
247
|
-
tool_calls = []
|
248
|
-
# Regex to find all tool blocks, non-greedy match for content
|
249
|
-
pattern = re.compile(r"\[TOOL\](.*?)\[/TOOL\]", re.DOTALL | re.IGNORECASE)
|
250
|
-
matches = pattern.finditer(text)
|
251
|
-
|
252
|
-
for match in matches:
|
253
|
-
tool_content = match.group(1).strip()
|
254
|
-
tool_name = None
|
255
|
-
parameters = {}
|
256
|
-
try:
|
257
|
-
for line in tool_content.split("\n"):
|
258
|
-
line = line.strip()
|
259
|
-
if not line:
|
260
|
-
continue
|
261
|
-
if line.lower().startswith("name:"):
|
262
|
-
tool_name = line[5:].strip()
|
263
|
-
elif line.lower().startswith("parameters:"):
|
264
|
-
params_text = line[11:].strip()
|
265
|
-
try:
|
266
|
-
# Prefer JSON parsing
|
267
|
-
parameters = json.loads(params_text)
|
268
|
-
except json.JSONDecodeError:
|
269
|
-
logger.warning(
|
270
|
-
f"Failed to parse parameters as JSON, falling back: {params_text}"
|
271
|
-
)
|
272
|
-
# Fallback: Treat as simple key=value (less robust)
|
273
|
-
try:
|
274
|
-
# Basic eval might work for {"key": "value"} but is risky
|
275
|
-
# parameters = eval(params_text) # Avoid eval if possible
|
276
|
-
# Safer fallback: Assume simple string if not JSON-like
|
277
|
-
if not params_text.startswith("{"):
|
278
|
-
# Try splitting key=value pairs? Very brittle.
|
279
|
-
# For now, log warning and skip complex fallback parsing
|
280
|
-
logger.error(
|
281
|
-
f"Cannot parse non-JSON parameters reliably: {params_text}"
|
282
|
-
)
|
283
|
-
parameters = {
|
284
|
-
"_raw_params": params_text
|
285
|
-
} # Store raw string
|
286
|
-
else:
|
287
|
-
# If it looks like a dict but isn't valid JSON, log error
|
288
|
-
logger.error(
|
289
|
-
f"Invalid dictionary format for parameters: {params_text}"
|
290
|
-
)
|
291
|
-
parameters = {"_raw_params": params_text}
|
292
|
-
|
293
|
-
except Exception as parse_err:
|
294
|
-
logger.error(
|
295
|
-
f"Fallback parameter parsing failed: {parse_err}"
|
296
|
-
)
|
297
|
-
parameters = {
|
298
|
-
"_raw_params": params_text
|
299
|
-
} # Store raw string on error
|
300
|
-
|
301
|
-
if tool_name:
|
302
|
-
tool_calls.append({"name": tool_name, "parameters": parameters})
|
303
|
-
else:
|
304
|
-
logger.warning(f"Parsed tool block missing name: {tool_content}")
|
305
|
-
except Exception as e:
|
306
|
-
logger.error(f"Error parsing tool content: {tool_content} - {e}")
|
307
|
-
|
308
|
-
logger.info(f"Parsed {len(tool_calls)} tool calls from response.")
|
309
|
-
return tool_calls
|
310
|
-
|
311
|
-
# --- Helper to execute a single parsed tool call ---
|
312
|
-
async def _execute_single_tool(
|
313
|
-
self, agent_name: str, tool_call: Dict[str, Any]
|
314
|
-
) -> Dict[str, Any]:
|
315
|
-
"""Executes a single tool call dictionary and returns its result."""
|
316
|
-
tool_name = tool_call.get("name")
|
317
|
-
parameters = tool_call.get("parameters", {})
|
318
|
-
if not tool_name:
|
319
|
-
return {
|
320
|
-
"tool_name": "unknown",
|
321
|
-
"status": "error",
|
322
|
-
"message": "Tool name missing in parsed call",
|
323
|
-
}
|
324
|
-
# Ensure parameters is a dict, even if parsing failed
|
325
|
-
if not isinstance(parameters, dict):
|
326
|
-
logger.warning(
|
327
|
-
f"Parameters for tool '{tool_name}' is not a dict: {parameters}. Attempting execution with empty params."
|
328
|
-
)
|
329
|
-
parameters = {}
|
330
|
-
|
331
|
-
logger.debug(
|
332
|
-
f"Preparing to execute tool '{tool_name}' with params: {parameters}"
|
333
|
-
)
|
334
|
-
result = await self.execute_tool(agent_name, tool_name, parameters)
|
335
|
-
# Add tool name to result for easier aggregation
|
336
|
-
result["tool_name"] = tool_name
|
337
|
-
return result
|
338
|
-
|
339
207
|
async def generate_response(
|
340
208
|
self,
|
341
209
|
agent_name: str,
|
@@ -362,9 +230,8 @@ class AgentService(AgentServiceInterface):
|
|
362
230
|
] = "aac",
|
363
231
|
prompt: Optional[str] = None,
|
364
232
|
) -> AsyncGenerator[Union[str, bytes], None]: # pragma: no cover
|
365
|
-
"""Generate a response
|
366
|
-
|
367
|
-
"""
|
233
|
+
"""Generate a response using OpenAI function calling (tools API) via generate_text."""
|
234
|
+
|
368
235
|
agent = next((a for a in self.agents if a.name == agent_name), None)
|
369
236
|
if not agent:
|
370
237
|
error_msg = f"Agent '{agent_name}' not found."
|
@@ -381,268 +248,88 @@ class AgentService(AgentServiceInterface):
|
|
381
248
|
yield error_msg
|
382
249
|
return
|
383
250
|
|
384
|
-
|
385
|
-
|
386
|
-
)
|
251
|
+
# Build system prompt and messages
|
252
|
+
system_prompt = self.get_agent_system_prompt(agent_name)
|
253
|
+
user_content = str(query)
|
254
|
+
if images:
|
255
|
+
user_content += (
|
256
|
+
"\n\n[Images attached]" # Optionally, handle images as needed
|
257
|
+
)
|
258
|
+
|
259
|
+
# Compose the prompt for generate_text
|
260
|
+
full_prompt = ""
|
261
|
+
if memory_context:
|
262
|
+
full_prompt += f"CONVERSATION HISTORY:\n{memory_context}\n\n"
|
263
|
+
if prompt:
|
264
|
+
full_prompt += f"ADDITIONAL PROMPT:\n{prompt}\n\n"
|
265
|
+
full_prompt += user_content
|
266
|
+
full_prompt += f"USER IDENTIFIER: {user_id}"
|
267
|
+
|
268
|
+
# Get OpenAI function schemas for this agent's tools
|
269
|
+
functions = []
|
270
|
+
for tool in self.get_agent_tools(agent_name):
|
271
|
+
functions.append(
|
272
|
+
{
|
273
|
+
"name": tool["name"],
|
274
|
+
"description": tool.get("description", ""),
|
275
|
+
"parameters": tool.get("parameters", {}),
|
276
|
+
}
|
277
|
+
)
|
387
278
|
|
279
|
+
response_text = ""
|
388
280
|
try:
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
if memory_context:
|
396
|
-
system_prompt_parts.append(f"\nCONVERSATION HISTORY:\n{memory_context}")
|
397
|
-
if prompt:
|
398
|
-
system_prompt_parts.append(f"\nADDITIONAL PROMPT:\n{prompt}")
|
399
|
-
final_system_prompt = "\n\n".join(filter(None, system_prompt_parts))
|
400
|
-
|
401
|
-
# --- Initial Response Generation (No Streaming) ---
|
402
|
-
initial_llm_response_buffer = ""
|
403
|
-
tool_calls_detected = False
|
404
|
-
start_marker = "[TOOL]"
|
405
|
-
|
406
|
-
logger.info(f"Generating initial response for agent '{agent_name}'...")
|
407
|
-
|
408
|
-
# --- CHOOSE LLM METHOD BASED ON IMAGE PRESENCE ---
|
409
|
-
if images:
|
410
|
-
# Use the new vision method if images are present
|
411
|
-
logger.info(
|
412
|
-
f"Using generate_text_with_images for {len(images)} images."
|
413
|
-
)
|
414
|
-
# Ensure query is string for the text part
|
415
|
-
text_query = str(query) if isinstance(query, bytes) else query
|
416
|
-
initial_llm_response_buffer = (
|
417
|
-
await self.llm_provider.generate_text_with_images(
|
418
|
-
prompt=text_query,
|
419
|
-
images=images,
|
420
|
-
system_prompt=final_system_prompt,
|
421
|
-
)
|
422
|
-
)
|
423
|
-
else:
|
424
|
-
# Use the standard text generation method
|
425
|
-
logger.info("Using generate_text (no images provided).")
|
426
|
-
initial_llm_response_buffer = await self.llm_provider.generate_text(
|
427
|
-
prompt=str(query),
|
428
|
-
system_prompt=final_system_prompt,
|
281
|
+
while True:
|
282
|
+
response = await self.llm_provider.generate_text(
|
283
|
+
prompt=full_prompt,
|
284
|
+
system_prompt=system_prompt,
|
285
|
+
functions=functions if functions else None,
|
286
|
+
function_call="auto" if functions else None,
|
429
287
|
api_key=self.api_key,
|
430
288
|
base_url=self.base_url,
|
431
289
|
model=self.model,
|
432
290
|
)
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
"
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
#
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
instructions=audio_instructions,
|
452
|
-
):
|
453
|
-
yield chunk
|
454
|
-
else:
|
455
|
-
yield initial_llm_response_buffer
|
456
|
-
return
|
457
|
-
|
458
|
-
# Check for tool markers in the complete response
|
459
|
-
if start_marker.lower() in initial_llm_response_buffer.lower():
|
460
|
-
tool_calls_detected = True
|
461
|
-
logger.info("Tool call marker detected in initial response.")
|
462
|
-
|
463
|
-
logger.debug(
|
464
|
-
f"Full initial LLM response buffer:\n--- START ---\n{initial_llm_response_buffer}\n--- END ---"
|
465
|
-
)
|
466
|
-
logger.info(
|
467
|
-
f"Initial LLM response received (length: {len(initial_llm_response_buffer)}). Tools detected: {tool_calls_detected}"
|
468
|
-
)
|
469
|
-
|
470
|
-
# --- Tool Execution Phase (if tools were detected) ---
|
471
|
-
final_response_text = ""
|
472
|
-
if tool_calls_detected:
|
473
|
-
# NOTE: If tools need to operate on image content, this logic needs significant changes.
|
474
|
-
# Assuming for now tools operate based on the text query or the LLM's understanding derived from images.
|
475
|
-
parsed_calls = self._parse_tool_calls(initial_llm_response_buffer)
|
476
|
-
|
477
|
-
if parsed_calls:
|
478
|
-
# ... (existing sequential tool execution with substitution) ...
|
479
|
-
executed_tool_results = []
|
480
|
-
tool_results_map: Dict[str, str] = {}
|
291
|
+
if (
|
292
|
+
not response
|
293
|
+
or not hasattr(response, "choices")
|
294
|
+
or not response.choices
|
295
|
+
):
|
296
|
+
logger.error("No response or choices from LLM provider.")
|
297
|
+
response_text = "I apologize, but I could not generate a response."
|
298
|
+
break
|
299
|
+
|
300
|
+
choice = response.choices[0]
|
301
|
+
message = getattr(
|
302
|
+
choice, "message", choice
|
303
|
+
) # Support both OpenAI and instructor
|
304
|
+
|
305
|
+
# If the model wants to call a function/tool
|
306
|
+
if hasattr(message, "function_call") and message.function_call:
|
307
|
+
function_name = message.function_call.name
|
308
|
+
arguments = json.loads(message.function_call.arguments)
|
481
309
|
logger.info(
|
482
|
-
f"
|
483
|
-
)
|
484
|
-
for i, call in enumerate(parsed_calls):
|
485
|
-
# ... (existing substitution logic) ...
|
486
|
-
tool_name_to_exec = call.get("name", "unknown")
|
487
|
-
logger.info(
|
488
|
-
f"Executing tool {i + 1}/{len(parsed_calls)}: {tool_name_to_exec}"
|
489
|
-
)
|
490
|
-
try:
|
491
|
-
original_params = call.get("parameters", {})
|
492
|
-
substituted_params = self._substitute_placeholders(
|
493
|
-
original_params, tool_results_map
|
494
|
-
)
|
495
|
-
if substituted_params != original_params:
|
496
|
-
logger.info(
|
497
|
-
f"Substituted parameters for tool '{tool_name_to_exec}': {substituted_params}"
|
498
|
-
)
|
499
|
-
call["parameters"] = substituted_params
|
500
|
-
except Exception as sub_err:
|
501
|
-
logger.error(
|
502
|
-
f"Error substituting placeholders for tool '{tool_name_to_exec}': {sub_err}",
|
503
|
-
exc_info=True,
|
504
|
-
)
|
505
|
-
|
506
|
-
# ... (existing tool execution call) ...
|
507
|
-
try:
|
508
|
-
result = await self._execute_single_tool(agent_name, call)
|
509
|
-
executed_tool_results.append(result)
|
510
|
-
if result.get("status") == "success":
|
511
|
-
tool_result_str = str(result.get("result", ""))
|
512
|
-
tool_results_map[tool_name_to_exec] = tool_result_str
|
513
|
-
logger.debug(
|
514
|
-
f"Stored result for '{tool_name_to_exec}' (length: {len(tool_result_str)})"
|
515
|
-
)
|
516
|
-
else:
|
517
|
-
error_message = result.get("message", "Unknown error")
|
518
|
-
tool_results_map[tool_name_to_exec] = (
|
519
|
-
f"Error: {error_message}"
|
520
|
-
)
|
521
|
-
logger.warning(
|
522
|
-
f"Tool '{tool_name_to_exec}' failed, storing error message."
|
523
|
-
)
|
524
|
-
except Exception as tool_exec_err:
|
525
|
-
logger.error(
|
526
|
-
f"Exception during execution of tool {tool_name_to_exec}: {tool_exec_err}",
|
527
|
-
exc_info=True,
|
528
|
-
)
|
529
|
-
error_result = {
|
530
|
-
"tool_name": tool_name_to_exec,
|
531
|
-
"status": "error",
|
532
|
-
"message": f"Exception during execution: {str(tool_exec_err)}",
|
533
|
-
}
|
534
|
-
executed_tool_results.append(error_result)
|
535
|
-
tool_results_map[tool_name_to_exec] = (
|
536
|
-
f"Error: {str(tool_exec_err)}"
|
537
|
-
)
|
538
|
-
|
539
|
-
logger.info("Sequential tool execution with substitution complete.")
|
540
|
-
|
541
|
-
# ... (existing formatting of tool results) ...
|
542
|
-
tool_results_text_parts = []
|
543
|
-
for i, result in enumerate(executed_tool_results):
|
544
|
-
tool_name = result.get("tool_name", "unknown")
|
545
|
-
if (
|
546
|
-
isinstance(result, Exception)
|
547
|
-
or result.get("status") == "error"
|
548
|
-
):
|
549
|
-
error_msg = (
|
550
|
-
result.get("message", str(result))
|
551
|
-
if isinstance(result, dict)
|
552
|
-
else str(result)
|
553
|
-
)
|
554
|
-
logger.error(f"Tool '{tool_name}' failed: {error_msg}")
|
555
|
-
tool_results_text_parts.append(
|
556
|
-
f"Tool {i + 1} ({tool_name}) Execution Failed:\n{error_msg}"
|
557
|
-
)
|
558
|
-
else:
|
559
|
-
tool_output = str(result.get("result", ""))
|
560
|
-
tool_results_text_parts.append(
|
561
|
-
f"Tool {i + 1} ({tool_name}) Result:\n{tool_output}"
|
562
|
-
)
|
563
|
-
tool_results_context = "\n\n".join(tool_results_text_parts)
|
564
|
-
|
565
|
-
# --- Generate Final Response using Tool Results (No Streaming) ---
|
566
|
-
# Include original query (text part) and mention images were provided if applicable
|
567
|
-
original_query_context = f"Original Query: {str(query)}"
|
568
|
-
if images:
|
569
|
-
original_query_context += f" (with {len(images)} image(s))"
|
570
|
-
|
571
|
-
follow_up_prompt = f"{original_query_context}\n\nRESULTS FROM TOOL CALLS:\n{tool_results_context}\n\nBased on the original query, any provided images, and the tool results, please provide the final response to the user."
|
572
|
-
follow_up_system_prompt_parts = [
|
573
|
-
self.get_agent_system_prompt(agent_name)
|
574
|
-
]
|
575
|
-
follow_up_system_prompt_parts.append(f"USER IDENTIFIER: {user_id}")
|
576
|
-
if memory_context:
|
577
|
-
follow_up_system_prompt_parts.append(
|
578
|
-
f"\nORIGINAL CONVERSATION HISTORY:\n{memory_context}"
|
579
|
-
)
|
580
|
-
if prompt:
|
581
|
-
follow_up_system_prompt_parts.append(
|
582
|
-
f"\nORIGINAL ADDITIONAL PROMPT:\n{prompt}"
|
583
|
-
)
|
584
|
-
follow_up_system_prompt_parts.append(
|
585
|
-
f"\nCONTEXT: You previously decided to run {len(parsed_calls)} tool(s) sequentially. The results are provided above."
|
586
|
-
)
|
587
|
-
final_follow_up_system_prompt = "\n\n".join(
|
588
|
-
filter(None, follow_up_system_prompt_parts)
|
310
|
+
f"Model requested tool '{function_name}' with args: {arguments}"
|
589
311
|
)
|
590
312
|
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
# Use standard text generation for the final synthesis
|
595
|
-
synthesized_response_buffer = await self.llm_provider.generate_text(
|
596
|
-
prompt=follow_up_prompt,
|
597
|
-
system_prompt=final_follow_up_system_prompt,
|
598
|
-
api_key=self.api_key,
|
599
|
-
base_url=self.base_url,
|
600
|
-
model=self.model
|
601
|
-
or self.llm_provider.text_model, # Use text model for synthesis
|
313
|
+
# Execute the tool (async)
|
314
|
+
tool_result = await self.execute_tool(
|
315
|
+
agent_name, function_name, arguments
|
602
316
|
)
|
603
317
|
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
):
|
610
|
-
logger.error(
|
611
|
-
f"LLM provider failed during final generation: {synthesized_response_buffer}"
|
612
|
-
)
|
613
|
-
if output_format == "audio":
|
614
|
-
async for chunk in self.llm_provider.tts(
|
615
|
-
synthesized_response_buffer,
|
616
|
-
voice=audio_voice,
|
617
|
-
response_format=audio_output_format,
|
618
|
-
instructions=audio_instructions,
|
619
|
-
):
|
620
|
-
yield chunk
|
621
|
-
else:
|
622
|
-
yield synthesized_response_buffer
|
623
|
-
return
|
624
|
-
|
625
|
-
final_response_text = synthesized_response_buffer
|
626
|
-
logger.info(
|
627
|
-
f"Final synthesized response length: {len(final_response_text)}"
|
318
|
+
# Add the tool result to the prompt for the next round
|
319
|
+
# (You may want to format this differently for your use case)
|
320
|
+
full_prompt += (
|
321
|
+
f"\n\nTool '{function_name}' was called with arguments {arguments}.\n"
|
322
|
+
f"Result: {tool_result}\n"
|
628
323
|
)
|
324
|
+
continue # Loop again, LLM will see tool result and may call another tool or finish
|
629
325
|
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
)
|
634
|
-
final_response_text = initial_llm_response_buffer
|
635
|
-
else:
|
636
|
-
final_response_text = initial_llm_response_buffer
|
637
|
-
logger.info("No tools detected. Using initial response as final.")
|
326
|
+
# Otherwise, it's a normal message (final answer)
|
327
|
+
response_text = message.content
|
328
|
+
break
|
638
329
|
|
639
|
-
#
|
640
|
-
processed_final_text =
|
330
|
+
# Apply output guardrails if any
|
331
|
+
processed_final_text = response_text
|
641
332
|
if self.output_guardrails:
|
642
|
-
logger.info(
|
643
|
-
f"Applying output guardrails to final text response (length: {len(processed_final_text)})"
|
644
|
-
)
|
645
|
-
original_len = len(processed_final_text)
|
646
333
|
for guardrail in self.output_guardrails:
|
647
334
|
try:
|
648
335
|
processed_final_text = await guardrail.process(
|
@@ -652,25 +339,13 @@ class AgentService(AgentServiceInterface):
|
|
652
339
|
logger.error(
|
653
340
|
f"Error applying output guardrail {guardrail.__class__.__name__}: {e}"
|
654
341
|
)
|
655
|
-
if len(processed_final_text) != original_len:
|
656
|
-
logger.info(
|
657
|
-
f"Guardrails modified final text length from {original_len} to {len(processed_final_text)}"
|
658
|
-
)
|
659
342
|
|
660
343
|
self.last_text_response = processed_final_text
|
661
344
|
|
662
345
|
if output_format == "text":
|
663
|
-
|
664
|
-
yield processed_final_text
|
665
|
-
else:
|
666
|
-
logger.warning("Final processed text was empty.")
|
667
|
-
yield ""
|
346
|
+
yield processed_final_text or ""
|
668
347
|
elif output_format == "audio":
|
669
|
-
|
670
|
-
cleaned_audio_buffer = self._clean_for_audio(text_for_tts)
|
671
|
-
logger.info(
|
672
|
-
f"Processing {len(cleaned_audio_buffer)} characters for audio output"
|
673
|
-
)
|
348
|
+
cleaned_audio_buffer = self._clean_for_audio(processed_final_text)
|
674
349
|
if cleaned_audio_buffer:
|
675
350
|
async for audio_chunk in self.llm_provider.tts(
|
676
351
|
text=cleaned_audio_buffer,
|
@@ -680,14 +355,8 @@ class AgentService(AgentServiceInterface):
|
|
680
355
|
):
|
681
356
|
yield audio_chunk
|
682
357
|
else:
|
683
|
-
|
684
|
-
|
685
|
-
logger.info(
|
686
|
-
f"Response generation complete for agent '{agent_name}': {len(self.last_text_response)} final chars"
|
687
|
-
)
|
688
|
-
|
358
|
+
yield ""
|
689
359
|
except Exception as e:
|
690
|
-
# --- Error Handling ---
|
691
360
|
import traceback
|
692
361
|
|
693
362
|
error_msg = (
|
@@ -707,166 +376,8 @@ class AgentService(AgentServiceInterface):
|
|
707
376
|
else:
|
708
377
|
yield error_msg
|
709
378
|
|
710
|
-
async def _bytes_to_generator(self, data: bytes) -> AsyncGenerator[bytes, None]:
|
711
|
-
"""Convert bytes to an async generator for streaming."""
|
712
|
-
chunk_size = 4096
|
713
|
-
for i in range(0, len(data), chunk_size):
|
714
|
-
yield data[i : i + chunk_size]
|
715
|
-
await asyncio.sleep(0.01)
|
716
|
-
|
717
|
-
async def _handle_tool_call(self, agent_name: str, tool_text: str) -> str:
|
718
|
-
"""Handle marker-based tool calls."""
|
719
|
-
try:
|
720
|
-
start_marker = "[TOOL]"
|
721
|
-
end_marker = "[/TOOL]"
|
722
|
-
start_idx = tool_text.find(start_marker) + len(start_marker)
|
723
|
-
end_idx = tool_text.find(end_marker)
|
724
|
-
if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
|
725
|
-
logger.error(f"Malformed tool call text received: {tool_text}")
|
726
|
-
return "Error: Malformed tool call format."
|
727
|
-
|
728
|
-
tool_content = tool_text[start_idx:end_idx].strip()
|
729
|
-
tool_name = None
|
730
|
-
parameters = {}
|
731
|
-
|
732
|
-
for line in tool_content.split("\n"):
|
733
|
-
line = line.strip()
|
734
|
-
if not line:
|
735
|
-
continue
|
736
|
-
if line.startswith("name:"):
|
737
|
-
tool_name = line[5:].strip()
|
738
|
-
elif line.startswith("parameters:"):
|
739
|
-
params_text = line[11:].strip()
|
740
|
-
try:
|
741
|
-
# Attempt to parse as JSON first for robustness
|
742
|
-
parameters = json.loads(params_text)
|
743
|
-
except json.JSONDecodeError:
|
744
|
-
# Fallback to comma-separated key=value pairs
|
745
|
-
param_pairs = params_text.split(",")
|
746
|
-
for pair in param_pairs:
|
747
|
-
if "=" in pair:
|
748
|
-
k, v = pair.split("=", 1)
|
749
|
-
parameters[k.strip()] = v.strip()
|
750
|
-
logger.warning(
|
751
|
-
f"Parsed tool parameters using fallback method: {params_text}"
|
752
|
-
)
|
753
|
-
|
754
|
-
if not tool_name:
|
755
|
-
logger.error(f"Tool name missing in tool call: {tool_content}")
|
756
|
-
return "Error: Tool name missing in call."
|
757
|
-
|
758
|
-
result = await self.execute_tool(agent_name, tool_name, parameters)
|
759
|
-
|
760
|
-
if result.get("status") == "success":
|
761
|
-
tool_result = str(result.get("result", ""))
|
762
|
-
return tool_result
|
763
|
-
else:
|
764
|
-
error_msg = f"Error calling {tool_name}: {result.get('message', 'Unknown error')}"
|
765
|
-
logger.error(error_msg)
|
766
|
-
return error_msg
|
767
|
-
|
768
|
-
except Exception as e:
|
769
|
-
import traceback
|
770
|
-
|
771
|
-
logger.error(f"Error processing tool call: {e}\n{traceback.format_exc()}")
|
772
|
-
return f"Error processing tool call: {str(e)}"
|
773
|
-
|
774
|
-
def _get_tool_usage_prompt(self, agent_name: str) -> str:
|
775
|
-
"""Generate marker-based instructions for tool usage."""
|
776
|
-
# Only include tools actually assigned to this agent
|
777
|
-
agent_tools = self.tool_registry.get_agent_tools(agent_name)
|
778
|
-
if not agent_tools:
|
779
|
-
return ""
|
780
|
-
|
781
|
-
# Simplify tool representation for the prompt
|
782
|
-
simplified_tools = []
|
783
|
-
for tool in agent_tools:
|
784
|
-
simplified_tool = {
|
785
|
-
"name": tool.get("name"),
|
786
|
-
"description": tool.get("description"),
|
787
|
-
"parameters": tool.get("parameters", {}).get("properties", {}),
|
788
|
-
}
|
789
|
-
simplified_tools.append(simplified_tool)
|
790
|
-
|
791
|
-
tools_json = json.dumps(simplified_tools, indent=2)
|
792
|
-
|
793
|
-
return f"""
|
794
|
-
AVAILABLE TOOLS:
|
795
|
-
{tools_json}
|
796
|
-
|
797
|
-
⚠️ CRITICAL INSTRUCTIONS FOR TOOL USAGE:
|
798
|
-
1. EXECUTION ORDER MATTERS: If multiple steps are needed (e.g., get information THEN use it), you MUST output the [TOOL] blocks in the exact sequence they need to run. Output the information-gathering tool call FIRST, then the tool call that uses the information.
|
799
|
-
2. ONLY TOOL CALLS: When using a tool, NEVER include explanatory text before or after the tool call block. Only output the exact tool call format shown below.
|
800
|
-
3. USE TOOLS WHEN NEEDED: Always call the necessary tool to give the latest information, especially for time-sensitive queries.
|
801
|
-
|
802
|
-
TOOL USAGE FORMAT:
|
803
|
-
[TOOL]
|
804
|
-
name: tool_name
|
805
|
-
parameters: {{"key1": "value1", "key2": "value2"}}
|
806
|
-
[/TOOL]
|
807
|
-
|
808
|
-
EXAMPLES:
|
809
|
-
|
810
|
-
✅ CORRECT - Get news THEN email (Correct Order):
|
811
|
-
[TOOL]
|
812
|
-
name: search_internet
|
813
|
-
parameters: {{"query": "latest news on Canada"}}
|
814
|
-
[/TOOL]
|
815
|
-
[TOOL]
|
816
|
-
name: mcp
|
817
|
-
parameters: {{"query": "Send an email to
|
818
|
-
bob@bob.com with subject
|
819
|
-
'Friendly Reminder to Clean Your Room'
|
820
|
-
and body 'Hi Bob, just a friendly
|
821
|
-
reminder to please clean your room
|
822
|
-
when you get a chance.'"}}
|
823
|
-
[/TOOL]
|
824
|
-
(Note: The system will handle replacing placeholders like '{{output_of_search_internet}}' if possible, but the ORDER is crucial.)
|
825
|
-
|
826
|
-
|
827
|
-
❌ INCORRECT - Wrong Order:
|
828
|
-
[TOOL]
|
829
|
-
name: mcp
|
830
|
-
parameters: {{"query": "Send an email to
|
831
|
-
bob@bob.com with subject
|
832
|
-
'Friendly Reminder to Clean Your Room'
|
833
|
-
and body 'Hi Bob, just a friendly
|
834
|
-
reminder to please clean your room
|
835
|
-
when you get a chance.'"}}
|
836
|
-
[/TOOL]
|
837
|
-
[TOOL]
|
838
|
-
name: search_internet
|
839
|
-
parameters: {{"query": "latest news on Canada"}}
|
840
|
-
[/TOOL]
|
841
|
-
|
842
|
-
|
843
|
-
❌ INCORRECT - Explanatory Text:
|
844
|
-
To get the news, I'll search.
|
845
|
-
[TOOL]
|
846
|
-
name: search_internet
|
847
|
-
parameters: {{"query": "latest news on Solana"}}
|
848
|
-
[/TOOL]
|
849
|
-
Now I will email it.
|
850
|
-
[TOOL]
|
851
|
-
name: mcp
|
852
|
-
parameters: {{"query": "Send an email to
|
853
|
-
bob@bob.com with subject
|
854
|
-
'Friendly Reminder to Clean Your Room'
|
855
|
-
and body 'Hi Bob, just a friendly
|
856
|
-
reminder to please clean your room
|
857
|
-
when you get a chance.'"}}
|
858
|
-
[/TOOL]
|
859
|
-
|
860
|
-
|
861
|
-
REMEMBER:
|
862
|
-
- Output ONLY the [TOOL] blocks in the correct execution order.
|
863
|
-
- I will execute the tools sequentially as you provide them.
|
864
|
-
- You will receive the results of ALL tool calls before formulating the final response.
|
865
|
-
"""
|
866
|
-
|
867
379
|
def _clean_for_audio(self, text: str) -> str:
|
868
380
|
"""Remove Markdown formatting, emojis, and non-pronounceable characters from text."""
|
869
|
-
import re
|
870
381
|
|
871
382
|
if not text:
|
872
383
|
return ""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: solana-agent
|
3
|
-
Version: 29.2.
|
3
|
+
Version: 29.2.1
|
4
4
|
Summary: AI Agents for Solana
|
5
5
|
License: MIT
|
6
6
|
Keywords: solana,solana ai,solana agent,ai,ai agent,ai agents
|
@@ -41,9 +41,7 @@ Description-Content-Type: text/markdown
|
|
41
41
|
[](https://opensource.org/licenses/MIT)
|
42
42
|
[](https://codecov.io/gh/truemagic-coder/solana-agent)
|
43
43
|
[](https://github.com/truemagic-coder/solana-agent/actions/workflows/ci.yml)
|
44
|
-
[](https://github.com/truemagic-coder/solana-agent)
|
45
44
|
[](https://github.com/astral-sh/ruff)
|
46
|
-
[](https://libraries.io/pypi/solana-agent)
|
47
45
|
|
48
46
|

|
49
47
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
solana_agent/__init__.py,sha256=g83qhMOCwcWL19V4CYbQwl0Ykpb0xn49OUh05i-pu3g,1001
|
2
2
|
solana_agent/adapters/__init__.py,sha256=tiEEuuy0NF3ngc_tGEcRTt71zVI58v3dYY9RvMrF2Cg,204
|
3
3
|
solana_agent/adapters/mongodb_adapter.py,sha256=Hq3S8VzfLmnPjV40z8yJXGqUamOJcX5GbOMd-1nNWO4,3175
|
4
|
-
solana_agent/adapters/openai_adapter.py,sha256=
|
4
|
+
solana_agent/adapters/openai_adapter.py,sha256=nKfJzgtxNfQWLp6vcu-2Z8zBbvpYgTOEil7nPH7QIlU,23340
|
5
5
|
solana_agent/adapters/pinecone_adapter.py,sha256=XlfOpoKHwzpaU4KZnovO2TnEYbsw-3B53ZKQDtBeDgU,23847
|
6
6
|
solana_agent/cli.py,sha256=FGvTIQmKLp6XsQdyKtuhIIfbBtMmcCCXfigNrj4bzMc,4704
|
7
7
|
solana_agent/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -17,7 +17,7 @@ solana_agent/interfaces/client/client.py,sha256=hFYe04lFGbp4BDlUMOnYQrp_SQXFPckt
|
|
17
17
|
solana_agent/interfaces/guardrails/guardrails.py,sha256=gZCQ1FrirW-mX6s7FoYrbRs6golsp-x269kk4kQiZzc,572
|
18
18
|
solana_agent/interfaces/plugins/plugins.py,sha256=Rz52cWBLdotwf4kV-2mC79tRYlN29zHSu1z9-y1HVPk,3329
|
19
19
|
solana_agent/interfaces/providers/data_storage.py,sha256=Y92Cq8BtC55VlsYLD7bo3ofqQabNnlg7Q4H1Q6CDsLU,1713
|
20
|
-
solana_agent/interfaces/providers/llm.py,sha256=
|
20
|
+
solana_agent/interfaces/providers/llm.py,sha256=IBtvl_p2Dd9pzuA6ub6DZRn50Td8f-XMtcc1M4V3gvA,2872
|
21
21
|
solana_agent/interfaces/providers/memory.py,sha256=h3HEOwWCiFGIuFBX49XOv1jFaQW3NGjyKPOfmQloevk,1011
|
22
22
|
solana_agent/interfaces/providers/vector_storage.py,sha256=XPYzvoWrlDVFCS9ItBmoqCFWXXWNYY-d9I7_pvP7YYk,1561
|
23
23
|
solana_agent/interfaces/services/agent.py,sha256=MgLudTwzCzzzSR6PsVTB-w5rhGDHB5B81TGjo2z3G-A,2152
|
@@ -32,12 +32,12 @@ solana_agent/plugins/tools/auto_tool.py,sha256=uihijtlc9CCqCIaRcwPuuN7o1SHIpWL2G
|
|
32
32
|
solana_agent/repositories/__init__.py,sha256=fP83w83CGzXLnSdq-C5wbw9EhWTYtqE2lQTgp46-X_4,163
|
33
33
|
solana_agent/repositories/memory.py,sha256=SKQJJisrERccqd4cm4ERlp5BmKHVQAp1fzp8ce4i2bw,8377
|
34
34
|
solana_agent/services/__init__.py,sha256=iko0c2MlF8b_SA_nuBGFllr2E3g_JowOrOzGcnU9tkA,162
|
35
|
-
solana_agent/services/agent.py,sha256=
|
35
|
+
solana_agent/services/agent.py,sha256=q5cYzY6Jp5HFtYgJtrqTBWp5Z2n3dXdpQ5YScSKfvPo,18056
|
36
36
|
solana_agent/services/knowledge_base.py,sha256=ZvOPrSmcNDgUzz4bJIQ4LeRl9vMZiK9hOfs71IpB7Bk,32735
|
37
37
|
solana_agent/services/query.py,sha256=ENUfs4WSTpODMRXppDVW-Y3li9jYn8pOfQIHIPerUdQ,18498
|
38
38
|
solana_agent/services/routing.py,sha256=C5Ku4t9TqvY7S8wlUPMTC04HCrT4Ib3E8Q8yX0lVU_s,7137
|
39
|
-
solana_agent-29.2.
|
40
|
-
solana_agent-29.2.
|
41
|
-
solana_agent-29.2.
|
42
|
-
solana_agent-29.2.
|
43
|
-
solana_agent-29.2.
|
39
|
+
solana_agent-29.2.1.dist-info/LICENSE,sha256=BnSRc-NSFuyF2s496l_4EyrwAP6YimvxWcjPiJ0J7g4,1057
|
40
|
+
solana_agent-29.2.1.dist-info/METADATA,sha256=5yfqF9bxgPvHMjsxgor_vHhpmnRHODY8N3IQ4QgexQU,35620
|
41
|
+
solana_agent-29.2.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
42
|
+
solana_agent-29.2.1.dist-info/entry_points.txt,sha256=-AuT_mfqk8dlZ0pHuAjx1ouAWpTRjpqvEUa6YV3lmc0,53
|
43
|
+
solana_agent-29.2.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|