solana-agent 31.1.6__py3-none-any.whl → 31.2.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/ffmpeg_transcoder.py +279 -0
- solana_agent/adapters/openai_adapter.py +5 -0
- solana_agent/adapters/openai_realtime_ws.py +1613 -0
- solana_agent/client/solana_agent.py +29 -3
- solana_agent/factories/agent_factory.py +2 -1
- solana_agent/interfaces/client/client.py +18 -1
- solana_agent/interfaces/providers/audio.py +40 -0
- solana_agent/interfaces/providers/llm.py +0 -1
- solana_agent/interfaces/providers/realtime.py +100 -0
- solana_agent/interfaces/services/agent.py +0 -1
- solana_agent/interfaces/services/query.py +12 -1
- solana_agent/repositories/memory.py +184 -19
- solana_agent/services/agent.py +0 -5
- solana_agent/services/query.py +561 -6
- solana_agent/services/realtime.py +506 -0
- {solana_agent-31.1.6.dist-info → solana_agent-31.2.0.dist-info}/METADATA +40 -9
- {solana_agent-31.1.6.dist-info → solana_agent-31.2.0.dist-info}/RECORD +20 -15
- {solana_agent-31.1.6.dist-info → solana_agent-31.2.0.dist-info}/LICENSE +0 -0
- {solana_agent-31.1.6.dist-info → solana_agent-31.2.0.dist-info}/WHEEL +0 -0
- {solana_agent-31.1.6.dist-info → solana_agent-31.2.0.dist-info}/entry_points.txt +0 -0
@@ -52,6 +52,23 @@ class SolanaAgent(SolanaAgentInterface):
|
|
52
52
|
capture_schema: Optional[Dict[str, Any]] = None,
|
53
53
|
capture_name: Optional[str] = None,
|
54
54
|
output_format: Literal["text", "audio"] = "text",
|
55
|
+
# Realtime (WebSocket) options — used when realtime=True
|
56
|
+
realtime: bool = False,
|
57
|
+
vad: Optional[bool] = False,
|
58
|
+
rt_encode_input: bool = False,
|
59
|
+
rt_encode_output: bool = False,
|
60
|
+
rt_voice: Literal[
|
61
|
+
"alloy",
|
62
|
+
"ash",
|
63
|
+
"ballad",
|
64
|
+
"cedar",
|
65
|
+
"coral",
|
66
|
+
"echo",
|
67
|
+
"marin",
|
68
|
+
"sage",
|
69
|
+
"shimmer",
|
70
|
+
"verse",
|
71
|
+
] = "marin",
|
55
72
|
audio_voice: Literal[
|
56
73
|
"alloy",
|
57
74
|
"ash",
|
@@ -64,7 +81,6 @@ class SolanaAgent(SolanaAgentInterface):
|
|
64
81
|
"sage",
|
65
82
|
"shimmer",
|
66
83
|
] = "nova",
|
67
|
-
audio_instructions: str = "You speak in a friendly and helpful manner.",
|
68
84
|
audio_output_format: Literal[
|
69
85
|
"mp3", "opus", "aac", "flac", "wav", "pcm"
|
70
86
|
] = "aac",
|
@@ -82,8 +98,14 @@ class SolanaAgent(SolanaAgentInterface):
|
|
82
98
|
message: Text message or audio bytes
|
83
99
|
prompt: Optional prompt for the agent
|
84
100
|
output_format: Response format ("text" or "audio")
|
101
|
+
capture_schema: Optional Pydantic schema for structured output
|
102
|
+
capture_name: Optional name for structured output capture
|
103
|
+
realtime: Whether to use realtime (WebSocket) processing
|
104
|
+
vad: Whether to use voice activity detection (for audio input)
|
105
|
+
rt_encode_input: Whether to re-encode input audio for compatibility
|
106
|
+
rt_encode_output: Whether to re-encode output audio for compatibility
|
107
|
+
rt_voice: Voice to use for realtime audio output
|
85
108
|
audio_voice: Voice to use for audio output
|
86
|
-
audio_instructions: Not used in this version
|
87
109
|
audio_output_format: Audio output format
|
88
110
|
audio_input_format: Audio input format
|
89
111
|
router: Optional routing service for processing
|
@@ -98,8 +120,12 @@ class SolanaAgent(SolanaAgentInterface):
|
|
98
120
|
query=message,
|
99
121
|
images=images,
|
100
122
|
output_format=output_format,
|
123
|
+
realtime=realtime,
|
124
|
+
vad=vad,
|
125
|
+
rt_encode_input=rt_encode_input,
|
126
|
+
rt_encode_output=rt_encode_output,
|
127
|
+
rt_voice=rt_voice,
|
101
128
|
audio_voice=audio_voice,
|
102
|
-
audio_instructions=audio_instructions,
|
103
129
|
audio_output_format=audio_output_format,
|
104
130
|
audio_input_format=audio_input_format,
|
105
131
|
prompt=prompt,
|
@@ -19,6 +19,7 @@ from solana_agent.services.query import QueryService
|
|
19
19
|
from solana_agent.services.agent import AgentService
|
20
20
|
from solana_agent.services.routing import RoutingService
|
21
21
|
from solana_agent.services.knowledge_base import KnowledgeBaseService
|
22
|
+
# Realtime is now managed per-call in QueryService.process; no factory wiring
|
22
23
|
|
23
24
|
# Repository imports
|
24
25
|
from solana_agent.repositories.memory import MemoryRepository
|
@@ -80,7 +81,7 @@ class SolanaAgentFactory:
|
|
80
81
|
return guardrails
|
81
82
|
|
82
83
|
@staticmethod
|
83
|
-
def create_from_config(config: Dict[str, Any]) -> QueryService:
|
84
|
+
def create_from_config(config: Dict[str, Any]) -> QueryService: # pragma: no cover
|
84
85
|
"""Create the agent system from configuration.
|
85
86
|
|
86
87
|
Args:
|
@@ -16,6 +16,24 @@ class SolanaAgent(ABC):
|
|
16
16
|
message: Union[str, bytes],
|
17
17
|
prompt: Optional[str] = None,
|
18
18
|
output_format: Literal["text", "audio"] = "text",
|
19
|
+
capture_schema: Optional[Dict[str, Any]] = None,
|
20
|
+
capture_name: Optional[str] = None,
|
21
|
+
realtime: bool = False,
|
22
|
+
vad: bool = False,
|
23
|
+
rt_encode_input: bool = False,
|
24
|
+
rt_encode_output: bool = False,
|
25
|
+
rt_voice: Literal[
|
26
|
+
"alloy",
|
27
|
+
"ash",
|
28
|
+
"ballad",
|
29
|
+
"cedar",
|
30
|
+
"coral",
|
31
|
+
"echo",
|
32
|
+
"marin",
|
33
|
+
"sage",
|
34
|
+
"shimmer",
|
35
|
+
"verse",
|
36
|
+
] = "marin",
|
19
37
|
audio_voice: Literal[
|
20
38
|
"alloy",
|
21
39
|
"ash",
|
@@ -28,7 +46,6 @@ class SolanaAgent(ABC):
|
|
28
46
|
"sage",
|
29
47
|
"shimmer",
|
30
48
|
] = "nova",
|
31
|
-
audio_instructions: str = "You speak in a friendly and helpful manner.",
|
32
49
|
audio_output_format: Literal[
|
33
50
|
"mp3", "opus", "aac", "flac", "wav", "pcm"
|
34
51
|
] = "aac",
|
@@ -0,0 +1,40 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
|
5
|
+
|
6
|
+
class AudioTranscoder(ABC):
|
7
|
+
"""Abstract audio transcoder for converting between compressed audio and PCM16.
|
8
|
+
|
9
|
+
Implementations may rely on external tools (e.g., ffmpeg).
|
10
|
+
"""
|
11
|
+
|
12
|
+
@abstractmethod
|
13
|
+
async def to_pcm16(
|
14
|
+
self, audio_bytes: bytes, input_mime: str, rate_hz: int
|
15
|
+
) -> bytes:
|
16
|
+
"""Transcode arbitrary audio to mono PCM16LE at rate_hz.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
audio_bytes: Source audio bytes (e.g., MP4/AAC)
|
20
|
+
input_mime: Source mime-type (e.g., 'audio/mp4', 'audio/aac')
|
21
|
+
rate_hz: Target sample rate
|
22
|
+
Returns:
|
23
|
+
Raw PCM16LE mono bytes at the given rate
|
24
|
+
"""
|
25
|
+
raise NotImplementedError
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
async def from_pcm16(
|
29
|
+
self, pcm16_bytes: bytes, output_mime: str, rate_hz: int
|
30
|
+
) -> bytes:
|
31
|
+
"""Transcode mono PCM16LE at rate_hz to the desired output mime.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
pcm16_bytes: Raw PCM16LE bytes
|
35
|
+
output_mime: Target mime-type (e.g., 'audio/aac')
|
36
|
+
rate_hz: Sample rate of the PCM
|
37
|
+
Returns:
|
38
|
+
Encoded audio bytes
|
39
|
+
"""
|
40
|
+
raise NotImplementedError
|
@@ -0,0 +1,100 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Any, AsyncGenerator, Dict, Literal, Optional, Awaitable, Callable
|
5
|
+
|
6
|
+
|
7
|
+
@dataclass
|
8
|
+
class RealtimeSessionOptions:
|
9
|
+
model: Optional[str] = None
|
10
|
+
voice: Literal[
|
11
|
+
"alloy",
|
12
|
+
"ash",
|
13
|
+
"ballad",
|
14
|
+
"cedar",
|
15
|
+
"coral",
|
16
|
+
"echo",
|
17
|
+
"marin",
|
18
|
+
"sage",
|
19
|
+
"shimmer",
|
20
|
+
"verse",
|
21
|
+
] = "marin"
|
22
|
+
vad_enabled: bool = True
|
23
|
+
input_rate_hz: int = 24000
|
24
|
+
output_rate_hz: int = 24000
|
25
|
+
input_mime: str = "audio/pcm" # 16-bit PCM
|
26
|
+
output_mime: str = "audio/pcm" # 16-bit PCM
|
27
|
+
instructions: Optional[str] = None
|
28
|
+
# Optional: tools payload compatible with OpenAI Realtime session.update
|
29
|
+
tools: Optional[list[dict[str, Any]]] = None
|
30
|
+
tool_choice: str = "auto"
|
31
|
+
# Tool execution behavior
|
32
|
+
# Max time to allow a tool to run before timing out (seconds)
|
33
|
+
tool_timeout_s: float = 300.0
|
34
|
+
# Optional guard: if a tool takes longer than this to complete, skip sending
|
35
|
+
# function_call_output to avoid stale/expired call_id issues. Set to None to always send.
|
36
|
+
tool_result_max_age_s: Optional[float] = None
|
37
|
+
|
38
|
+
|
39
|
+
class BaseRealtimeSession(ABC):
|
40
|
+
"""Abstract realtime session supporting bidirectional audio/text over WebSocket."""
|
41
|
+
|
42
|
+
@abstractmethod
|
43
|
+
async def connect(self) -> None: # pragma: no cover
|
44
|
+
pass
|
45
|
+
|
46
|
+
@abstractmethod
|
47
|
+
async def close(self) -> None: # pragma: no cover
|
48
|
+
pass
|
49
|
+
|
50
|
+
# --- Client events ---
|
51
|
+
@abstractmethod
|
52
|
+
async def update_session(
|
53
|
+
self, session_patch: Dict[str, Any]
|
54
|
+
) -> None: # pragma: no cover
|
55
|
+
pass
|
56
|
+
|
57
|
+
@abstractmethod
|
58
|
+
async def append_audio(self, pcm16_bytes: bytes) -> None: # pragma: no cover
|
59
|
+
"""Append 16-bit PCM audio bytes (matching configured input rate/mime)."""
|
60
|
+
pass
|
61
|
+
|
62
|
+
@abstractmethod
|
63
|
+
async def commit_input(self) -> None: # pragma: no cover
|
64
|
+
pass
|
65
|
+
|
66
|
+
@abstractmethod
|
67
|
+
async def clear_input(self) -> None: # pragma: no cover
|
68
|
+
pass
|
69
|
+
|
70
|
+
@abstractmethod
|
71
|
+
async def create_response(
|
72
|
+
self, response_patch: Optional[Dict[str, Any]] = None
|
73
|
+
) -> None: # pragma: no cover
|
74
|
+
pass
|
75
|
+
|
76
|
+
# --- Server events (demuxed) ---
|
77
|
+
@abstractmethod
|
78
|
+
def iter_events(self) -> AsyncGenerator[Dict[str, Any], None]: # pragma: no cover
|
79
|
+
pass
|
80
|
+
|
81
|
+
@abstractmethod
|
82
|
+
def iter_output_audio(self) -> AsyncGenerator[bytes, None]: # pragma: no cover
|
83
|
+
pass
|
84
|
+
|
85
|
+
@abstractmethod
|
86
|
+
def iter_input_transcript(self) -> AsyncGenerator[str, None]: # pragma: no cover
|
87
|
+
pass
|
88
|
+
|
89
|
+
@abstractmethod
|
90
|
+
def iter_output_transcript(self) -> AsyncGenerator[str, None]: # pragma: no cover
|
91
|
+
pass
|
92
|
+
|
93
|
+
# --- Optional tool execution hook ---
|
94
|
+
@abstractmethod
|
95
|
+
def set_tool_executor(
|
96
|
+
self,
|
97
|
+
executor: Callable[[str, Dict[str, Any]], Awaitable[Dict[str, Any]]],
|
98
|
+
) -> None: # pragma: no cover
|
99
|
+
"""Register a coroutine that executes a tool by name with arguments and returns a result dict."""
|
100
|
+
pass
|
@@ -15,6 +15,18 @@ class QueryService(ABC):
|
|
15
15
|
user_id: str,
|
16
16
|
query: Union[str, bytes],
|
17
17
|
output_format: Literal["text", "audio"] = "text",
|
18
|
+
rt_voice: Literal[
|
19
|
+
"alloy",
|
20
|
+
"ash",
|
21
|
+
"ballad",
|
22
|
+
"cedar",
|
23
|
+
"coral",
|
24
|
+
"echo",
|
25
|
+
"marin",
|
26
|
+
"sage",
|
27
|
+
"shimmer",
|
28
|
+
"verse",
|
29
|
+
] = "marin",
|
18
30
|
audio_voice: Literal[
|
19
31
|
"alloy",
|
20
32
|
"ash",
|
@@ -27,7 +39,6 @@ class QueryService(ABC):
|
|
27
39
|
"sage",
|
28
40
|
"shimmer",
|
29
41
|
] = "nova",
|
30
|
-
audio_instructions: str = "You speak in a friendly and helpful manner.",
|
31
42
|
audio_output_format: Literal[
|
32
43
|
"mp3", "opus", "aac", "flac", "wav", "pcm"
|
33
44
|
] = "aac",
|
@@ -25,6 +25,7 @@ class MemoryRepository(MemoryProvider):
|
|
25
25
|
self.mongo = None
|
26
26
|
self.collection = None
|
27
27
|
self.captures_collection = "captures"
|
28
|
+
self.stream_collection = "conversation_stream"
|
28
29
|
else:
|
29
30
|
self.mongo = mongo_adapter
|
30
31
|
self.collection = "conversations"
|
@@ -32,7 +33,7 @@ class MemoryRepository(MemoryProvider):
|
|
32
33
|
self.mongo.create_collection(self.collection)
|
33
34
|
self.mongo.create_index(self.collection, [("user_id", 1)])
|
34
35
|
self.mongo.create_index(self.collection, [("timestamp", 1)])
|
35
|
-
except Exception as e:
|
36
|
+
except Exception as e: # pragma: no cover
|
36
37
|
logger.error(f"Error initializing MongoDB: {e}")
|
37
38
|
|
38
39
|
try:
|
@@ -50,15 +51,150 @@ class MemoryRepository(MemoryProvider):
|
|
50
51
|
[("user_id", 1), ("agent_name", 1), ("capture_name", 1)],
|
51
52
|
unique=True,
|
52
53
|
)
|
53
|
-
except Exception as e:
|
54
|
+
except Exception as e: # pragma: no cover
|
54
55
|
logger.error(f"Error creating unique index for captures: {e}")
|
55
|
-
except Exception as e:
|
56
|
+
except Exception as e: # pragma: no cover
|
56
57
|
logger.error(f"Error initializing MongoDB captures collection: {e}")
|
57
58
|
self.captures_collection = "captures"
|
58
59
|
|
60
|
+
# Defer stream collection creation to first use to preserve legacy init expectations
|
61
|
+
self.stream_collection = "conversation_stream"
|
62
|
+
|
59
63
|
# Zep setup
|
60
64
|
self.zep = AsyncZepCloud(api_key=zep_api_key) if zep_api_key else None
|
61
65
|
|
66
|
+
# --- Realtime streaming helpers (Mongo only) ---
|
67
|
+
async def begin_stream_turn(
|
68
|
+
self, user_id: str
|
69
|
+
) -> Optional[str]: # pragma: no cover
|
70
|
+
"""Begin a realtime turn by creating/returning a turn_id (Mongo only)."""
|
71
|
+
if not self.mongo:
|
72
|
+
return None
|
73
|
+
from uuid import uuid4
|
74
|
+
|
75
|
+
turn_id = str(uuid4())
|
76
|
+
try:
|
77
|
+
now = datetime.now(timezone.utc)
|
78
|
+
# Ensure stream collection and indexes exist lazily
|
79
|
+
try:
|
80
|
+
if not self.mongo.collection_exists(self.stream_collection):
|
81
|
+
self.mongo.create_collection(self.stream_collection)
|
82
|
+
self.mongo.create_index(self.stream_collection, [("user_id", 1)])
|
83
|
+
self.mongo.create_index(
|
84
|
+
self.stream_collection, [("turn_id", 1)], unique=True
|
85
|
+
)
|
86
|
+
self.mongo.create_index(self.stream_collection, [("partial", 1)])
|
87
|
+
self.mongo.create_index(self.stream_collection, [("timestamp", 1)])
|
88
|
+
except Exception: # pragma: no cover
|
89
|
+
pass
|
90
|
+
self.mongo.insert_one(
|
91
|
+
self.stream_collection,
|
92
|
+
{
|
93
|
+
"user_id": user_id,
|
94
|
+
"turn_id": turn_id,
|
95
|
+
"user_partial": "",
|
96
|
+
"assistant_partial": "",
|
97
|
+
"partial": True,
|
98
|
+
"timestamp": now,
|
99
|
+
"created_at": now,
|
100
|
+
},
|
101
|
+
)
|
102
|
+
return turn_id
|
103
|
+
except Exception as e: # pragma: no cover
|
104
|
+
logger.error(f"MongoDB begin_stream_turn error: {e}")
|
105
|
+
return None
|
106
|
+
|
107
|
+
async def update_stream_user(
|
108
|
+
self, user_id: str, turn_id: str, delta: str
|
109
|
+
) -> None: # pragma: no cover
|
110
|
+
if not self.mongo or not delta:
|
111
|
+
return
|
112
|
+
try:
|
113
|
+
doc = self.mongo.find_one(
|
114
|
+
self.stream_collection, {"turn_id": turn_id, "user_id": user_id}
|
115
|
+
)
|
116
|
+
if not doc:
|
117
|
+
return
|
118
|
+
content = (doc.get("user_partial") or "") + delta
|
119
|
+
self.mongo.update_one(
|
120
|
+
self.stream_collection,
|
121
|
+
{"turn_id": turn_id, "user_id": user_id},
|
122
|
+
{
|
123
|
+
"$set": {
|
124
|
+
"user_partial": content,
|
125
|
+
"timestamp": datetime.now(timezone.utc),
|
126
|
+
}
|
127
|
+
},
|
128
|
+
upsert=False,
|
129
|
+
)
|
130
|
+
except Exception as e: # pragma: no cover
|
131
|
+
logger.error(f"MongoDB update_stream_user error: {e}")
|
132
|
+
|
133
|
+
async def update_stream_assistant( # pragma: no cover
|
134
|
+
self, user_id: str, turn_id: str, delta: str
|
135
|
+
) -> None:
|
136
|
+
if not self.mongo or not delta:
|
137
|
+
return
|
138
|
+
try:
|
139
|
+
doc = self.mongo.find_one(
|
140
|
+
self.stream_collection, {"turn_id": turn_id, "user_id": user_id}
|
141
|
+
)
|
142
|
+
if not doc:
|
143
|
+
return
|
144
|
+
content = (doc.get("assistant_partial") or "") + delta
|
145
|
+
self.mongo.update_one(
|
146
|
+
self.stream_collection,
|
147
|
+
{"turn_id": turn_id, "user_id": user_id},
|
148
|
+
{
|
149
|
+
"$set": {
|
150
|
+
"assistant_partial": content,
|
151
|
+
"timestamp": datetime.now(timezone.utc),
|
152
|
+
}
|
153
|
+
},
|
154
|
+
upsert=False,
|
155
|
+
)
|
156
|
+
except Exception as e: # pragma: no cover
|
157
|
+
logger.error(f"MongoDB update_stream_assistant error: {e}")
|
158
|
+
|
159
|
+
async def finalize_stream_turn(
|
160
|
+
self, user_id: str, turn_id: str
|
161
|
+
) -> None: # pragma: no cover
|
162
|
+
if not self.mongo:
|
163
|
+
return
|
164
|
+
try:
|
165
|
+
doc = self.mongo.find_one(
|
166
|
+
self.stream_collection, {"turn_id": turn_id, "user_id": user_id}
|
167
|
+
)
|
168
|
+
if not doc:
|
169
|
+
return
|
170
|
+
user_text = doc.get("user_partial", "")
|
171
|
+
assistant_text = doc.get("assistant_partial", "")
|
172
|
+
now = datetime.now(timezone.utc)
|
173
|
+
self.mongo.update_one(
|
174
|
+
self.stream_collection,
|
175
|
+
{"turn_id": turn_id, "user_id": user_id},
|
176
|
+
{"$set": {"partial": False, "timestamp": now, "finalized_at": now}},
|
177
|
+
upsert=False,
|
178
|
+
)
|
179
|
+
# Also persist to conversations collection as a complete turn
|
180
|
+
if user_text or assistant_text:
|
181
|
+
try:
|
182
|
+
self.mongo.insert_one(
|
183
|
+
self.collection,
|
184
|
+
{
|
185
|
+
"user_id": user_id,
|
186
|
+
"user_message": user_text,
|
187
|
+
"assistant_message": assistant_text,
|
188
|
+
"timestamp": now,
|
189
|
+
},
|
190
|
+
)
|
191
|
+
except Exception as e: # pragma: no cover
|
192
|
+
logger.error(
|
193
|
+
f"MongoDB finalize_stream_turn insert conversations error: {e}"
|
194
|
+
)
|
195
|
+
except Exception as e: # pragma: no cover
|
196
|
+
logger.error(f"MongoDB finalize_stream_turn error: {e}")
|
197
|
+
|
62
198
|
async def store(self, user_id: str, messages: List[Dict[str, Any]]) -> None:
|
63
199
|
if not user_id or not isinstance(user_id, str):
|
64
200
|
raise ValueError("User ID cannot be None or empty")
|
@@ -98,7 +234,7 @@ class MemoryRepository(MemoryProvider):
|
|
98
234
|
"timestamp": datetime.now(timezone.utc),
|
99
235
|
},
|
100
236
|
)
|
101
|
-
except Exception as e:
|
237
|
+
except Exception as e: # pragma: no cover
|
102
238
|
logger.error(f"MongoDB storage error: {e}")
|
103
239
|
|
104
240
|
# Zep
|
@@ -120,49 +256,78 @@ class MemoryRepository(MemoryProvider):
|
|
120
256
|
await self.zep.thread.add_messages(
|
121
257
|
thread_id=user_id, messages=zep_messages
|
122
258
|
)
|
123
|
-
except Exception:
|
259
|
+
except Exception: # pragma: no cover
|
124
260
|
try:
|
125
261
|
try:
|
126
262
|
await self.zep.user.add(user_id=user_id)
|
127
|
-
except Exception as e:
|
263
|
+
except Exception as e: # pragma: no cover
|
128
264
|
logger.error(f"Zep user addition error: {e}")
|
129
265
|
try:
|
130
266
|
await self.zep.thread.create(thread_id=user_id, user_id=user_id)
|
131
|
-
except Exception as e:
|
267
|
+
except Exception as e: # pragma: no cover
|
132
268
|
logger.error(f"Zep thread creation error: {e}")
|
133
269
|
await self.zep.thread.add_messages(
|
134
270
|
thread_id=user_id, messages=zep_messages
|
135
271
|
)
|
136
|
-
except Exception as e:
|
272
|
+
except Exception as e: # pragma: no cover
|
137
273
|
logger.error(f"Zep memory addition error: {e}")
|
138
274
|
|
139
|
-
async def retrieve(self, user_id: str) -> str:
|
275
|
+
async def retrieve(self, user_id: str) -> str: # pragma: no cover
|
140
276
|
try:
|
277
|
+
# Preferred: Zep user context
|
141
278
|
memories = ""
|
142
279
|
if self.zep:
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
280
|
+
try:
|
281
|
+
memory = await self.zep.thread.get_user_context(thread_id=user_id)
|
282
|
+
if memory and memory.context:
|
283
|
+
memories = memory.context
|
284
|
+
except Exception as e: # pragma: no cover
|
285
|
+
logger.error(f"Zep retrieval error: {e}")
|
286
|
+
|
287
|
+
# Fallback: Build lightweight conversation history from Mongo if available
|
288
|
+
if not memories and self.mongo:
|
289
|
+
try:
|
290
|
+
# Fetch last 10 conversations for this user in ascending time order
|
291
|
+
docs = self.mongo.find(
|
292
|
+
self.collection,
|
293
|
+
{"user_id": user_id},
|
294
|
+
sort=[("timestamp", 1)],
|
295
|
+
limit=10,
|
296
|
+
)
|
297
|
+
if docs:
|
298
|
+
parts: List[str] = []
|
299
|
+
for d in docs:
|
300
|
+
u = (d or {}).get("user_message") or ""
|
301
|
+
a = (d or {}).get("assistant_message") or ""
|
302
|
+
# Only include complete turns to avoid partial/ambiguous history
|
303
|
+
if u and a:
|
304
|
+
parts.append(f"User: {u}")
|
305
|
+
parts.append(f"Assistant: {a}")
|
306
|
+
parts.append("") # blank line between turns
|
307
|
+
memories = "\n".join(parts).strip()
|
308
|
+
except Exception as e: # pragma: no cover
|
309
|
+
logger.error(f"Mongo fallback retrieval error: {e}")
|
310
|
+
|
311
|
+
return memories or ""
|
312
|
+
except Exception as e: # pragma: no cover
|
148
313
|
logger.error(f"Error retrieving memories: {e}")
|
149
314
|
return ""
|
150
315
|
|
151
|
-
async def delete(self, user_id: str) -> None:
|
316
|
+
async def delete(self, user_id: str) -> None: # pragma: no cover
|
152
317
|
if self.mongo:
|
153
318
|
try:
|
154
319
|
self.mongo.delete_all(self.collection, {"user_id": user_id})
|
155
|
-
except Exception as e:
|
320
|
+
except Exception as e: # pragma: no cover
|
156
321
|
logger.error(f"MongoDB deletion error: {e}")
|
157
322
|
if not self.zep:
|
158
323
|
return
|
159
324
|
try:
|
160
325
|
await self.zep.thread.delete(thread_id=user_id)
|
161
|
-
except Exception as e:
|
326
|
+
except Exception as e: # pragma: no cover
|
162
327
|
logger.error(f"Zep memory deletion error: {e}")
|
163
328
|
try:
|
164
329
|
await self.zep.user.delete(user_id=user_id)
|
165
|
-
except Exception as e:
|
330
|
+
except Exception as e: # pragma: no cover
|
166
331
|
logger.error(f"Zep user deletion error: {e}")
|
167
332
|
|
168
333
|
def find(
|
@@ -177,7 +342,7 @@ class MemoryRepository(MemoryProvider):
|
|
177
342
|
return []
|
178
343
|
try:
|
179
344
|
return self.mongo.find(collection, query, sort=sort, limit=limit, skip=skip)
|
180
|
-
except Exception as e:
|
345
|
+
except Exception as e: # pragma: no cover
|
181
346
|
logger.error(f"MongoDB find error: {e}")
|
182
347
|
return []
|
183
348
|
|
solana_agent/services/agent.py
CHANGED
@@ -258,7 +258,6 @@ class AgentService(AgentServiceInterface):
|
|
258
258
|
"sage",
|
259
259
|
"shimmer",
|
260
260
|
] = "nova",
|
261
|
-
audio_instructions: str = "You speak in a friendly and helpful manner.",
|
262
261
|
audio_output_format: Literal[
|
263
262
|
"mp3", "opus", "aac", "flac", "wav", "pcm"
|
264
263
|
] = "aac",
|
@@ -276,7 +275,6 @@ class AgentService(AgentServiceInterface):
|
|
276
275
|
if output_format == "audio":
|
277
276
|
async for chunk in self.llm_provider.tts(
|
278
277
|
error_msg,
|
279
|
-
instructions=audio_instructions,
|
280
278
|
response_format=audio_output_format,
|
281
279
|
voice=audio_voice,
|
282
280
|
):
|
@@ -339,7 +337,6 @@ class AgentService(AgentServiceInterface):
|
|
339
337
|
text=cleaned_audio_buffer,
|
340
338
|
voice=audio_voice,
|
341
339
|
response_format=audio_output_format,
|
342
|
-
instructions=audio_instructions,
|
343
340
|
):
|
344
341
|
yield audio_chunk
|
345
342
|
else:
|
@@ -457,7 +454,6 @@ class AgentService(AgentServiceInterface):
|
|
457
454
|
text=cleaned_audio_buffer,
|
458
455
|
voice=audio_voice,
|
459
456
|
response_format=audio_output_format,
|
460
|
-
instructions=audio_instructions,
|
461
457
|
):
|
462
458
|
yield audio_chunk
|
463
459
|
else:
|
@@ -479,7 +475,6 @@ class AgentService(AgentServiceInterface):
|
|
479
475
|
error_msg,
|
480
476
|
voice=audio_voice,
|
481
477
|
response_format=audio_output_format,
|
482
|
-
instructions=audio_instructions,
|
483
478
|
):
|
484
479
|
yield chunk
|
485
480
|
else:
|