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.
- solana_agent/adapters/openai_adapter.py +241 -22
- solana_agent/client/solana_agent.py +4 -1
- solana_agent/interfaces/client/client.py +1 -0
- solana_agent/interfaces/providers/llm.py +12 -0
- solana_agent/interfaces/services/agent.py +1 -0
- solana_agent/interfaces/services/query.py +2 -1
- solana_agent/services/agent.py +64 -57
- solana_agent/services/query.py +29 -11
- {solana_agent-28.2.0.dist-info → solana_agent-28.3.0.dist-info}/METADATA +43 -6
- {solana_agent-28.2.0.dist-info → solana_agent-28.3.0.dist-info}/RECORD +13 -13
- {solana_agent-28.2.0.dist-info → solana_agent-28.3.0.dist-info}/LICENSE +0 -0
- {solana_agent-28.2.0.dist-info → solana_agent-28.3.0.dist-info}/WHEEL +0 -0
- {solana_agent-28.2.0.dist-info → solana_agent-28.3.0.dist-info}/entry_points.txt +0 -0
@@ -5,9 +5,22 @@ These adapters implement the LLMProvider interface for different LLM services.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import logging
|
8
|
-
|
9
|
-
|
10
|
-
|
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.")
|
66
|
+
logger.info("Logfire configured successfully.")
|
43
67
|
except Exception as e:
|
44
|
-
logger.error(f"Failed to configure Logfire: {e}")
|
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
|
-
|
171
|
-
return full_text # Return the complete string
|
189
|
+
return response.choices[0].message.content
|
172
190
|
else:
|
173
|
-
logger.warning(
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
182
|
-
|
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
|
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
|
solana_agent/services/agent.py
CHANGED
@@ -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
|
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
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
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
|
421
|
-
|
422
|
-
|
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
|
-
#
|
459
|
-
executed_tool_results = []
|
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
|
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
|
-
#
|
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
|
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)}"
|
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
|
-
#
|
541
|
+
# ... (existing formatting of tool results) ...
|
531
542
|
tool_results_text_parts = []
|
532
|
-
for i, result in enumerate(
|
533
|
-
|
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
|
-
|
560
|
-
|
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
|
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
|
-
#
|
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
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
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__}
|
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(
|
solana_agent/services/query.py
CHANGED
@@ -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
|
-
)
|
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 []
|
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
|
-
|
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(
|
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.
|
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.
|
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.
|
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
|
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
|
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
|
-
"
|
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=
|
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
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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.
|
40
|
-
solana_agent-28.
|
41
|
-
solana_agent-28.
|
42
|
-
solana_agent-28.
|
43
|
-
solana_agent-28.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|