solana-agent 28.2.0__py3-none-any.whl → 28.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,9 +5,22 @@ These adapters implement the LLMProvider interface for different LLM services.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import AsyncGenerator, List, Literal, Optional, Type, TypeVar
9
-
10
- from openai import AsyncOpenAI
8
+ import base64
9
+ import io
10
+ import math
11
+ from typing import (
12
+ AsyncGenerator,
13
+ List,
14
+ Literal,
15
+ Optional,
16
+ Type,
17
+ TypeVar,
18
+ Dict,
19
+ Any,
20
+ Union,
21
+ )
22
+ from PIL import Image
23
+ from openai import AsyncOpenAI, OpenAIError
11
24
  from pydantic import BaseModel
12
25
  import instructor
13
26
  from instructor import Mode
@@ -21,12 +34,23 @@ logger = logging.getLogger(__name__)
21
34
  T = TypeVar("T", bound=BaseModel)
22
35
 
23
36
  DEFAULT_CHAT_MODEL = "gpt-4.1"
37
+ DEFAULT_VISION_MODEL = "gpt-4.1"
24
38
  DEFAULT_PARSE_MODEL = "gpt-4.1-nano"
25
39
  DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large"
26
40
  DEFAULT_EMBEDDING_DIMENSIONS = 3072
27
41
  DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-mini-transcribe"
28
42
  DEFAULT_TTS_MODEL = "tts-1"
29
43
 
44
+ # Image constants
45
+ SUPPORTED_IMAGE_FORMATS = {"PNG", "JPEG", "WEBP", "GIF"}
46
+ MAX_IMAGE_SIZE_MB = 20
47
+ MAX_TOTAL_IMAGE_SIZE_MB = 50
48
+ MAX_IMAGE_COUNT = 500
49
+ GPT41_PATCH_SIZE = 32
50
+ GPT41_MAX_PATCHES = 1536
51
+ GPT41_MINI_MULTIPLIER = 1.62
52
+ GPT41_NANO_MULTIPLIER = 2.46
53
+
30
54
 
31
55
  class OpenAIAdapter(LLMProvider):
32
56
  """OpenAI implementation of LLMProvider with web search capabilities."""
@@ -39,13 +63,14 @@ class OpenAIAdapter(LLMProvider):
39
63
  try:
40
64
  logfire.configure(token=logfire_api_key)
41
65
  self.logfire = True
42
- logger.info("Logfire configured successfully.") # Use logger.info
66
+ logger.info("Logfire configured successfully.")
43
67
  except Exception as e:
44
- logger.error(f"Failed to configure Logfire: {e}") # Use logger.error
68
+ logger.error(f"Failed to configure Logfire: {e}")
45
69
  self.logfire = False
46
70
 
47
71
  self.parse_model = DEFAULT_PARSE_MODEL
48
72
  self.text_model = DEFAULT_CHAT_MODEL
73
+ self.vision_model = DEFAULT_VISION_MODEL # Add vision model attribute
49
74
  self.transcription_model = DEFAULT_TRANSCRIPTION_MODEL
50
75
  self.tts_model = DEFAULT_TTS_MODEL
51
76
  self.embedding_model = DEFAULT_EMBEDDING_MODEL
@@ -139,20 +164,17 @@ class OpenAIAdapter(LLMProvider):
139
164
  base_url: Optional[str] = None,
140
165
  model: Optional[str] = None,
141
166
  ) -> str: # pragma: no cover
142
- """Generate text from OpenAI models as a single string."""
167
+ """Generate text from OpenAI models as a single string (no images)."""
143
168
  messages = []
144
169
  if system_prompt:
145
170
  messages.append({"role": "system", "content": system_prompt})
146
171
  messages.append({"role": "user", "content": prompt})
147
172
 
148
- # Prepare request parameters - stream is always False now
149
173
  request_params = {
150
174
  "messages": messages,
151
- "stream": False, # Hardcoded to False
152
175
  "model": model or self.text_model,
153
176
  }
154
177
 
155
- # Determine client based on provided api_key/base_url
156
178
  if api_key and base_url:
157
179
  client = AsyncOpenAI(api_key=api_key, base_url=base_url)
158
180
  else:
@@ -162,24 +184,221 @@ class OpenAIAdapter(LLMProvider):
162
184
  logfire.instrument_openai(client)
163
185
 
164
186
  try:
165
- # Make the non-streaming API call
166
187
  response = await client.chat.completions.create(**request_params)
167
-
168
- # Handle non-streaming response
169
188
  if response.choices and response.choices[0].message.content:
170
- full_text = response.choices[0].message.content
171
- return full_text # Return the complete string
189
+ return response.choices[0].message.content
172
190
  else:
173
- logger.warning(
174
- "Received non-streaming response with no content."
175
- ) # Use logger.warning
176
- return "" # Return empty string if no content
177
-
191
+ logger.warning("Received non-streaming response with no content.")
192
+ return ""
193
+ except OpenAIError as e: # Catch specific OpenAI errors
194
+ logger.error(f"OpenAI API error during text generation: {e}")
195
+ return f"I apologize, but I encountered an API error: {e}"
178
196
  except Exception as e:
179
- # Log the exception and return an error message string
180
197
  logger.exception(f"Error in generate_text: {e}")
181
- # Consider returning a more informative error string or raising
182
- return f"Error generating text: {e}"
198
+ return f"I apologize, but I encountered an unexpected error: {e}"
199
+
200
+ def _calculate_gpt41_image_cost(self, width: int, height: int, model: str) -> int:
201
+ """Calculates the token cost for an image with GPT-4.1 models."""
202
+ patches_wide = math.ceil(width / GPT41_PATCH_SIZE)
203
+ patches_high = math.ceil(height / GPT41_PATCH_SIZE)
204
+ total_patches_needed = patches_wide * patches_high
205
+
206
+ if total_patches_needed > GPT41_MAX_PATCHES:
207
+ scale_factor = math.sqrt(GPT41_MAX_PATCHES / total_patches_needed)
208
+ new_width = math.floor(width * scale_factor)
209
+ new_height = math.floor(height * scale_factor)
210
+
211
+ final_patches_wide_scaled = math.ceil(new_width / GPT41_PATCH_SIZE)
212
+ final_patches_high_scaled = math.ceil(new_height / GPT41_PATCH_SIZE)
213
+ image_tokens = final_patches_wide_scaled * final_patches_high_scaled
214
+
215
+ # Ensure it doesn't exceed the cap due to ceiling operations after scaling
216
+ image_tokens = min(image_tokens, GPT41_MAX_PATCHES)
217
+
218
+ logger.debug(
219
+ f"Image scaled down. Original patches: {total_patches_needed}, New dims: ~{new_width}x{new_height}, Final patches: {image_tokens}"
220
+ )
221
+
222
+ else:
223
+ image_tokens = total_patches_needed
224
+ logger.debug(f"Image fits within patch limit. Patches: {image_tokens}")
225
+
226
+ # Apply model-specific multiplier
227
+ if "mini" in model:
228
+ total_tokens = math.ceil(image_tokens * GPT41_MINI_MULTIPLIER)
229
+ elif "nano" in model:
230
+ total_tokens = math.ceil(image_tokens * GPT41_NANO_MULTIPLIER)
231
+ else: # Assume base gpt-4.1
232
+ total_tokens = image_tokens
233
+
234
+ logger.info(
235
+ f"Calculated token cost for image ({width}x{height}) with model '{model}': {total_tokens} tokens (base image tokens: {image_tokens})"
236
+ )
237
+ return total_tokens
238
+
239
+ async def generate_text_with_images(
240
+ self,
241
+ prompt: str,
242
+ images: List[Union[str, bytes]],
243
+ system_prompt: str = "",
244
+ detail: Literal["low", "high", "auto"] = "auto",
245
+ ) -> str: # pragma: no cover
246
+ """Generate text from OpenAI models using text and image inputs."""
247
+ if not images:
248
+ logger.warning(
249
+ "generate_text_with_images called with no images. Falling back to generate_text."
250
+ )
251
+ return await self.generate_text(prompt, system_prompt)
252
+
253
+ target_model = self.vision_model
254
+ if "gpt-4.1" not in target_model: # Basic check for vision model
255
+ logger.warning(
256
+ f"Model '{target_model}' might not support vision. Using it anyway."
257
+ )
258
+
259
+ content_list: List[Dict[str, Any]] = [{"type": "text", "text": prompt}]
260
+ total_image_bytes = 0
261
+ total_image_tokens = 0
262
+
263
+ if len(images) > MAX_IMAGE_COUNT:
264
+ logger.error(
265
+ f"Too many images provided ({len(images)}). Maximum is {MAX_IMAGE_COUNT}."
266
+ )
267
+ return f"Error: Too many images provided ({len(images)}). Maximum is {MAX_IMAGE_COUNT}."
268
+
269
+ for i, image_input in enumerate(images):
270
+ image_url_data: Dict[str, Any] = {"detail": detail}
271
+ image_bytes: Optional[bytes] = None
272
+ image_format: Optional[str] = None
273
+ width: Optional[int] = None
274
+ height: Optional[int] = None
275
+
276
+ try:
277
+ if isinstance(image_input, str): # It's a URL
278
+ logger.debug(f"Processing image URL: {image_input[:50]}...")
279
+ image_url_data["url"] = image_input
280
+ # Cannot easily validate size/format/dimensions or calculate cost for URLs
281
+ logger.warning(
282
+ "Cannot validate size/format or calculate token cost for image URLs."
283
+ )
284
+
285
+ elif isinstance(image_input, bytes): # It's image bytes
286
+ logger.debug(
287
+ f"Processing image bytes (size: {len(image_input)})..."
288
+ )
289
+ image_bytes = image_input
290
+ size_mb = len(image_bytes) / (1024 * 1024)
291
+ if size_mb > MAX_IMAGE_SIZE_MB:
292
+ logger.error(
293
+ f"Image {i + 1} size ({size_mb:.2f}MB) exceeds limit ({MAX_IMAGE_SIZE_MB}MB)."
294
+ )
295
+ return f"Error: Image {i + 1} size ({size_mb:.2f}MB) exceeds limit ({MAX_IMAGE_SIZE_MB}MB)."
296
+ total_image_bytes += len(image_bytes)
297
+
298
+ # Use Pillow to validate format and get dimensions
299
+ try:
300
+ img = Image.open(io.BytesIO(image_bytes))
301
+ image_format = img.format
302
+ width, height = img.size
303
+ img.verify() # Verify integrity
304
+ # Re-open after verify
305
+ img = Image.open(io.BytesIO(image_bytes))
306
+ width, height = img.size # Get dimensions again
307
+
308
+ if image_format not in SUPPORTED_IMAGE_FORMATS:
309
+ logger.error(
310
+ f"Unsupported image format '{image_format}' for image {i + 1}."
311
+ )
312
+ return f"Error: Unsupported image format '{image_format}'. Supported formats: {SUPPORTED_IMAGE_FORMATS}."
313
+
314
+ logger.debug(
315
+ f"Image {i + 1}: Format={image_format}, Dimensions={width}x{height}"
316
+ )
317
+
318
+ # Calculate cost only if dimensions are available
319
+ if width and height and "gpt-4.1" in target_model:
320
+ total_image_tokens += self._calculate_gpt41_image_cost(
321
+ width, height, target_model
322
+ )
323
+
324
+ except (IOError, SyntaxError) as img_err:
325
+ logger.error(
326
+ f"Invalid or corrupted image data for image {i + 1}: {img_err}"
327
+ )
328
+ return f"Error: Invalid or corrupted image data provided for image {i + 1}."
329
+ except Exception as pillow_err:
330
+ logger.error(
331
+ f"Pillow error processing image {i + 1}: {pillow_err}"
332
+ )
333
+ return f"Error: Could not process image data for image {i + 1}."
334
+
335
+ # Encode to Base64 Data URL
336
+ mime_type = Image.MIME.get(image_format)
337
+ if not mime_type:
338
+ logger.warning(
339
+ f"Could not determine MIME type for format {image_format}. Defaulting to image/jpeg."
340
+ )
341
+ mime_type = "image/jpeg"
342
+ base64_image = base64.b64encode(image_bytes).decode("utf-8")
343
+ image_url_data["url"] = f"data:{mime_type};base64,{base64_image}"
344
+
345
+ else:
346
+ logger.error(
347
+ f"Invalid image input type for image {i + 1}: {type(image_input)}"
348
+ )
349
+ return f"Error: Invalid image input type for image {i + 1}. Must be URL (str) or bytes."
350
+
351
+ content_list.append({"type": "image_url", "image_url": image_url_data})
352
+
353
+ except Exception as proc_err:
354
+ logger.error(
355
+ f"Error processing image {i + 1}: {proc_err}", exc_info=True
356
+ )
357
+ return f"Error: Failed to process image {i + 1}."
358
+
359
+ total_size_mb = total_image_bytes / (1024 * 1024)
360
+ if total_size_mb > MAX_TOTAL_IMAGE_SIZE_MB:
361
+ logger.error(
362
+ f"Total image size ({total_size_mb:.2f}MB) exceeds limit ({MAX_TOTAL_IMAGE_SIZE_MB}MB)."
363
+ )
364
+ return f"Error: Total image size ({total_size_mb:.2f}MB) exceeds limit ({MAX_TOTAL_IMAGE_SIZE_MB}MB)."
365
+
366
+ messages: List[Dict[str, Any]] = []
367
+ if system_prompt:
368
+ messages.append({"role": "system", "content": system_prompt})
369
+ messages.append({"role": "user", "content": content_list})
370
+
371
+ request_params = {
372
+ "messages": messages,
373
+ "model": target_model,
374
+ # "max_tokens": 300 # Optional: Add max_tokens if needed
375
+ }
376
+
377
+ if self.logfire:
378
+ logfire.instrument_openai(self.client)
379
+
380
+ logger.info(
381
+ f"Sending request to '{target_model}' with {len(images)} images. Total calculated image tokens (approx): {total_image_tokens}"
382
+ )
383
+
384
+ try:
385
+ response = await self.client.chat.completions.create(**request_params)
386
+ if response.choices and response.choices[0].message.content:
387
+ # Log actual usage if available
388
+ if response.usage:
389
+ logger.info(
390
+ f"OpenAI API Usage: Prompt={response.usage.prompt_tokens}, Completion={response.usage.completion_tokens}, Total={response.usage.total_tokens}"
391
+ )
392
+ return response.choices[0].message.content
393
+ else:
394
+ logger.warning("Received vision response with no content.")
395
+ return ""
396
+ except OpenAIError as e: # Catch specific OpenAI errors
397
+ logger.error(f"OpenAI API error during vision request: {e}")
398
+ return f"I apologize, but I encountered an API error: {e}"
399
+ except Exception as e:
400
+ logger.exception(f"Error in generate_text_with_images: {e}")
401
+ return f"I apologize, but I encountered an unexpected error: {e}"
183
402
 
184
403
  async def parse_structured_output(
185
404
  self,
@@ -68,8 +68,9 @@ class SolanaAgent(SolanaAgentInterface):
68
68
  "flac", "mp3", "mp4", "mpeg", "mpga", "m4a", "ogg", "wav", "webm"
69
69
  ] = "mp4",
70
70
  router: Optional[RoutingInterface] = None,
71
+ images: Optional[List[Union[str, bytes]]] = None,
71
72
  ) -> AsyncGenerator[Union[str, bytes], None]: # pragma: no cover
72
- """Process a user message and return the response stream.
73
+ """Process a user message (text or audio) and optional images, returning the response stream.
73
74
 
74
75
  Args:
75
76
  user_id: User ID
@@ -81,6 +82,7 @@ class SolanaAgent(SolanaAgentInterface):
81
82
  audio_output_format: Audio output format
82
83
  audio_input_format: Audio input format
83
84
  router: Optional routing service for processing
85
+ images: Optional list of image URLs (str) or image bytes.
84
86
 
85
87
  Returns:
86
88
  Async generator yielding response chunks (text strings or audio bytes)
@@ -88,6 +90,7 @@ class SolanaAgent(SolanaAgentInterface):
88
90
  async for chunk in self.query_service.process(
89
91
  user_id=user_id,
90
92
  query=message,
93
+ images=images,
91
94
  output_format=output_format,
92
95
  audio_voice=audio_voice,
93
96
  audio_instructions=audio_instructions,
@@ -34,6 +34,7 @@ class SolanaAgent(ABC):
34
34
  "flac", "mp3", "mp4", "mpeg", "mpga", "m4a", "ogg", "wav", "webm"
35
35
  ] = "mp4",
36
36
  router: Optional[RoutingInterface] = None,
37
+ images: Optional[List[Union[str, bytes]]] = None,
37
38
  ) -> AsyncGenerator[Union[str, bytes], None]:
38
39
  """Process a user message and return the response stream."""
39
40
  pass
@@ -6,6 +6,7 @@ from typing import (
6
6
  Optional,
7
7
  Type,
8
8
  TypeVar,
9
+ Union,
9
10
  )
10
11
 
11
12
  from pydantic import BaseModel
@@ -91,3 +92,14 @@ class LLMProvider(ABC):
91
92
  A list of floats representing the embedding vector.
92
93
  """
93
94
  pass
95
+
96
+ @abstractmethod
97
+ async def generate_text_with_images(
98
+ self,
99
+ prompt: str,
100
+ images: List[Union[str, bytes]],
101
+ system_prompt: str = "",
102
+ detail: Literal["low", "high", "auto"] = "auto",
103
+ ) -> str:
104
+ """Generate text from the language model using images."""
105
+ pass
@@ -44,6 +44,7 @@ class AgentService(ABC):
44
44
  "mp3", "opus", "aac", "flac", "wav", "pcm"
45
45
  ] = "aac",
46
46
  prompt: Optional[str] = None,
47
+ images: Optional[List[Union[str, bytes]]] = None,
47
48
  ) -> AsyncGenerator[Union[str, bytes], None]:
48
49
  """Generate a response from an agent."""
49
50
  pass
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, AsyncGenerator, Dict, Literal, Optional, Union
2
+ from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Union
3
3
 
4
4
  from solana_agent.interfaces.services.routing import RoutingService as RoutingInterface
5
5
 
@@ -34,6 +34,7 @@ class QueryService(ABC):
34
34
  ] = "mp4",
35
35
  prompt: Optional[str] = None,
36
36
  router: Optional[RoutingInterface] = None,
37
+ images: Optional[List[Union[str, bytes]]] = None,
37
38
  ) -> AsyncGenerator[Union[str, bytes], None]:
38
39
  """Process the user request and generate a response."""
39
40
  pass
@@ -341,6 +341,7 @@ class AgentService(AgentServiceInterface):
341
341
  agent_name: str,
342
342
  user_id: str,
343
343
  query: Union[str, bytes],
344
+ images: Optional[List[Union[str, bytes]]] = None,
344
345
  memory_context: str = "",
345
346
  output_format: Literal["text", "audio"] = "text",
346
347
  audio_voice: Literal[
@@ -362,16 +363,13 @@ class AgentService(AgentServiceInterface):
362
363
  prompt: Optional[str] = None,
363
364
  ) -> AsyncGenerator[Union[str, bytes], None]: # pragma: no cover
364
365
  """Generate a response, supporting multiple sequential tool calls with placeholder substitution.
365
-
366
- Text responses are always generated as a single block.
367
- Audio responses always buffer text before TTS.
366
+ Optionally accepts images for vision-capable models.
368
367
  """
369
368
  agent = next((a for a in self.agents if a.name == agent_name), None)
370
369
  if not agent:
371
370
  error_msg = f"Agent '{agent_name}' not found."
372
371
  logger.warning(error_msg)
373
372
  if output_format == "audio":
374
- # Assuming tts returns an async generator
375
373
  async for chunk in self.llm_provider.tts(
376
374
  error_msg,
377
375
  instructions=audio_instructions,
@@ -380,11 +378,11 @@ class AgentService(AgentServiceInterface):
380
378
  ):
381
379
  yield chunk
382
380
  else:
383
- yield error_msg # Yield the single error string
381
+ yield error_msg
384
382
  return
385
383
 
386
384
  logger.debug(
387
- f"Generating response for agent '{agent_name}'. Output format: {output_format}."
385
+ f"Generating response for agent '{agent_name}'. Output format: {output_format}. Images provided: {bool(images)}."
388
386
  )
389
387
 
390
388
  try:
@@ -406,20 +404,40 @@ class AgentService(AgentServiceInterface):
406
404
  start_marker = "[TOOL]"
407
405
 
408
406
  logger.info(f"Generating initial response for agent '{agent_name}'...")
409
- # Call generate_text and await the string result
410
- initial_llm_response_buffer = await self.llm_provider.generate_text(
411
- prompt=str(query),
412
- system_prompt=final_system_prompt,
413
- api_key=self.api_key,
414
- base_url=self.base_url,
415
- model=self.model,
416
- )
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,
429
+ api_key=self.api_key,
430
+ base_url=self.base_url,
431
+ model=self.model,
432
+ )
433
+ # --- END LLM METHOD CHOICE ---
417
434
 
418
435
  # Check for errors returned as string by the adapter
419
- if isinstance(
420
- initial_llm_response_buffer, str
421
- ) and initial_llm_response_buffer.startswith(
422
- "I apologize, but I encountered an error"
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:")
423
441
  ):
424
442
  logger.error(
425
443
  f"LLM provider failed during initial generation: {initial_llm_response_buffer}"
@@ -452,24 +470,23 @@ class AgentService(AgentServiceInterface):
452
470
  # --- Tool Execution Phase (if tools were detected) ---
453
471
  final_response_text = ""
454
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.
455
475
  parsed_calls = self._parse_tool_calls(initial_llm_response_buffer)
456
476
 
457
477
  if parsed_calls:
458
- # --- Execute tools SEQUENTIALLY with Placeholder Substitution ---
459
- executed_tool_results = [] # Store full result dicts
460
- # Map tool names to their string results for substitution
478
+ # ... (existing sequential tool execution with substitution) ...
479
+ executed_tool_results = []
461
480
  tool_results_map: Dict[str, str] = {}
462
-
463
481
  logger.info(
464
482
  f"Executing {len(parsed_calls)} tools sequentially with substitution..."
465
483
  )
466
484
  for i, call in enumerate(parsed_calls):
485
+ # ... (existing substitution logic) ...
467
486
  tool_name_to_exec = call.get("name", "unknown")
468
487
  logger.info(
469
488
  f"Executing tool {i + 1}/{len(parsed_calls)}: {tool_name_to_exec}"
470
489
  )
471
-
472
- # --- Substitute placeholders in parameters ---
473
490
  try:
474
491
  original_params = call.get("parameters", {})
475
492
  substituted_params = self._substitute_placeholders(
@@ -479,20 +496,17 @@ class AgentService(AgentServiceInterface):
479
496
  logger.info(
480
497
  f"Substituted parameters for tool '{tool_name_to_exec}': {substituted_params}"
481
498
  )
482
- call["parameters"] = substituted_params # Update call dict
499
+ call["parameters"] = substituted_params
483
500
  except Exception as sub_err:
484
501
  logger.error(
485
502
  f"Error substituting placeholders for tool '{tool_name_to_exec}': {sub_err}",
486
503
  exc_info=True,
487
504
  )
488
- # Proceed with original params but log the error
489
505
 
490
- # --- Execute the tool ---
506
+ # ... (existing tool execution call) ...
491
507
  try:
492
508
  result = await self._execute_single_tool(agent_name, call)
493
509
  executed_tool_results.append(result)
494
-
495
- # --- Store successful result string for future substitutions ---
496
510
  if result.get("status") == "success":
497
511
  tool_result_str = str(result.get("result", ""))
498
512
  tool_results_map[tool_name_to_exec] = tool_result_str
@@ -500,15 +514,13 @@ class AgentService(AgentServiceInterface):
500
514
  f"Stored result for '{tool_name_to_exec}' (length: {len(tool_result_str)})"
501
515
  )
502
516
  else:
503
- # Store error message as result
504
517
  error_message = result.get("message", "Unknown error")
505
518
  tool_results_map[tool_name_to_exec] = (
506
519
  f"Error: {error_message}"
507
520
  )
508
521
  logger.warning(
509
- f"Tool '{tool_name_to_exec}' failed, storing error message as result."
522
+ f"Tool '{tool_name_to_exec}' failed, storing error message."
510
523
  )
511
-
512
524
  except Exception as tool_exec_err:
513
525
  logger.error(
514
526
  f"Exception during execution of tool {tool_name_to_exec}: {tool_exec_err}",
@@ -521,20 +533,15 @@ class AgentService(AgentServiceInterface):
521
533
  }
522
534
  executed_tool_results.append(error_result)
523
535
  tool_results_map[tool_name_to_exec] = (
524
- f"Error: {str(tool_exec_err)}" # Store error
536
+ f"Error: {str(tool_exec_err)}"
525
537
  )
526
538
 
527
539
  logger.info("Sequential tool execution with substitution complete.")
528
- # --- End Sequential Execution ---
529
540
 
530
- # Format results for the follow-up prompt (use executed_tool_results)
541
+ # ... (existing formatting of tool results) ...
531
542
  tool_results_text_parts = []
532
- for i, result in enumerate(
533
- executed_tool_results
534
- ): # Use the collected results
535
- tool_name = result.get(
536
- "tool_name", "unknown"
537
- ) # Name should be in the result dict now
543
+ for i, result in enumerate(executed_tool_results):
544
+ tool_name = result.get("tool_name", "unknown")
538
545
  if (
539
546
  isinstance(result, Exception)
540
547
  or result.get("status") == "error"
@@ -556,8 +563,12 @@ class AgentService(AgentServiceInterface):
556
563
  tool_results_context = "\n\n".join(tool_results_text_parts)
557
564
 
558
565
  # --- Generate Final Response using Tool Results (No Streaming) ---
559
- follow_up_prompt = f"Original Query: {str(query)}\n\nRESULTS FROM TOOL CALLS:\n{tool_results_context}\n\nBased on the original query and the tool results, please provide the final response to the user."
560
- # Rebuild system prompt
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."
561
572
  follow_up_system_prompt_parts = [
562
573
  self.get_agent_system_prompt(agent_name)
563
574
  ]
@@ -571,7 +582,7 @@ class AgentService(AgentServiceInterface):
571
582
  f"\nORIGINAL ADDITIONAL PROMPT:\n{prompt}"
572
583
  )
573
584
  follow_up_system_prompt_parts.append(
574
- f"\nCONTEXT: You previously decided to run {len(parsed_calls)} tool(s) sequentially to answer the query. The results are provided above."
585
+ f"\nCONTEXT: You previously decided to run {len(parsed_calls)} tool(s) sequentially. The results are provided above."
575
586
  )
576
587
  final_follow_up_system_prompt = "\n\n".join(
577
588
  filter(None, follow_up_system_prompt_parts)
@@ -580,25 +591,25 @@ class AgentService(AgentServiceInterface):
580
591
  logger.info(
581
592
  "Generating final response incorporating tool results..."
582
593
  )
583
- # Call generate_text and await the string result
594
+ # Use standard text generation for the final synthesis
584
595
  synthesized_response_buffer = await self.llm_provider.generate_text(
585
596
  prompt=follow_up_prompt,
586
597
  system_prompt=final_follow_up_system_prompt,
587
598
  api_key=self.api_key,
588
599
  base_url=self.base_url,
589
- model=self.model,
600
+ model=self.model
601
+ or self.llm_provider.text_model, # Use text model for synthesis
590
602
  )
591
603
 
592
- # Check for errors returned as string by the adapter
593
- if isinstance(
594
- synthesized_response_buffer, str
595
- ) and synthesized_response_buffer.startswith(
596
- "I apologize, but I encountered an error"
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:")
597
609
  ):
598
610
  logger.error(
599
611
  f"LLM provider failed during final generation: {synthesized_response_buffer}"
600
612
  )
601
- # Yield the error and exit
602
613
  if output_format == "audio":
603
614
  async for chunk in self.llm_provider.tts(
604
615
  synthesized_response_buffer,
@@ -617,13 +628,11 @@ class AgentService(AgentServiceInterface):
617
628
  )
618
629
 
619
630
  else:
620
- # Tools detected but parsing failed
621
631
  logger.warning(
622
632
  "Tool markers detected, but no valid tool calls parsed. Treating initial response as final."
623
633
  )
624
634
  final_response_text = initial_llm_response_buffer
625
635
  else:
626
- # No tools detected
627
636
  final_response_text = initial_llm_response_buffer
628
637
  logger.info("No tools detected. Using initial response as final.")
629
638
 
@@ -641,7 +650,7 @@ class AgentService(AgentServiceInterface):
641
650
  )
642
651
  except Exception as e:
643
652
  logger.error(
644
- f"Error applying output guardrail {guardrail.__class__.__name__} to final text: {e}"
653
+ f"Error applying output guardrail {guardrail.__class__.__name__}: {e}"
645
654
  )
646
655
  if len(processed_final_text) != original_len:
647
656
  logger.info(
@@ -651,14 +660,12 @@ class AgentService(AgentServiceInterface):
651
660
  self.last_text_response = processed_final_text
652
661
 
653
662
  if output_format == "text":
654
- # Yield the single final string
655
663
  if processed_final_text:
656
664
  yield processed_final_text
657
665
  else:
658
666
  logger.warning("Final processed text was empty.")
659
667
  yield ""
660
668
  elif output_format == "audio":
661
- # TTS still needs a generator
662
669
  text_for_tts = processed_final_text
663
670
  cleaned_audio_buffer = self._clean_for_audio(text_for_tts)
664
671
  logger.info(
@@ -22,9 +22,8 @@ from solana_agent.interfaces.services.knowledge_base import (
22
22
  )
23
23
  from solana_agent.interfaces.guardrails.guardrails import (
24
24
  InputGuardrail,
25
- ) # <-- Import InputGuardrail
25
+ )
26
26
 
27
- # Service imports (assuming AgentService is the concrete implementation)
28
27
  from solana_agent.services.agent import AgentService
29
28
  from solana_agent.services.routing import RoutingService
30
29
 
@@ -58,12 +57,13 @@ class QueryService(QueryServiceInterface):
58
57
  self.memory_provider = memory_provider
59
58
  self.knowledge_base = knowledge_base
60
59
  self.kb_results_count = kb_results_count
61
- self.input_guardrails = input_guardrails or [] # <-- Store guardrails
60
+ self.input_guardrails = input_guardrails or []
62
61
 
63
62
  async def process(
64
63
  self,
65
64
  user_id: str,
66
65
  query: Union[str, bytes],
66
+ images: Optional[List[Union[str, bytes]]] = None,
67
67
  output_format: Literal["text", "audio"] = "text",
68
68
  audio_voice: Literal[
69
69
  "alloy",
@@ -92,6 +92,7 @@ class QueryService(QueryServiceInterface):
92
92
  Args:
93
93
  user_id: User ID
94
94
  query: Text query or audio bytes
95
+ images: Optional list of image URLs (str) or image bytes.
95
96
  output_format: Response format ("text" or "audio")
96
97
  audio_voice: Voice for TTS (text-to-speech)
97
98
  audio_instructions: Audio voice instructions
@@ -143,7 +144,14 @@ class QueryService(QueryServiceInterface):
143
144
  # --- End Apply Input Guardrails ---
144
145
 
145
146
  # --- 3. Handle Simple Greetings ---
146
- if user_text.strip().lower() in ["test", "hello", "hi", "hey", "ping"]:
147
+ # Simple greetings typically don't involve images
148
+ if not images and user_text.strip().lower() in [
149
+ "test",
150
+ "hello",
151
+ "hi",
152
+ "hey",
153
+ "ping",
154
+ ]:
147
155
  response = "Hello! How can I help you today?"
148
156
  logger.info("Handling simple greeting.")
149
157
  if output_format == "audio":
@@ -201,7 +209,7 @@ class QueryService(QueryServiceInterface):
201
209
  # --- 6. Route Query ---
202
210
  agent_name = "default" # Fallback agent
203
211
  try:
204
- # Use processed user_text for routing
212
+ # Use processed user_text for routing (images generally don't affect routing logic here)
205
213
  if router:
206
214
  agent_name = await router.route_query(user_text)
207
215
  else:
@@ -225,12 +233,13 @@ class QueryService(QueryServiceInterface):
225
233
  logger.debug(f"Combined context length: {len(combined_context)}")
226
234
 
227
235
  # --- 8. Generate Response ---
228
- # Pass the processed user_text to the agent service
236
+ # Pass the processed user_text and images to the agent service
229
237
  if output_format == "audio":
230
238
  async for audio_chunk in self.agent_service.generate_response(
231
239
  agent_name=agent_name,
232
240
  user_id=user_id,
233
241
  query=user_text, # Pass processed text
242
+ images=images,
234
243
  memory_context=combined_context,
235
244
  output_format="audio",
236
245
  audio_voice=audio_voice,
@@ -241,10 +250,11 @@ class QueryService(QueryServiceInterface):
241
250
  yield audio_chunk
242
251
 
243
252
  # Store conversation using processed user_text
253
+ # Note: Storing images in history is not directly supported by current memory provider interface
244
254
  if self.memory_provider:
245
255
  await self._store_conversation(
246
256
  user_id=user_id,
247
- user_message=user_text,
257
+ user_message=user_text, # Store only text part of user query
248
258
  assistant_message=self.agent_service.last_text_response,
249
259
  )
250
260
  else:
@@ -253,6 +263,7 @@ class QueryService(QueryServiceInterface):
253
263
  agent_name=agent_name,
254
264
  user_id=user_id,
255
265
  query=user_text, # Pass processed text
266
+ images=images, # <-- Pass images
256
267
  memory_context=combined_context,
257
268
  output_format="text",
258
269
  prompt=prompt,
@@ -261,10 +272,11 @@ class QueryService(QueryServiceInterface):
261
272
  full_text_response += chunk
262
273
 
263
274
  # Store conversation using processed user_text
275
+ # Note: Storing images in history is not directly supported by current memory provider interface
264
276
  if self.memory_provider and full_text_response:
265
277
  await self._store_conversation(
266
278
  user_id=user_id,
267
- user_message=user_text,
279
+ user_message=user_text, # Store only text part of user query
268
280
  assistant_message=full_text_response,
269
281
  )
270
282
 
@@ -370,11 +382,15 @@ class QueryService(QueryServiceInterface):
370
382
  if conv.get("timestamp")
371
383
  else None
372
384
  )
385
+ # Assuming the stored format matches what _store_conversation saves
386
+ # (which currently only stores text messages)
373
387
  formatted_conversations.append(
374
388
  {
375
389
  "id": str(conv.get("_id")),
376
- "user_message": conv.get("user_message"),
377
- "assistant_message": conv.get("assistant_message"),
390
+ "user_message": conv.get("user_message"), # Or how it's stored
391
+ "assistant_message": conv.get(
392
+ "assistant_message"
393
+ ), # Or how it's stored
378
394
  "timestamp": timestamp,
379
395
  }
380
396
  )
@@ -413,11 +429,13 @@ class QueryService(QueryServiceInterface):
413
429
 
414
430
  Args:
415
431
  user_id: User ID
416
- user_message: User message (potentially processed by input guardrails)
432
+ user_message: User message (text part, potentially processed by input guardrails)
417
433
  assistant_message: Assistant message (potentially processed by output guardrails)
418
434
  """
419
435
  if self.memory_provider:
420
436
  try:
437
+ # Store only the text parts for now, as memory provider interface
438
+ # doesn't explicitly handle image data storage in history.
421
439
  await self.memory_provider.store(
422
440
  user_id,
423
441
  [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: solana-agent
3
- Version: 28.2.0
3
+ Version: 28.3.0
4
4
  Summary: AI Agents for Solana
5
5
  License: MIT
6
6
  Keywords: solana,solana ai,solana agent,ai,ai agent,ai agents
@@ -15,10 +15,11 @@ Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
17
  Requires-Dist: instructor (>=1.7.9,<2.0.0)
18
- Requires-Dist: llama-index-core (>=0.12.30,<0.13.0)
18
+ Requires-Dist: llama-index-core (>=0.12.32,<0.13.0)
19
19
  Requires-Dist: llama-index-embeddings-openai (>=0.3.1,<0.4.0)
20
20
  Requires-Dist: logfire (>=3.14.0,<4.0.0)
21
21
  Requires-Dist: openai (>=1.75.0,<2.0.0)
22
+ Requires-Dist: pillow (>=11.2.1,<12.0.0)
22
23
  Requires-Dist: pinecone (>=6.0.2,<7.0.0)
23
24
  Requires-Dist: pydantic (>=2)
24
25
  Requires-Dist: pymongo (>=4.12.0,<5.0.0)
@@ -26,7 +27,7 @@ Requires-Dist: pypdf (>=5.4.0,<6.0.0)
26
27
  Requires-Dist: rich (>=13)
27
28
  Requires-Dist: scrubadub (>=2.0.1,<3.0.0)
28
29
  Requires-Dist: typer (>=0.15.2,<0.16.0)
29
- Requires-Dist: zep-cloud (>=2.10.1,<3.0.0)
30
+ Requires-Dist: zep-cloud (>=2.10.2,<3.0.0)
30
31
  Project-URL: Documentation, https://docs.solana-agent.com
31
32
  Project-URL: Homepage, https://solana-agent.com
32
33
  Project-URL: Repository, https://github.com/truemagic-coder/solana-agent
@@ -56,7 +57,7 @@ Build your AI agents in three lines of code!
56
57
  * Fast Responses
57
58
  * Solana Ecosystem Integration
58
59
  * Multi-Agent Swarm
59
- * Multi-Modal Streaming (Text & Audio)
60
+ * Multi-Modal (Images & Audio & Text)
60
61
  * Conversational Memory & History
61
62
  * Internet Search
62
63
  * Intelligent Routing
@@ -80,7 +81,7 @@ Build your AI agents in three lines of code!
80
81
  * MCP tool usage with first-class support for [Zapier](https://zapier.com/mcp)
81
82
  * Integrated observability and tracing via [Pydantic Logfire](https://pydantic.dev/logfire)
82
83
  * Designed for a multi-agent swarm
83
- * Seamless text and audio streaming with real-time multi-modal processing
84
+ * Seamless streaming with real-time multi-modal processing of text, audio, and images
84
85
  * Persistent memory that preserves context across all agent interactions
85
86
  * Quick Internet search to answer users' queries
86
87
  * Streamlined message history for all agent interactions
@@ -286,6 +287,42 @@ async for response in solana_agent.process("user123", audio_content, audio_input
286
287
  print(response, end="")
287
288
  ```
288
289
 
290
+ ### Image/Text Streaming
291
+
292
+ ```python
293
+ from solana_agent import SolanaAgent
294
+
295
+ config = {
296
+ "openai": {
297
+ "api_key": "your-openai-api-key",
298
+ },
299
+ "agents": [
300
+ {
301
+ "name": "vision_expert",
302
+ "instructions": "You are an expert at analyzing images and answering questions about them.",
303
+ "specialization": "Image analysis",
304
+ }
305
+ ],
306
+ }
307
+
308
+ solana_agent = SolanaAgent(config=config)
309
+
310
+ # Example with an image URL
311
+ image_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
312
+
313
+ # Example reading image bytes from a file
314
+ image_bytes = await image_file.read()
315
+
316
+ # You can mix URLs and bytes in the list
317
+ images_to_process = [
318
+ image_url,
319
+ image_bytes,
320
+ ]
321
+
322
+ async for response in solana_agent.process("user123", "What is in this image? Describe the scene.", images=images_to_process):
323
+ print(response, end="")
324
+ ```
325
+
289
326
  ### Command Line Interface (CLI)
290
327
 
291
328
  Solana Agent includes a command-line interface (CLI) for text-based chat using a configuration file.
@@ -585,7 +622,7 @@ config = {
585
622
  "rpc_url": "your-solana-rpc-url",
586
623
  },
587
624
  },
588
- "ai_agents": [
625
+ "agents": [
589
626
  {
590
627
  "name": "solana_expert",
591
628
  "instructions": "You are an expert Solana blockchain assistant. You always use the Solana tool to perform actions on the Solana blockchain.",
@@ -1,11 +1,11 @@
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=0KWIa6kaFbUFvtKUzuV_0p0RFlPPGKrDVIEU2McVY3k,2734
4
- solana_agent/adapters/openai_adapter.py,sha256=rSa1yYYIpr5ES3Zw0DZm7iGp59p0WYBQXH0EQ1UDAB4,13594
4
+ solana_agent/adapters/openai_adapter.py,sha256=XnocNAV1nJGcjpRgOyMXnyDQSU8HvTx9zmb4pWtSb58,23432
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
8
- solana_agent/client/solana_agent.py,sha256=jUGWxYJL9ZWxGsVX9C6FrRQyX7r6Cep0ijcfm7cbkJI,10098
8
+ solana_agent/client/solana_agent.py,sha256=-oVH_xGS9Al3csQ-IK9jlQhheutbfm69QBXmAc8Hmkw,10289
9
9
  solana_agent/domains/__init__.py,sha256=HiC94wVPRy-QDJSSRywCRrhrFfTBeHjfi5z-QfZv46U,168
10
10
  solana_agent/domains/agent.py,sha256=3Q1wg4eIul0CPpaYBOjEthKTfcdhf1SAiWc2R-IMGO8,2561
11
11
  solana_agent/domains/routing.py,sha256=1yR4IswGcmREGgbOOI6TKCfuM7gYGOhQjLkBqnZ-rNo,582
@@ -13,16 +13,16 @@ solana_agent/factories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
13
13
  solana_agent/factories/agent_factory.py,sha256=kduhtCMAxiPmCW_wx-hGGlhehRRGt4OBKY8r-R-LZnI,13246
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
- solana_agent/interfaces/client/client.py,sha256=hsvaQiQdz3MLMNc77oD6ocvvnyl7Ez2n087ptFDA19M,3687
16
+ solana_agent/interfaces/client/client.py,sha256=RURf6W3dSK4mlQ_8ZTLKmh5TIu5QNpXphp_cye5yhPE,3745
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=Wxn0qXIk7BmpI0FBrhjJVV6DmsfLUpUauZR-pE3brz8,2395
20
+ solana_agent/interfaces/providers/llm.py,sha256=FbK6HNMBOIONPE-ljPRElkO2fmFbkzWEo4KuYfcDEFE,2727
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
- solana_agent/interfaces/services/agent.py,sha256=YsxyvBPK3ygBEStLyL4BwmIl84NMrV3dK0PlwCFoyq0,2094
23
+ solana_agent/interfaces/services/agent.py,sha256=MgLudTwzCzzzSR6PsVTB-w5rhGDHB5B81TGjo2z3G-A,2152
24
24
  solana_agent/interfaces/services/knowledge_base.py,sha256=HsU4fAMc_oOUCqCX2z76_IbAtbTNTyvffHZ49J0ynSQ,2092
25
- solana_agent/interfaces/services/query.py,sha256=QfpBA3hrv8pQdNbK05hbu3Vh3-53F46IFcoUjwj5J9w,1568
25
+ solana_agent/interfaces/services/query.py,sha256=eLMMwc8hwHHjxFxlvVvkZfoQi8cSgQycWJbYAVphl9E,1632
26
26
  solana_agent/interfaces/services/routing.py,sha256=Qbn3-DQGVSQKaegHDekSFmn_XCklA0H2f0XUx9-o3wA,367
27
27
  solana_agent/plugins/__init__.py,sha256=coZdgJKq1ExOaj6qB810i3rEhbjdVlrkN76ozt_Ojgo,193
28
28
  solana_agent/plugins/manager.py,sha256=mO_dKSVJ8GToD3wZflMcpKDEBXRoaaMRtY267HENCI0,5542
@@ -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=e-27ju6wmurxSxULzr_uDHxxdnvw8KrJt9NWyvAz-i4,7684
34
34
  solana_agent/services/__init__.py,sha256=iko0c2MlF8b_SA_nuBGFllr2E3g_JowOrOzGcnU9tkA,162
35
- solana_agent/services/agent.py,sha256=9FB1Tj7v8JwJVVmZwK8IOSrBbgbV4iFZOtFHzw3gcEs,41780
35
+ solana_agent/services/agent.py,sha256=QoeQq_OEWyLdBS0FPa-lXm5qiE0RnRfrCKiFTfOSGE0,42369
36
36
  solana_agent/services/knowledge_base.py,sha256=D4QNGC3Z8E7iX-CEGpRks0lW4wWJt-WorO3J8mu6ayU,35318
37
- solana_agent/services/query.py,sha256=bAoUfe_2EBVEVeh99-2E9KZ0zaHUzf7Lqel3rlHyNX8,17459
37
+ solana_agent/services/query.py,sha256=ENUfs4WSTpODMRXppDVW-Y3li9jYn8pOfQIHIPerUdQ,18498
38
38
  solana_agent/services/routing.py,sha256=C5Ku4t9TqvY7S8wlUPMTC04HCrT4Ib3E8Q8yX0lVU_s,7137
39
- solana_agent-28.2.0.dist-info/LICENSE,sha256=BnSRc-NSFuyF2s496l_4EyrwAP6YimvxWcjPiJ0J7g4,1057
40
- solana_agent-28.2.0.dist-info/METADATA,sha256=b_KhpeB3McBc0uAu63yjkJn4g6_FG_EyO4-uMPQjYno,28286
41
- solana_agent-28.2.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
42
- solana_agent-28.2.0.dist-info/entry_points.txt,sha256=-AuT_mfqk8dlZ0pHuAjx1ouAWpTRjpqvEUa6YV3lmc0,53
43
- solana_agent-28.2.0.dist-info/RECORD,,
39
+ solana_agent-28.3.0.dist-info/LICENSE,sha256=BnSRc-NSFuyF2s496l_4EyrwAP6YimvxWcjPiJ0J7g4,1057
40
+ solana_agent-28.3.0.dist-info/METADATA,sha256=d0bjKGS6LRao_sJXWyCizuoN4aoGNRBoQDXht8HcqGQ,29305
41
+ solana_agent-28.3.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
42
+ solana_agent-28.3.0.dist-info/entry_points.txt,sha256=-AuT_mfqk8dlZ0pHuAjx1ouAWpTRjpqvEUa6YV3lmc0,53
43
+ solana_agent-28.3.0.dist-info/RECORD,,