solana-agent 20.1.2__py3-none-any.whl → 31.4.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.
Files changed (45) hide show
  1. solana_agent/__init__.py +10 -5
  2. solana_agent/adapters/ffmpeg_transcoder.py +375 -0
  3. solana_agent/adapters/mongodb_adapter.py +15 -2
  4. solana_agent/adapters/openai_adapter.py +679 -0
  5. solana_agent/adapters/openai_realtime_ws.py +1813 -0
  6. solana_agent/adapters/pinecone_adapter.py +543 -0
  7. solana_agent/cli.py +128 -0
  8. solana_agent/client/solana_agent.py +180 -20
  9. solana_agent/domains/agent.py +13 -13
  10. solana_agent/domains/routing.py +18 -8
  11. solana_agent/factories/agent_factory.py +239 -38
  12. solana_agent/guardrails/pii.py +107 -0
  13. solana_agent/interfaces/client/client.py +95 -12
  14. solana_agent/interfaces/guardrails/guardrails.py +26 -0
  15. solana_agent/interfaces/plugins/plugins.py +2 -1
  16. solana_agent/interfaces/providers/__init__.py +0 -0
  17. solana_agent/interfaces/providers/audio.py +40 -0
  18. solana_agent/interfaces/providers/data_storage.py +9 -2
  19. solana_agent/interfaces/providers/llm.py +86 -9
  20. solana_agent/interfaces/providers/memory.py +13 -1
  21. solana_agent/interfaces/providers/realtime.py +212 -0
  22. solana_agent/interfaces/providers/vector_storage.py +53 -0
  23. solana_agent/interfaces/services/agent.py +27 -12
  24. solana_agent/interfaces/services/knowledge_base.py +59 -0
  25. solana_agent/interfaces/services/query.py +41 -8
  26. solana_agent/interfaces/services/routing.py +0 -1
  27. solana_agent/plugins/manager.py +37 -16
  28. solana_agent/plugins/registry.py +34 -19
  29. solana_agent/plugins/tools/__init__.py +0 -5
  30. solana_agent/plugins/tools/auto_tool.py +1 -0
  31. solana_agent/repositories/memory.py +332 -111
  32. solana_agent/services/__init__.py +1 -1
  33. solana_agent/services/agent.py +390 -241
  34. solana_agent/services/knowledge_base.py +768 -0
  35. solana_agent/services/query.py +1858 -153
  36. solana_agent/services/realtime.py +626 -0
  37. solana_agent/services/routing.py +104 -51
  38. solana_agent-31.4.0.dist-info/METADATA +1070 -0
  39. solana_agent-31.4.0.dist-info/RECORD +49 -0
  40. {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info}/WHEEL +1 -1
  41. solana_agent-31.4.0.dist-info/entry_points.txt +3 -0
  42. solana_agent/adapters/llm_adapter.py +0 -160
  43. solana_agent-20.1.2.dist-info/METADATA +0 -464
  44. solana_agent-20.1.2.dist-info/RECORD +0 -35
  45. {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info/licenses}/LICENSE +0 -0
solana_agent/__init__.py CHANGED
@@ -5,8 +5,6 @@ This package provides a modular framework for building AI agent systems with
5
5
  multiple specialized agents, memory management, and conversation routing.
6
6
  """
7
7
 
8
- __version__ = "14.0.0" # Update with your actual version
9
-
10
8
  # Client interface (main entry point)
11
9
  from solana_agent.client.solana_agent import SolanaAgent
12
10
 
@@ -16,18 +14,25 @@ from solana_agent.factories.agent_factory import SolanaAgentFactory
16
14
  # Useful tools and utilities
17
15
  from solana_agent.plugins.manager import PluginManager
18
16
  from solana_agent.plugins.registry import ToolRegistry
19
- from solana_agent.plugins.tools import AutoTool
17
+ from solana_agent.plugins.tools.auto_tool import AutoTool
18
+ from solana_agent.interfaces.plugins.plugins import Tool
19
+ from solana_agent.interfaces.guardrails.guardrails import (
20
+ InputGuardrail,
21
+ OutputGuardrail,
22
+ )
20
23
 
21
24
  # Package metadata
22
25
  __all__ = [
23
26
  # Main client interfaces
24
27
  "SolanaAgent",
25
-
26
28
  # Factories
27
29
  "SolanaAgentFactory",
28
-
29
30
  # Tools
30
31
  "PluginManager",
31
32
  "ToolRegistry",
32
33
  "AutoTool",
34
+ "Tool",
35
+ # Guardrails
36
+ "InputGuardrail",
37
+ "OutputGuardrail",
33
38
  ]
@@ -0,0 +1,375 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import logging
6
+ from typing import List, AsyncGenerator
7
+ import tempfile
8
+ import os
9
+
10
+ from solana_agent.interfaces.providers.audio import AudioTranscoder
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class FFmpegTranscoder(AudioTranscoder):
16
+ """FFmpeg-based transcoder. Requires 'ffmpeg' binary in PATH.
17
+
18
+ This uses subprocess to stream bytes through ffmpeg for encode/decode.
19
+ """
20
+
21
+ async def _run_ffmpeg(
22
+ self, args: List[str], data: bytes
23
+ ) -> bytes: # pragma: no cover
24
+ logger.info("FFmpeg: starting process args=%s, input_len=%d", args, len(data))
25
+ proc = await asyncio.create_subprocess_exec(
26
+ "ffmpeg",
27
+ *args,
28
+ stdin=asyncio.subprocess.PIPE,
29
+ stdout=asyncio.subprocess.PIPE,
30
+ stderr=asyncio.subprocess.PIPE,
31
+ )
32
+ stdout, stderr = await proc.communicate(input=data)
33
+ if proc.returncode != 0:
34
+ err = (stderr or b"").decode("utf-8", errors="ignore")
35
+ logger.error("FFmpeg failed (code=%s): %s", proc.returncode, err[:2000])
36
+ raise RuntimeError("ffmpeg failed to transcode audio")
37
+ logger.info("FFmpeg: finished successfully, output_len=%d", len(stdout or b""))
38
+ if stderr:
39
+ logger.debug(
40
+ "FFmpeg stderr: %s", stderr.decode("utf-8", errors="ignore")[:2000]
41
+ )
42
+ return stdout
43
+
44
+ async def to_pcm16( # pragma: no cover
45
+ self, audio_bytes: bytes, input_mime: str, rate_hz: int
46
+ ) -> bytes:
47
+ """Decode compressed audio to mono PCM16LE at rate_hz."""
48
+ logger.info(
49
+ "Transcode to PCM16: input_mime=%s, rate_hz=%d, input_len=%d",
50
+ input_mime,
51
+ rate_hz,
52
+ len(audio_bytes),
53
+ )
54
+ # iOS-recorded MP4/M4A often requires a seekable input for reliable demuxing.
55
+ # Decode from a temporary file instead of stdin for MP4/M4A.
56
+ if input_mime in ("audio/mp4", "audio/m4a"):
57
+ suffix = ".m4a" if input_mime == "audio/m4a" else ".mp4"
58
+ tmp_path = None
59
+ try:
60
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
61
+ tmp_path = tf.name
62
+ tf.write(audio_bytes)
63
+ args = [
64
+ "-hide_banner",
65
+ "-loglevel",
66
+ "error",
67
+ "-i",
68
+ tmp_path,
69
+ "-vn", # ignore any video tracks
70
+ "-acodec",
71
+ "pcm_s16le",
72
+ "-ac",
73
+ "1",
74
+ "-ar",
75
+ str(rate_hz),
76
+ "-f",
77
+ "s16le",
78
+ "pipe:1",
79
+ ]
80
+ out = await self._run_ffmpeg(args, b"")
81
+ logger.info(
82
+ "Transcoded (MP4/M4A temp-file) to PCM16: output_len=%d", len(out)
83
+ )
84
+ return out
85
+ finally:
86
+ if tmp_path:
87
+ with contextlib.suppress(Exception):
88
+ os.remove(tmp_path)
89
+
90
+ # For other formats, prefer a format hint when helpful and decode from stdin.
91
+ hinted_format = None
92
+ if input_mime in ("audio/aac",):
93
+ # Raw AAC is typically in ADTS stream format
94
+ hinted_format = "adts"
95
+ elif input_mime in ("audio/ogg", "audio/webm"):
96
+ hinted_format = None # container detection is decent here
97
+ elif input_mime in ("audio/wav", "audio/x-wav"):
98
+ hinted_format = "wav"
99
+
100
+ args = [
101
+ "-hide_banner",
102
+ "-loglevel",
103
+ "error",
104
+ ]
105
+ if hinted_format:
106
+ args += ["-f", hinted_format]
107
+ args += [
108
+ "-i",
109
+ "pipe:0",
110
+ "-acodec",
111
+ "pcm_s16le",
112
+ "-ac",
113
+ "1",
114
+ "-ar",
115
+ str(rate_hz),
116
+ "-f",
117
+ "s16le",
118
+ "pipe:1",
119
+ ]
120
+ out = await self._run_ffmpeg(args, audio_bytes)
121
+ logger.info("Transcoded to PCM16: output_len=%d", len(out))
122
+ return out
123
+
124
+ async def from_pcm16( # pragma: no cover
125
+ self, pcm16_bytes: bytes, output_mime: str, rate_hz: int
126
+ ) -> bytes:
127
+ """Encode PCM16LE to desired format (AAC ADTS, fragmented MP4, or MP3)."""
128
+ logger.info(
129
+ "Encode from PCM16: output_mime=%s, rate_hz=%d, input_len=%d",
130
+ output_mime,
131
+ rate_hz,
132
+ len(pcm16_bytes),
133
+ )
134
+
135
+ if output_mime in ("audio/mpeg", "audio/mp3"):
136
+ # Encode to MP3 (often better streaming compatibility on mobile)
137
+ args = [
138
+ "-hide_banner",
139
+ "-loglevel",
140
+ "error",
141
+ "-f",
142
+ "s16le",
143
+ "-ac",
144
+ "1",
145
+ "-ar",
146
+ str(rate_hz),
147
+ "-i",
148
+ "pipe:0",
149
+ "-c:a",
150
+ "libmp3lame",
151
+ "-b:a",
152
+ "128k",
153
+ "-f",
154
+ "mp3",
155
+ "pipe:1",
156
+ ]
157
+ out = await self._run_ffmpeg(args, pcm16_bytes)
158
+ logger.info(
159
+ "Encoded from PCM16 to %s: output_len=%d", output_mime, len(out)
160
+ )
161
+ return out
162
+
163
+ if output_mime in ("audio/aac",):
164
+ # Encode to AAC in ADTS stream; good for streaming over sockets/HTTP chunked
165
+ args = [
166
+ "-hide_banner",
167
+ "-loglevel",
168
+ "error",
169
+ "-f",
170
+ "s16le",
171
+ "-ac",
172
+ "1",
173
+ "-ar",
174
+ str(rate_hz),
175
+ "-i",
176
+ "pipe:0",
177
+ "-c:a",
178
+ "aac",
179
+ "-b:a",
180
+ "96k",
181
+ "-f",
182
+ "adts",
183
+ "pipe:1",
184
+ ]
185
+ out = await self._run_ffmpeg(args, pcm16_bytes)
186
+ logger.info(
187
+ "Encoded from PCM16 to %s: output_len=%d", output_mime, len(out)
188
+ )
189
+ return out
190
+
191
+ if output_mime in ("audio/mp4", "audio/m4a"):
192
+ # Encode to fragmented MP4 (fMP4) with AAC for better iOS compatibility
193
+ # For streaming, write an initial moov and fragment over stdout.
194
+ args = [
195
+ "-hide_banner",
196
+ "-loglevel",
197
+ "error",
198
+ "-f",
199
+ "s16le",
200
+ "-ac",
201
+ "1",
202
+ "-ar",
203
+ str(rate_hz),
204
+ "-i",
205
+ "pipe:0",
206
+ "-c:a",
207
+ "aac",
208
+ "-b:a",
209
+ "96k",
210
+ "-movflags",
211
+ "+frag_keyframe+empty_moov",
212
+ "-f",
213
+ "mp4",
214
+ "pipe:1",
215
+ ]
216
+ out = await self._run_ffmpeg(args, pcm16_bytes)
217
+ logger.info(
218
+ "Encoded from PCM16 to %s (fMP4): output_len=%d", output_mime, len(out)
219
+ )
220
+ return out
221
+
222
+ # Default: passthrough
223
+ logger.info("Encode passthrough (no change), output_len=%d", len(pcm16_bytes))
224
+ return pcm16_bytes
225
+
226
+ async def stream_from_pcm16( # pragma: no cover
227
+ self,
228
+ pcm_iter: AsyncGenerator[bytes, None],
229
+ output_mime: str,
230
+ rate_hz: int,
231
+ read_chunk_size: int = 4096,
232
+ ) -> AsyncGenerator[bytes, None]:
233
+ """Start a single continuous encoder and stream encoded audio chunks.
234
+
235
+ - Launches one ffmpeg subprocess for the entire response.
236
+ - Feeds PCM16LE mono bytes from pcm_iter into stdin.
237
+ - Yields encoded bytes from stdout as they become available.
238
+ """
239
+ if output_mime in ("audio/mpeg", "audio/mp3"):
240
+ args = [
241
+ "-hide_banner",
242
+ "-loglevel",
243
+ "error",
244
+ "-f",
245
+ "s16le",
246
+ "-ac",
247
+ "1",
248
+ "-ar",
249
+ str(rate_hz),
250
+ "-i",
251
+ "pipe:0",
252
+ "-c:a",
253
+ "libmp3lame",
254
+ "-b:a",
255
+ "128k",
256
+ "-f",
257
+ "mp3",
258
+ "pipe:1",
259
+ ]
260
+ elif output_mime in ("audio/aac",):
261
+ args = [
262
+ "-hide_banner",
263
+ "-loglevel",
264
+ "error",
265
+ "-f",
266
+ "s16le",
267
+ "-ac",
268
+ "1",
269
+ "-ar",
270
+ str(rate_hz),
271
+ "-i",
272
+ "pipe:0",
273
+ "-c:a",
274
+ "aac",
275
+ "-b:a",
276
+ "96k",
277
+ "-f",
278
+ "adts",
279
+ "pipe:1",
280
+ ]
281
+ elif output_mime in ("audio/mp4", "audio/m4a"):
282
+ args = [
283
+ "-hide_banner",
284
+ "-loglevel",
285
+ "error",
286
+ "-f",
287
+ "s16le",
288
+ "-ac",
289
+ "1",
290
+ "-ar",
291
+ str(rate_hz),
292
+ "-i",
293
+ "pipe:0",
294
+ "-c:a",
295
+ "aac",
296
+ "-b:a",
297
+ "96k",
298
+ "-movflags",
299
+ "+frag_keyframe+empty_moov",
300
+ "-f",
301
+ "mp4",
302
+ "pipe:1",
303
+ ]
304
+ else:
305
+ # Passthrough streaming: just yield input
306
+ async for chunk in pcm_iter:
307
+ yield chunk
308
+ return
309
+
310
+ logger.info("FFmpeg(stream): starting args=%s", args)
311
+ proc = await asyncio.create_subprocess_exec(
312
+ "ffmpeg",
313
+ *args,
314
+ stdin=asyncio.subprocess.PIPE,
315
+ stdout=asyncio.subprocess.PIPE,
316
+ stderr=asyncio.subprocess.PIPE,
317
+ )
318
+
319
+ assert proc.stdin is not None and proc.stdout is not None
320
+
321
+ async def _writer():
322
+ try:
323
+ async for pcm in pcm_iter:
324
+ if not pcm:
325
+ continue
326
+ proc.stdin.write(pcm)
327
+ # Backpressure
328
+ await proc.stdin.drain()
329
+ except asyncio.CancelledError:
330
+ # Swallow cancellation; stdin will be closed below.
331
+ pass
332
+ except Exception as e:
333
+ logger.debug("FFmpeg(stream) writer error: %s", str(e))
334
+ finally:
335
+ with contextlib.suppress(Exception):
336
+ proc.stdin.close()
337
+
338
+ writer_task = asyncio.create_task(_writer())
339
+
340
+ buf = bytearray()
341
+ try:
342
+ while True:
343
+ data = await proc.stdout.read(read_chunk_size)
344
+ if not data:
345
+ break
346
+ buf.extend(data)
347
+ # Emit fixed-size chunks even if read returns a larger blob
348
+ while len(buf) >= read_chunk_size:
349
+ yield bytes(buf[:read_chunk_size])
350
+ del buf[:read_chunk_size]
351
+ # Flush any remainder
352
+ if buf:
353
+ yield bytes(buf)
354
+ finally:
355
+ # Ensure writer is done
356
+ if not writer_task.done():
357
+ with contextlib.suppress(Exception):
358
+ writer_task.cancel()
359
+ try:
360
+ await writer_task
361
+ except asyncio.CancelledError:
362
+ pass
363
+ except Exception:
364
+ pass
365
+ # Drain remaining stderr and check return code
366
+ try:
367
+ stderr = await proc.stderr.read() if proc.stderr else b""
368
+ code = await proc.wait()
369
+ if code != 0:
370
+ err = (stderr or b"").decode("utf-8", errors="ignore")
371
+ logger.error(
372
+ "FFmpeg(stream) failed (code=%s): %s", code, err[:2000]
373
+ )
374
+ except Exception:
375
+ pass
@@ -3,6 +3,7 @@ MongoDB adapter for the Solana Agent system.
3
3
 
4
4
  This adapter implements the DataStorageProvider interface for MongoDB.
5
5
  """
6
+
6
7
  import uuid
7
8
  from typing import Dict, List, Tuple, Optional
8
9
 
@@ -31,6 +32,16 @@ class MongoDBAdapter(DataStorageProvider):
31
32
  self.db[collection].insert_one(document)
32
33
  return document["_id"]
33
34
 
35
+ def insert_many(self, collection: str, documents: List[Dict]) -> List[str]:
36
+ for document in documents:
37
+ if "_id" not in document:
38
+ document["_id"] = str(uuid.uuid4())
39
+ result = self.db[collection].insert_many(documents)
40
+ return [str(doc_id) for doc_id in result.inserted_ids]
41
+
42
+ def delete_many(self, collection: str, query: Dict):
43
+ return self.db[collection].delete_many(query)
44
+
34
45
  def find_one(self, collection: str, query: Dict) -> Optional[Dict]:
35
46
  return self.db[collection].find_one(query)
36
47
 
@@ -40,7 +51,7 @@ class MongoDBAdapter(DataStorageProvider):
40
51
  query: Dict,
41
52
  sort: Optional[List[Tuple]] = None,
42
53
  limit: int = 0,
43
- skip: int = 0
54
+ skip: int = 0,
44
55
  ) -> List[Dict]:
45
56
  cursor = self.db[collection].find(query)
46
57
  if sort:
@@ -51,7 +62,9 @@ class MongoDBAdapter(DataStorageProvider):
51
62
  cursor = cursor.skip(skip)
52
63
  return list(cursor)
53
64
 
54
- def update_one(self, collection: str, query: Dict, update: Dict, upsert: bool = False) -> bool:
65
+ def update_one(
66
+ self, collection: str, query: Dict, update: Dict, upsert: bool = False
67
+ ) -> bool:
55
68
  result = self.db[collection].update_one(query, update, upsert=upsert)
56
69
  return result.modified_count > 0 or (upsert and result.upserted_id is not None)
57
70