solana-agent 31.1.7__py3-none-any.whl → 31.2.1__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.
@@ -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
@@ -68,7 +68,6 @@ class LLMProvider(ABC):
68
68
  async def tts(
69
69
  self,
70
70
  text: str,
71
- instructions: str = "You speak in a friendly and helpful manner.",
72
71
  voice: Literal[
73
72
  "alloy",
74
73
  "ash",
@@ -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
@@ -41,7 +41,6 @@ class AgentService(ABC):
41
41
  "sage",
42
42
  "shimmer",
43
43
  ] = "nova",
44
- audio_instructions: str = "You speak in a friendly and helpful manner.",
45
44
  audio_output_format: Literal[
46
45
  "mp3", "opus", "aac", "flac", "wav", "pcm"
47
46
  ] = "aac",
@@ -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
- memory = await self.zep.thread.get_user_context(thread_id=user_id)
144
- if memory and memory.context:
145
- memories = memory.context
146
- return memories
147
- except Exception as e:
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
 
@@ -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: