solana-agent 29.2.0__py3-none-any.whl → 29.2.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.
@@ -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
- ) -> str: # pragma: no cover
167
- """Generate text from OpenAI models as a single string (no images)."""
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
- if response.choices and response.choices[0].message.content:
189
- return response.choices[0].message.content
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 f"I apologize, but I encountered an API error: {e}"
197
+ return None
196
198
  except Exception as e:
197
199
  logger.exception(f"Error in generate_text: {e}")
198
- return f"I apologize, but I encountered an unexpected error: {e}"
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
- ) -> str:
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
 
@@ -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, supporting multiple sequential tool calls with placeholder substitution.
366
- Optionally accepts images for vision-capable models.
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
- logger.debug(
385
- f"Generating response for agent '{agent_name}'. Output format: {output_format}. Images provided: {bool(images)}."
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
- # --- System Prompt Assembly ---
390
- system_prompt_parts = [self.get_agent_system_prompt(agent_name)]
391
- tool_instructions = self._get_tool_usage_prompt(agent_name)
392
- if tool_instructions:
393
- system_prompt_parts.append(tool_instructions)
394
- system_prompt_parts.append(f"USER IDENTIFIER: {user_id}")
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
- # --- END LLM METHOD CHOICE ---
434
-
435
- # Check for errors returned as string by the adapter
436
- if isinstance(initial_llm_response_buffer, str) and (
437
- initial_llm_response_buffer.startswith(
438
- "I apologize, but I encountered an"
439
- )
440
- or initial_llm_response_buffer.startswith("Error:")
441
- ):
442
- logger.error(
443
- f"LLM provider failed during initial generation: {initial_llm_response_buffer}"
444
- )
445
- # Yield the error and exit
446
- if output_format == "audio":
447
- async for chunk in self.llm_provider.tts(
448
- initial_llm_response_buffer,
449
- voice=audio_voice,
450
- response_format=audio_output_format,
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"Executing {len(parsed_calls)} tools sequentially with substitution..."
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
- logger.info(
592
- "Generating final response incorporating tool results..."
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
- if isinstance(synthesized_response_buffer, str) and (
605
- synthesized_response_buffer.startswith(
606
- "I apologize, but I encountered an"
607
- )
608
- or synthesized_response_buffer.startswith("Error:")
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
- else:
631
- logger.warning(
632
- "Tool markers detected, but no valid tool calls parsed. Treating initial response as final."
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
- # --- Final Output Processing (Guardrails, TTS, Yielding) ---
640
- processed_final_text = final_response_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
- if processed_final_text:
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
- text_for_tts = processed_final_text
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
- logger.warning("Final text for audio was empty after cleaning.")
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.0
3
+ Version: 29.2.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
@@ -41,9 +41,7 @@ Description-Content-Type: text/markdown
41
41
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
42
42
  [![codecov](https://img.shields.io/codecov/c/github/truemagic-coder/solana-agent/main.svg)](https://codecov.io/gh/truemagic-coder/solana-agent)
43
43
  [![Build Status](https://img.shields.io/github/actions/workflow/status/truemagic-coder/solana-agent/ci.yml?branch=main)](https://github.com/truemagic-coder/solana-agent/actions/workflows/ci.yml)
44
- [![Lines of Code](https://tokei.rs/b1/github/truemagic-coder/solana-agent?type=python&category=code&style=flat)](https://github.com/truemagic-coder/solana-agent)
45
44
  [![Ruff Style](https://img.shields.io/badge/style-ruff-41B5BE)](https://github.com/astral-sh/ruff)
46
- [![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/truemagic-coder/solana-agent)](https://libraries.io/pypi/solana-agent)
47
45
 
48
46
  ![Solana Agent Logo](https://dl.walletbubbles.com/solana-agent-logo.png?width=200)
49
47
 
@@ -56,7 +54,7 @@ Build your AI agents in three lines of code!
56
54
  * Simple Agent Definition
57
55
  * Fast Responses
58
56
  * Multi-Vendor Support
59
- * Solana Ecosystem Integration
57
+ * Solana Integration
60
58
  * Multi-Agent Swarm
61
59
  * Multi-Modal (Images & Audio & Text)
62
60
  * Conversational Memory & History
@@ -81,7 +79,7 @@ Build your AI agents in three lines of code!
81
79
  * Simple agent definition using JSON
82
80
  * Fast AI responses
83
81
  * Multi-vendor support including OpenAI, Grok, and Gemini AI services
84
- * Solana Ecosystem Integration via [AgentiPy](https://github.com/niceberginc/agentipy)
82
+ * Solana Integration with transfers and swaps
85
83
  * MCP tool usage with first-class support for [Zapier](https://zapier.com/mcp)
86
84
  * Integrated observability and tracing via [Pydantic Logfire](https://pydantic.dev/logfire)
87
85
  * Designed for a multi-agent swarm
@@ -108,7 +106,6 @@ Build your AI agents in three lines of code!
108
106
  * [MongoDB](https://mongodb.com) - Conversational History (optional)
109
107
  * [Zep Cloud](https://getzep.com) - Conversational Memory (optional)
110
108
  * [Pinecone](https://pinecone.io) - Knowledge Base (optional)
111
- * [AgentiPy](https://agentipy.fun) - Solana Ecosystem (optional)
112
109
  * [Zapier](https://zapier.com) - App Integrations (optional)
113
110
  * [Pydantic Logfire](https://pydantic.dev/logfire) - Observability and Tracing (optional)
114
111
 
@@ -658,36 +655,68 @@ Tools empower agents to interact with external systems, fetch data, or perform a
658
655
 
659
656
  Tools can be used from plugins like Solana Agent Kit (sakit) or via inline tools. Tools available via plugins integrate automatically with Solana Agent.
660
657
 
661
- * Agents can use multiple tools per response and should apply the right sequential order (like send an email to bob@bob.com with the latest news on Solana)
662
- * Agents choose the best tools for the job
663
- * Solana Agent doesn't use OpenAI function calling (tools) as they don't support async functions
664
- * Solana Agent tools are async functions
658
+ ### Solana Transfer
665
659
 
666
- ### Solana
660
+ This plugin enables Solana Agent to transfer SOL and SPL tokens from the agent's wallet to the destination wallet.
661
+
662
+ Don't use tickers - but mint addresses in your user queries.
667
663
 
668
664
  `pip install sakit`
669
665
 
670
666
  ```python
671
667
  config = {
672
668
  "tools": {
673
- "solana": {
674
- "private_key": "your-solana-wallet-private-key", # base58 encoded string
675
- "rpc_url": "your-solana-rpc-url",
669
+ "solana_transfer": {
670
+ "rpc_url": "my-rpc-url", # Required - your RPC URL - Helius is recommended
671
+ "private_key": "my-private-key", # Required - base58 string - please use env vars to store the key as it is very confidential
676
672
  },
677
673
  },
678
674
  "agents": [
679
675
  {
680
676
  "name": "solana_expert",
681
- "instructions": "You are an expert Solana blockchain assistant. You always use the Solana tool to perform actions on the Solana blockchain.",
682
- "specialization": "Solana blockchain interaction",
683
- "tools": ["solana"], # Enable the tool for this agent
677
+ "instructions": "You are a Solana expert that can transfer tokens.",
678
+ "specialization": "Solana Blockchain",
679
+ "tools": ["solana_transfer"], # Enable the tool for this agent
684
680
  }
685
- ]
681
+ ],
682
+ }
683
+
684
+ solana_agent = SolanaAgent(config=config)
685
+
686
+ async for response in solana_agent.process("user123", "Transfer 0.01 Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB to DzvqBkUHUhuhHtNKGWSCVEAm2rHdm9bxxdQYC6mZBZyF"):
687
+ print(response, end="")
688
+ ```
689
+
690
+ ### Solana Swap
691
+
692
+ This plugin enables Solana Agent to trade (swap) tokens using Jupiter.
693
+
694
+ Don't use tickers - but mint addresses in your user queries.
695
+
696
+ `pip install sakit`
697
+
698
+ ```python
699
+ config = {
700
+ "tools": {
701
+ "solana_swap": {
702
+ "rpc_url": "my-rpc-url", # Required - your RPC URL - Helius is recommended
703
+ "private_key": "my-private-key", # Required - base58 string - please use env vars to store the key as it is very confidential
704
+ "jupiter_url": "my-custom-url" # Optional - if you are using a custom Jupiter service like Metis from QuickNode
705
+ },
706
+ },
707
+ "agents": [
708
+ {
709
+ "name": "solana_expert",
710
+ "instructions": "You are a Solana expert that can swap tokens.",
711
+ "specialization": "Solana Blockchain",
712
+ "tools": ["solana_swap"], # Enable the tool for this agent
713
+ }
714
+ ],
686
715
  }
687
716
 
688
717
  solana_agent = SolanaAgent(config=config)
689
718
 
690
- async for response in solana_agent.process("user123", "What is my SOL balance?"):
719
+ async for response in solana_agent.process("user123", "Swap 0.01 Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB to So11111111111111111111111111111111111111112"):
691
720
  print(response, end="")
692
721
  ```
693
722
 
@@ -739,7 +768,7 @@ from solana_agent import SolanaAgent
739
768
  config = {
740
769
  "tools": {
741
770
  "mcp": {
742
- "urls": ["my-zapier-mcp-url"],
771
+ "url": "my-zapier-mcp-url",
743
772
  }
744
773
  },
745
774
  "agents": [
@@ -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=XnocNAV1nJGcjpRgOyMXnyDQSU8HvTx9zmb4pWtSb58,23432
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=FbK6HNMBOIONPE-ljPRElkO2fmFbkzWEo4KuYfcDEFE,2727
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=0zehiZyTw91IQ_nskxYZF8plecA66m5rD-OA7Cr1Xq8,42462
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.0.dist-info/LICENSE,sha256=BnSRc-NSFuyF2s496l_4EyrwAP6YimvxWcjPiJ0J7g4,1057
40
- solana_agent-29.2.0.dist-info/METADATA,sha256=7GiWgj6IbAnxg-xP5kvROu_qUFYK7ZUcf3H-w84My_o,35946
41
- solana_agent-29.2.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
42
- solana_agent-29.2.0.dist-info/entry_points.txt,sha256=-AuT_mfqk8dlZ0pHuAjx1ouAWpTRjpqvEUa6YV3lmc0,53
43
- solana_agent-29.2.0.dist-info/RECORD,,
39
+ solana_agent-29.2.2.dist-info/LICENSE,sha256=BnSRc-NSFuyF2s496l_4EyrwAP6YimvxWcjPiJ0J7g4,1057
40
+ solana_agent-29.2.2.dist-info/METADATA,sha256=cCliPjVhs1OYUrlewGZDXyRvASDLRKMgrC2g7DPzaYY,36578
41
+ solana_agent-29.2.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
42
+ solana_agent-29.2.2.dist-info/entry_points.txt,sha256=-AuT_mfqk8dlZ0pHuAjx1ouAWpTRjpqvEUa6YV3lmc0,53
43
+ solana_agent-29.2.2.dist-info/RECORD,,