codex-agent-framework 0.1.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.
- codex_agent/__init__.py +38 -0
- codex_agent/__main__.py +18 -0
- codex_agent/agent.py +1179 -0
- codex_agent/ai.py +494 -0
- codex_agent/builtin_commands.py +136 -0
- codex_agent/builtin_providers.py +42 -0
- codex_agent/builtin_tools.py +418 -0
- codex_agent/chat.py +139 -0
- codex_agent/command.py +19 -0
- codex_agent/event.py +136 -0
- codex_agent/get_text/__init__.py +20 -0
- codex_agent/get_text/default_gitignore +110 -0
- codex_agent/get_text/get_text.py +1240 -0
- codex_agent/get_text/simpler_get_text.py +184 -0
- codex_agent/get_webdriver.py +44 -0
- codex_agent/image.py +304 -0
- codex_agent/latex.py +165 -0
- codex_agent/memory.py +318 -0
- codex_agent/message.py +423 -0
- codex_agent/prompts/image_generation_system_prompt.txt +13 -0
- codex_agent/prompts/system_prompt.txt +88 -0
- codex_agent/provider.py +19 -0
- codex_agent/stream_utils.py +645 -0
- codex_agent/tool.py +235 -0
- codex_agent/utils.py +374 -0
- codex_agent/voice.py +353 -0
- codex_agent/worker.py +190 -0
- codex_agent_framework-0.1.1.dist-info/METADATA +361 -0
- codex_agent_framework-0.1.1.dist-info/RECORD +33 -0
- codex_agent_framework-0.1.1.dist-info/WHEEL +5 -0
- codex_agent_framework-0.1.1.dist-info/entry_points.txt +2 -0
- codex_agent_framework-0.1.1.dist-info/licenses/LICENSE +21 -0
- codex_agent_framework-0.1.1.dist-info/top_level.txt +1 -0
codex_agent/voice.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
from .utils import utf8_safe_tokenize
|
|
2
|
+
import time
|
|
3
|
+
import re
|
|
4
|
+
from .stream_utils import Task, Streamer, TokenStreamer, fenced
|
|
5
|
+
from .event import AudioPlaybackEvent
|
|
6
|
+
from modict import modict
|
|
7
|
+
from pydub import AudioSegment
|
|
8
|
+
from pydub.playback import _play_with_simpleaudio, _play_with_pyaudio
|
|
9
|
+
|
|
10
|
+
def silent_play(audio):
|
|
11
|
+
"""Play audio using the best available backend without console output.
|
|
12
|
+
|
|
13
|
+
Tries backends in order: simpleaudio, pyaudio, ffplay.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
audio: AudioSegment object to play.
|
|
17
|
+
"""
|
|
18
|
+
# Sur Windows, ffplay rare, simpleaudio/pyaudio par défaut
|
|
19
|
+
# Sur Linux/Mac, ffplay fréquent
|
|
20
|
+
try:
|
|
21
|
+
# Essaye simpleaudio
|
|
22
|
+
return _play_with_simpleaudio(audio)
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
try:
|
|
26
|
+
# Essaye pyaudio
|
|
27
|
+
return _play_with_pyaudio(audio)
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
try:
|
|
31
|
+
# Utilise ffplay en mode silencieux
|
|
32
|
+
import subprocess
|
|
33
|
+
import tempfile
|
|
34
|
+
with tempfile.NamedTemporaryFile("w+b", suffix=".wav", delete=True) as f:
|
|
35
|
+
audio.export(f.name, "wav")
|
|
36
|
+
subprocess.call(
|
|
37
|
+
[
|
|
38
|
+
"ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", f.name
|
|
39
|
+
],
|
|
40
|
+
stdout=subprocess.DEVNULL,
|
|
41
|
+
stderr=subprocess.DEVNULL
|
|
42
|
+
)
|
|
43
|
+
except Exception:
|
|
44
|
+
print("Aucun backend audio fonctionnel trouvé.")
|
|
45
|
+
|
|
46
|
+
class PlayAudio(Task):
|
|
47
|
+
"""Task for playing audio asynchronously.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
agent: The agent instance.
|
|
51
|
+
args: Positional arguments for the task.
|
|
52
|
+
kwargs: Keyword arguments for the task.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self,agent,args=None,kwargs=None):
|
|
56
|
+
super().__init__(args=args,kwargs=kwargs)
|
|
57
|
+
self.agent=agent
|
|
58
|
+
|
|
59
|
+
def target(self,audio):
|
|
60
|
+
"""Play audio through event handlers, falling back to local playback.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
audio: AudioSegment to play.
|
|
64
|
+
"""
|
|
65
|
+
if audio is not None:
|
|
66
|
+
if self.agent.events.has_handlers(AudioPlaybackEvent):
|
|
67
|
+
self.agent.emit(AudioPlaybackEvent, audio=audio)
|
|
68
|
+
else:
|
|
69
|
+
silent_play(audio)
|
|
70
|
+
|
|
71
|
+
class MuteTagProcessor(Streamer):
|
|
72
|
+
"""Stream processor that wraps code blocks, LaTeX formulas, and links in MUTE tags.
|
|
73
|
+
|
|
74
|
+
These tagged sections will be skipped during text-to-speech conversion.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
agent: The agent instance.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self,agent):
|
|
81
|
+
super().__init__()
|
|
82
|
+
self.agent=agent
|
|
83
|
+
self.token_streamer = TokenStreamer([
|
|
84
|
+
#Intentionally muted blocs -> aggregate in buffer and pass through
|
|
85
|
+
(fenced('<MUTE>','</MUTE>',flags=re.DOTALL|re.MULTILINE), lambda m: m.group()),
|
|
86
|
+
# Mute markdown code blocs
|
|
87
|
+
(fenced('```','```',flags=re.DOTALL|re.MULTILINE), lambda m: f'<MUTE>{m.group()}</MUTE>'),
|
|
88
|
+
# Mute LaTeX formulas
|
|
89
|
+
(fenced(r'\$\$',r'\$\$',flags=re.DOTALL|re.MULTILINE), lambda m: f'<MUTE>{m.group()}</MUTE>'),
|
|
90
|
+
(fenced(r'\\\[',r'\\\]',flags=re.DOTALL|re.MULTILINE), lambda m: f'<MUTE>{m.group()}</MUTE>'),
|
|
91
|
+
(fenced(r'\\\(',r'\\\)',flags=re.DOTALL|re.MULTILINE), lambda m: f'<MUTE>{m.group()}</MUTE>'),
|
|
92
|
+
(r'\$[^$\n]+?\$', lambda m: f'<MUTE>{m.group()}</MUTE>'),
|
|
93
|
+
# Mute links
|
|
94
|
+
(r'https?://\S+', lambda m: f'<MUTE>{m.group()}</MUTE>'),
|
|
95
|
+
], threaded=False)
|
|
96
|
+
|
|
97
|
+
def stream_processor(self, stream):
|
|
98
|
+
"""Process stream and add MUTE tags around code/formulas/links."""
|
|
99
|
+
return self.token_streamer.process(stream)
|
|
100
|
+
|
|
101
|
+
class MuteAnalyzer(Streamer):
|
|
102
|
+
"""Splits token stream into muted and non-muted parts.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
agent: The agent instance.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self,agent):
|
|
109
|
+
super().__init__()
|
|
110
|
+
self.agent=agent
|
|
111
|
+
|
|
112
|
+
def stream_processor(self, stream):
|
|
113
|
+
"""Split stream into tuples of (mode, text) where mode is 'mute' or 'speak'."""
|
|
114
|
+
for token in stream:
|
|
115
|
+
if token.startswith('<MUTE>') and token.endswith('</MUTE>'):
|
|
116
|
+
yield ('mute', token[6:-7]) # Strip <MUTE>...</MUTE> tags
|
|
117
|
+
else:
|
|
118
|
+
yield ('speak',token)
|
|
119
|
+
|
|
120
|
+
class LineAgregator(Streamer):
|
|
121
|
+
"""Aggregates tokens into lines for voice processing.
|
|
122
|
+
|
|
123
|
+
Buffers tokens until newline characters are encountered or token count
|
|
124
|
+
exceeds threshold, then yields complete lines with their mode (mute/speak).
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
agent: The agent instance.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self,agent):
|
|
131
|
+
super().__init__()
|
|
132
|
+
self.agent=agent
|
|
133
|
+
|
|
134
|
+
def stream_processor(self,stream):
|
|
135
|
+
"""Aggregate tokens into lines based on newlines and token count.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
stream: Input stream of (mode, token) tuples.
|
|
139
|
+
|
|
140
|
+
Yields:
|
|
141
|
+
Tuples of (mode, line_text) for each aggregated line.
|
|
142
|
+
"""
|
|
143
|
+
lines=""
|
|
144
|
+
token_count=0
|
|
145
|
+
last_mode=None
|
|
146
|
+
for mode, token in stream:
|
|
147
|
+
|
|
148
|
+
if mode!=last_mode:
|
|
149
|
+
if lines and last_mode is not None:
|
|
150
|
+
yield (last_mode,lines)
|
|
151
|
+
lines=""
|
|
152
|
+
token_count=0
|
|
153
|
+
last_mode=mode
|
|
154
|
+
|
|
155
|
+
token_count+=1
|
|
156
|
+
while '\n' in token: #in case a token contains several occurences of '\n'
|
|
157
|
+
parts=token.split('\n')
|
|
158
|
+
lines+=parts[0]+'\n'
|
|
159
|
+
if token_count>50:
|
|
160
|
+
yield (last_mode,lines)
|
|
161
|
+
lines=""
|
|
162
|
+
token_count=0
|
|
163
|
+
token='\n'.join(parts[1:])
|
|
164
|
+
else:
|
|
165
|
+
lines+=token
|
|
166
|
+
if lines and last_mode is not None:
|
|
167
|
+
yield (last_mode,lines)
|
|
168
|
+
|
|
169
|
+
class LineToAudio(Streamer):
|
|
170
|
+
"""Converts text lines to audio segments using text-to-speech.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
agent: The agent instance with AI client for TTS.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(self,agent):
|
|
177
|
+
super().__init__()
|
|
178
|
+
self.agent=agent
|
|
179
|
+
|
|
180
|
+
def text_to_audio(self,text):
|
|
181
|
+
"""Convert text to audio using configured TTS model.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
text: Text string to convert to speech.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
AudioSegment object or None if text is empty.
|
|
188
|
+
"""
|
|
189
|
+
# Create MP3 audio
|
|
190
|
+
|
|
191
|
+
if text.strip():
|
|
192
|
+
|
|
193
|
+
params=modict(
|
|
194
|
+
model=self.agent.config.get('voice_model','gpt-4o-mini-tts'),
|
|
195
|
+
voice=self.agent.config.get('voice','nova'),
|
|
196
|
+
instructions=self.agent.config.get('voice_instructions','You speak with a friendly and casual tone.'),
|
|
197
|
+
input=text
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
audio = self.agent.ai.text_to_audio(**params)
|
|
201
|
+
|
|
202
|
+
return audio
|
|
203
|
+
else:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
def stream_processor(self,stream):
|
|
207
|
+
"""Process stream of lines, converting non-muted text to audio.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
stream: Stream of (mode, line) tuples.
|
|
211
|
+
|
|
212
|
+
Yields:
|
|
213
|
+
Tuples of (line, audio) where audio is AudioSegment or None for muted lines.
|
|
214
|
+
"""
|
|
215
|
+
for mode,line in stream:
|
|
216
|
+
if not line:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
if mode=='mute':
|
|
220
|
+
audio=None
|
|
221
|
+
else:
|
|
222
|
+
audio=self.text_to_audio(line)
|
|
223
|
+
|
|
224
|
+
yield (line,audio)
|
|
225
|
+
|
|
226
|
+
class AudioPlayer(Streamer):
|
|
227
|
+
"""Plays audio while streaming tokens at synchronized pace.
|
|
228
|
+
|
|
229
|
+
Plays audio segments in background while yielding tokens at a rate
|
|
230
|
+
synchronized with the audio playback duration.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
agent: The agent instance.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def __init__(self,agent):
|
|
237
|
+
super().__init__()
|
|
238
|
+
self.agent=agent
|
|
239
|
+
|
|
240
|
+
def stream_processor(self,stream):
|
|
241
|
+
"""Play audio and yield tokens synchronized with playback.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
stream: Stream of (line, audio) tuples.
|
|
245
|
+
|
|
246
|
+
Yields:
|
|
247
|
+
Individual tokens with timing synchronized to audio playback.
|
|
248
|
+
"""
|
|
249
|
+
for line,audio in stream:
|
|
250
|
+
tokenized=utf8_safe_tokenize(line)
|
|
251
|
+
if audio is not None:
|
|
252
|
+
sleep_time_per_token = 0.95*(audio.duration_seconds / len(tokenized)) # Durée ajustée par token
|
|
253
|
+
task=PlayAudio(self.agent,args=(audio,))
|
|
254
|
+
task.start()
|
|
255
|
+
for token in tokenized:
|
|
256
|
+
yield token
|
|
257
|
+
time.sleep(sleep_time_per_token)
|
|
258
|
+
task.join()
|
|
259
|
+
else:
|
|
260
|
+
for token in tokenized:
|
|
261
|
+
yield token
|
|
262
|
+
time.sleep(0.02)
|
|
263
|
+
|
|
264
|
+
class Throttler(Streamer):
|
|
265
|
+
"""Buffers and batches tokens to control output rate.
|
|
266
|
+
|
|
267
|
+
Accumulates tokens in a buffer and yields them in batches to prevent
|
|
268
|
+
overwhelming downstream consumers.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
agent: The agent instance with buffer size configuration.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
def __init__(self,agent):
|
|
275
|
+
super().__init__()
|
|
276
|
+
self.agent=agent
|
|
277
|
+
|
|
278
|
+
def stream_processor(self,stream):
|
|
279
|
+
"""Buffer and batch tokens before yielding.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
stream: Input token stream.
|
|
283
|
+
|
|
284
|
+
Yields:
|
|
285
|
+
Batched token strings.
|
|
286
|
+
"""
|
|
287
|
+
buffer=''
|
|
288
|
+
buffer_size=self.agent.config.get('voice_buffer_size',3) # Number of tokens to buffer before yielding the result
|
|
289
|
+
for i,token in enumerate(stream):
|
|
290
|
+
buffer+=token
|
|
291
|
+
if i%buffer_size==0:
|
|
292
|
+
yield buffer
|
|
293
|
+
buffer=''
|
|
294
|
+
if buffer:
|
|
295
|
+
yield buffer
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class VoiceProcessor(Streamer):
|
|
300
|
+
"""Orchestrates text-to-speech processing for token streams.
|
|
301
|
+
|
|
302
|
+
Coordinates multiple processing stages:
|
|
303
|
+
1. Tags code blocks, formulas, and links as muted
|
|
304
|
+
2. Analyzes and splits into muted/spoken sections
|
|
305
|
+
3. Aggregates tokens into lines
|
|
306
|
+
4. Converts spoken lines to audio
|
|
307
|
+
5. Plays audio synchronized with token output
|
|
308
|
+
6. Throttles output for controlled streaming
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
agent: The agent instance with voice configuration.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
def __init__(self,agent):
|
|
315
|
+
super().__init__()
|
|
316
|
+
self.agent=agent
|
|
317
|
+
self.mute_tag_processor=MuteTagProcessor(self.agent)
|
|
318
|
+
self.line_splitter=LineAgregator(self.agent)
|
|
319
|
+
self.line_to_audio=LineToAudio(self.agent)
|
|
320
|
+
self.speech_processor=AudioPlayer(self.agent)
|
|
321
|
+
self.mute_analyzer=MuteAnalyzer(self.agent)
|
|
322
|
+
self.throttler=Throttler(self.agent)
|
|
323
|
+
|
|
324
|
+
def __call__(self,stream):
|
|
325
|
+
"""Process stream with voice if enabled, otherwise pass through.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
stream: Input token stream.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Processed token stream (with or without voice).
|
|
332
|
+
"""
|
|
333
|
+
if self.agent.config.get('voice_enabled',False):
|
|
334
|
+
return self.process(stream)
|
|
335
|
+
else:
|
|
336
|
+
return stream
|
|
337
|
+
|
|
338
|
+
def stream_processor(self, stream):
|
|
339
|
+
"""Chain all voice processing stages together.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
stream: Input token stream.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Token stream synchronized with speech output.
|
|
346
|
+
"""
|
|
347
|
+
tagged_stream = self.mute_tag_processor.process(stream)
|
|
348
|
+
analyzed_stream= self.mute_analyzer.process(tagged_stream)
|
|
349
|
+
marked_lines = self.line_splitter.process(analyzed_stream)
|
|
350
|
+
lines_with_playback = self.line_to_audio.process(marked_lines)
|
|
351
|
+
synced_stream=self.speech_processor.process(lines_with_playback)
|
|
352
|
+
throttled_stream=self.throttler.process(synced_stream)
|
|
353
|
+
return throttled_stream
|
codex_agent/worker.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from modict import modict
|
|
2
|
+
|
|
3
|
+
from codex_backend_sdk import CodexClient, OutputItem, ReasoningDelta, ResponseAborted, ResponseFailed, TextDelta, ToolCall
|
|
4
|
+
from .tool import Tool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Worker:
|
|
8
|
+
"""One-shot backend worker base class.
|
|
9
|
+
|
|
10
|
+
Subclasses specialize behavior by overriding class fields such as
|
|
11
|
+
``system_prompt``, ``tools``, and model defaults.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
system_prompt = "Complete the requested task directly and return only the useful result."
|
|
15
|
+
model = "gpt-5.4"
|
|
16
|
+
reasoning_effort = "medium"
|
|
17
|
+
verbosity = "medium"
|
|
18
|
+
tools = []
|
|
19
|
+
tool_choice = None
|
|
20
|
+
output_schema = None
|
|
21
|
+
resolve_tools = True
|
|
22
|
+
name = "worker"
|
|
23
|
+
max_retries = 3
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
client=None,
|
|
29
|
+
system_prompt=None,
|
|
30
|
+
model=None,
|
|
31
|
+
reasoning_effort=None,
|
|
32
|
+
verbosity=None,
|
|
33
|
+
tools=None,
|
|
34
|
+
tool_choice=None,
|
|
35
|
+
output_schema=None,
|
|
36
|
+
resolve_tools=None,
|
|
37
|
+
name=None,
|
|
38
|
+
max_retries=None,
|
|
39
|
+
):
|
|
40
|
+
self.system_prompt = system_prompt if system_prompt is not None else self.system_prompt
|
|
41
|
+
self.model = model if model is not None else self.model
|
|
42
|
+
self.reasoning_effort = reasoning_effort if reasoning_effort is not None else self.reasoning_effort
|
|
43
|
+
self.verbosity = verbosity if verbosity is not None else self.verbosity
|
|
44
|
+
self.tools = list(tools if tools is not None else self.tools)
|
|
45
|
+
self.tool_choice = tool_choice if tool_choice is not None else self.tool_choice
|
|
46
|
+
self.output_schema = output_schema if output_schema is not None else self.output_schema
|
|
47
|
+
self.resolve_tools = resolve_tools if resolve_tools is not None else self.resolve_tools
|
|
48
|
+
self.name = name if name is not None else self.name
|
|
49
|
+
self.max_retries = max_retries if max_retries is not None else self.max_retries
|
|
50
|
+
self._client = client
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def client(self):
|
|
54
|
+
if self._client is None:
|
|
55
|
+
self._client = CodexClient(model=self.model, max_retries=self.max_retries).authenticate()
|
|
56
|
+
return self._client
|
|
57
|
+
|
|
58
|
+
def get_tools(self):
|
|
59
|
+
return [
|
|
60
|
+
tool.to_response_tool() if hasattr(tool, "to_response_tool") else dict(tool)
|
|
61
|
+
for tool in self.tools
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
def get_local_tools(self):
|
|
65
|
+
return {
|
|
66
|
+
tool.name: tool
|
|
67
|
+
for tool in self.tools
|
|
68
|
+
if isinstance(tool, Tool)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def run(
|
|
72
|
+
self,
|
|
73
|
+
prompt,
|
|
74
|
+
*,
|
|
75
|
+
conversation_history=None,
|
|
76
|
+
tools=None,
|
|
77
|
+
model=None,
|
|
78
|
+
reasoning_effort=None,
|
|
79
|
+
verbosity=None,
|
|
80
|
+
tool_choice=None,
|
|
81
|
+
output_schema=None,
|
|
82
|
+
resolve_tools=None,
|
|
83
|
+
):
|
|
84
|
+
text = ""
|
|
85
|
+
reasoning = ""
|
|
86
|
+
tool_calls = []
|
|
87
|
+
tool_results = []
|
|
88
|
+
output_items = []
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
resolved_tools = tools if tools is not None else self.get_tools()
|
|
92
|
+
resolved_tool_choice = tool_choice or self.tool_choice or ("auto" if resolved_tools else "none")
|
|
93
|
+
resolved_output_schema = output_schema if output_schema is not None else self.output_schema
|
|
94
|
+
resolved_verbosity = verbosity or self.verbosity
|
|
95
|
+
|
|
96
|
+
events = self.client.stream(
|
|
97
|
+
prompt,
|
|
98
|
+
conversation_history=conversation_history or [],
|
|
99
|
+
instructions=self.system_prompt,
|
|
100
|
+
model=model or self.model,
|
|
101
|
+
reasoning=reasoning_effort or self.reasoning_effort,
|
|
102
|
+
reasoning_summary="auto" if (reasoning_effort or self.reasoning_effort) else None,
|
|
103
|
+
include_reasoning=bool(reasoning_effort or self.reasoning_effort),
|
|
104
|
+
verbosity=None if resolved_output_schema is not None else resolved_verbosity,
|
|
105
|
+
tools=resolved_tools,
|
|
106
|
+
tool_choice=resolved_tool_choice,
|
|
107
|
+
parallel_tool_calls=False,
|
|
108
|
+
output_schema=resolved_output_schema,
|
|
109
|
+
)
|
|
110
|
+
for event in events:
|
|
111
|
+
if isinstance(event, TextDelta):
|
|
112
|
+
text += event.text
|
|
113
|
+
elif isinstance(event, ReasoningDelta):
|
|
114
|
+
reasoning += event.text
|
|
115
|
+
elif isinstance(event, ToolCall):
|
|
116
|
+
tool_calls.append(event.raw)
|
|
117
|
+
should_resolve = self.resolve_tools if resolve_tools is None else resolve_tools
|
|
118
|
+
if should_resolve and len(tool_results) == 0:
|
|
119
|
+
tool = self.get_local_tools().get(event.name)
|
|
120
|
+
if tool is not None:
|
|
121
|
+
output = str(tool(**event.parsed_arguments()))
|
|
122
|
+
tool_results.append(modict(
|
|
123
|
+
call_id=event.call_id,
|
|
124
|
+
name=event.name,
|
|
125
|
+
output=output,
|
|
126
|
+
))
|
|
127
|
+
elif isinstance(event, OutputItem):
|
|
128
|
+
output_items.append(event.raw)
|
|
129
|
+
elif isinstance(event, ResponseFailed):
|
|
130
|
+
raise RuntimeError(f"[{event.code}] {event.message}")
|
|
131
|
+
except ResponseAborted:
|
|
132
|
+
raise
|
|
133
|
+
|
|
134
|
+
if tool_results:
|
|
135
|
+
tool_output = "\n".join(result.output for result in tool_results)
|
|
136
|
+
text = f"{text}\n{tool_output}".strip() if text else tool_output
|
|
137
|
+
|
|
138
|
+
return modict(
|
|
139
|
+
content=text,
|
|
140
|
+
reasoning=reasoning,
|
|
141
|
+
tool_calls=tool_calls,
|
|
142
|
+
tool_results=tool_results,
|
|
143
|
+
output_items=output_items,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def __call__(self, prompt, **kwargs):
|
|
147
|
+
return self.run(prompt, **kwargs).content
|
|
148
|
+
|
|
149
|
+
def abort(self):
|
|
150
|
+
if self._client is not None:
|
|
151
|
+
self._client.abort()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class SummarizerWorker(Worker):
|
|
155
|
+
name = "summarizer"
|
|
156
|
+
system_prompt = """You are a focused summarization worker.
|
|
157
|
+
|
|
158
|
+
Return a compact, faithful summary of the supplied content.
|
|
159
|
+
Preserve names, decisions, constraints, unresolved questions, and concrete facts.
|
|
160
|
+
Remove repetition and conversational filler.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TurnSummaryWorker(Worker):
|
|
165
|
+
name = "turn_summary"
|
|
166
|
+
model = "gpt-5.4-mini"
|
|
167
|
+
system_prompt = """You are a RAG archive worker.
|
|
168
|
+
|
|
169
|
+
Summarize one complete conversation turn into exactly one dense, semantically meaningful paragraph.
|
|
170
|
+
This will be used to populate a long-term memory store that the AI assistant can retrieve from in future conversations.
|
|
171
|
+
Focus on what is worth remembering long term for the AI assistant, not on the incidental details that are only relevant in the moment.
|
|
172
|
+
The turn may contain the user request, assistant messages and reasoning summaries, tool calls, tool results, server-tool outputs, generated files, errors, interruptions, and the final assistant response.
|
|
173
|
+
Preserve the user's intent, important facts, constraints, decisions, actions performed, concrete artifacts such as file paths or commands, final outcome, and any unresolved state.
|
|
174
|
+
Ignore operational noise, raw payloads, base64 data, duplicate stream fragments, and incidental logs unless they changed the outcome.
|
|
175
|
+
If the turn does not contain anything genuinely useful to remember long term, such as a simple greeting, thanks, acknowledgement, or other exchange with no durable user information, durable decision, artifact, or unresolved state, return an empty string.
|
|
176
|
+
Return only the paragraph, with no title, bullets, Markdown, JSON, or commentary.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def summarize_turn(self, turn, previous_summary=None, **kwargs):
|
|
180
|
+
prompt = "\n\n".join(message.as_string() for message in turn)
|
|
181
|
+
if previous_summary:
|
|
182
|
+
prompt = (
|
|
183
|
+
"Previous turn summary for background only. Do not summarize this previous summary; "
|
|
184
|
+
"use it only to understand the immediate prior context and formulate the new turn summary better.\n"
|
|
185
|
+
f"<previous_turn_summary>\n{previous_summary}\n</previous_turn_summary>\n\n"
|
|
186
|
+
"<turn_to_summarize>\n"
|
|
187
|
+
f"{prompt}\n"
|
|
188
|
+
"</turn_to_summarize>"
|
|
189
|
+
)
|
|
190
|
+
return self.run(prompt, **kwargs).content
|