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/memory.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from typing import List
|
|
5
|
+
from xml.sax.saxutils import escape, quoteattr
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from modict import modict
|
|
9
|
+
|
|
10
|
+
from .message import DeveloperMessage, Message
|
|
11
|
+
from .utils import short_id, token_count, timestamp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MemoryEntry(DeveloperMessage):
|
|
15
|
+
title = ""
|
|
16
|
+
source = "manual"
|
|
17
|
+
metadata = modict.factory(dict)
|
|
18
|
+
|
|
19
|
+
def to_response_format(self):
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def format_memory_results_xml(results, root=None):
|
|
24
|
+
entries = "\n".join(
|
|
25
|
+
(
|
|
26
|
+
f"<memory id={quoteattr(str(entry.id))} "
|
|
27
|
+
f"title={quoteattr(str(entry.get('title') or ''))}>"
|
|
28
|
+
f"\n{escape(str(entry.content))}\n"
|
|
29
|
+
f"</memory>"
|
|
30
|
+
)
|
|
31
|
+
for entry in results
|
|
32
|
+
)
|
|
33
|
+
if root is None:
|
|
34
|
+
return entries
|
|
35
|
+
return f"<{root}>\n{entries}\n</{root}>"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RAGMemoryConfig(modict):
|
|
39
|
+
_config = modict.config(enforce_json=True)
|
|
40
|
+
|
|
41
|
+
precision: int = 5
|
|
42
|
+
dimensions: int = 128
|
|
43
|
+
half_life_days: float = 2
|
|
44
|
+
half_life_steps: float = 2
|
|
45
|
+
moment_exponent: float = 3
|
|
46
|
+
max_tokens: int | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RAGMemory(modict):
|
|
50
|
+
"""Timestamped embedding retrieval over an explicit message collection."""
|
|
51
|
+
|
|
52
|
+
settings: RAGMemoryConfig = modict.factory(RAGMemoryConfig)
|
|
53
|
+
messages: List[Message] = modict.factory(list)
|
|
54
|
+
file: str | None = None
|
|
55
|
+
|
|
56
|
+
def __init__(self, *args, agent=None, file=None, messages=None, **kwargs):
|
|
57
|
+
config_data = {}
|
|
58
|
+
for key in list(kwargs):
|
|
59
|
+
if key in RAGMemoryConfig.__fields__:
|
|
60
|
+
config_data[key] = kwargs.pop(key)
|
|
61
|
+
super().__init__(*args, **kwargs)
|
|
62
|
+
self.set_attr("_agent", agent)
|
|
63
|
+
if config_data:
|
|
64
|
+
self.settings.update(config_data)
|
|
65
|
+
if file is not None:
|
|
66
|
+
self.file = file
|
|
67
|
+
if messages is not None:
|
|
68
|
+
self.messages = list(messages)
|
|
69
|
+
elif self.file:
|
|
70
|
+
self.load()
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def agent(self):
|
|
74
|
+
return self.get_attr("_agent", None)
|
|
75
|
+
|
|
76
|
+
def add(self, message):
|
|
77
|
+
self.messages.append(message)
|
|
78
|
+
return message
|
|
79
|
+
|
|
80
|
+
def load(self, file=None):
|
|
81
|
+
self.file = file or self.file
|
|
82
|
+
if not self.file or not os.path.isfile(self.file):
|
|
83
|
+
return self
|
|
84
|
+
with open(self.file, "r", encoding="utf-8") as f:
|
|
85
|
+
data = json.load(f)
|
|
86
|
+
|
|
87
|
+
self.settings.update(data.get("settings") or {})
|
|
88
|
+
self.messages = [
|
|
89
|
+
MemoryEntry(entry)
|
|
90
|
+
for entry in data.get("messages", [])
|
|
91
|
+
]
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def dump(self, file=None):
|
|
95
|
+
self.file = file or self.file
|
|
96
|
+
if not self.file:
|
|
97
|
+
return self
|
|
98
|
+
parent = os.path.dirname(self.file)
|
|
99
|
+
if parent:
|
|
100
|
+
os.makedirs(parent, exist_ok=True)
|
|
101
|
+
data = modict(
|
|
102
|
+
type="memory",
|
|
103
|
+
settings=self.settings,
|
|
104
|
+
messages=self.messages,
|
|
105
|
+
)
|
|
106
|
+
with open(self.file, "w", encoding="utf-8") as f:
|
|
107
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def remember(self, content, title="", source="manual", metadata=None, memory_id=None):
|
|
111
|
+
entry = MemoryEntry(
|
|
112
|
+
id=memory_id or short_id(),
|
|
113
|
+
content=content,
|
|
114
|
+
title=title,
|
|
115
|
+
source=source,
|
|
116
|
+
metadata=metadata or {},
|
|
117
|
+
timestamp=timestamp(),
|
|
118
|
+
)
|
|
119
|
+
self.ensure_embedding(entry)
|
|
120
|
+
self.add(entry)
|
|
121
|
+
self.dump()
|
|
122
|
+
return entry
|
|
123
|
+
|
|
124
|
+
def get(self, memory_id):
|
|
125
|
+
for entry in self.messages:
|
|
126
|
+
if entry.get("id") == memory_id:
|
|
127
|
+
return entry
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def find(self, ref):
|
|
131
|
+
entry = self.get(ref)
|
|
132
|
+
if entry is not None:
|
|
133
|
+
return entry
|
|
134
|
+
for entry in self.messages:
|
|
135
|
+
if entry.get("title") == ref:
|
|
136
|
+
return entry
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def edit(self, memory_id, content=None, title=None, source=None, metadata=None):
|
|
140
|
+
entry = self.find(memory_id)
|
|
141
|
+
if entry is None:
|
|
142
|
+
raise KeyError(memory_id)
|
|
143
|
+
if content is not None and content != entry.get("content"):
|
|
144
|
+
entry.content = content
|
|
145
|
+
entry.embedding = None
|
|
146
|
+
self.ensure_embedding(entry)
|
|
147
|
+
if title is not None:
|
|
148
|
+
entry.title = title
|
|
149
|
+
if source is not None:
|
|
150
|
+
entry.source = source
|
|
151
|
+
if metadata is not None:
|
|
152
|
+
entry.metadata = metadata
|
|
153
|
+
entry.timestamp = timestamp()
|
|
154
|
+
self.dump()
|
|
155
|
+
return entry
|
|
156
|
+
|
|
157
|
+
def delete(self, memory_id):
|
|
158
|
+
entry = self.find(memory_id)
|
|
159
|
+
if entry is None:
|
|
160
|
+
return None
|
|
161
|
+
self.messages = [
|
|
162
|
+
message for message in self.messages
|
|
163
|
+
if message.get("id") != entry.get("id")
|
|
164
|
+
]
|
|
165
|
+
self.dump()
|
|
166
|
+
return entry
|
|
167
|
+
|
|
168
|
+
def ensure_embedding(self, message):
|
|
169
|
+
if message.get("embedding") is not None:
|
|
170
|
+
return message.embedding
|
|
171
|
+
if self.agent is None:
|
|
172
|
+
raise ValueError("Message has no embedding and no agent was provided to embed it.")
|
|
173
|
+
return self.agent.ai.embed_message(
|
|
174
|
+
message,
|
|
175
|
+
precision=self.settings.precision,
|
|
176
|
+
dimensions=self.settings.dimensions,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def ensure_embeddings(self, messages):
|
|
180
|
+
for message in messages:
|
|
181
|
+
self.ensure_embedding(message)
|
|
182
|
+
|
|
183
|
+
def similarity(self, msg1, msg2):
|
|
184
|
+
self.ensure_embedding(msg1)
|
|
185
|
+
self.ensure_embedding(msg2)
|
|
186
|
+
return float((np.dot(msg1.embedding, msg2.embedding) + 1) / 2)
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def recency_weight(k: int, n: int, half_life: float) -> float:
|
|
190
|
+
if n == 1:
|
|
191
|
+
return 1
|
|
192
|
+
if not 0 <= k < n:
|
|
193
|
+
raise ValueError("k must be between in range(n)")
|
|
194
|
+
if half_life <= 0:
|
|
195
|
+
raise ValueError("half_life must be positive")
|
|
196
|
+
|
|
197
|
+
decay = np.log(2) / half_life
|
|
198
|
+
ratio = np.exp(-decay)
|
|
199
|
+
numerator = (1 - ratio) * ratio**k
|
|
200
|
+
denominator = 1 - ratio**n
|
|
201
|
+
return float(numerator / denominator)
|
|
202
|
+
|
|
203
|
+
def relevance_score(self, message, context, exponent=None):
|
|
204
|
+
if not context:
|
|
205
|
+
return 0
|
|
206
|
+
exponent = exponent or self.settings.moment_exponent
|
|
207
|
+
n = len(context)
|
|
208
|
+
similarities = [self.similarity(message, ctx_msg) for ctx_msg in reversed(context)]
|
|
209
|
+
weights = [self.recency_weight(k, n, self.settings.half_life_steps) for k in range(n)]
|
|
210
|
+
score = sum(
|
|
211
|
+
weight * similarity**exponent
|
|
212
|
+
for weight, similarity in zip(weights, similarities)
|
|
213
|
+
)
|
|
214
|
+
return float(score ** (1.0 / exponent))
|
|
215
|
+
|
|
216
|
+
def temporal_score(self, message, now=None):
|
|
217
|
+
now = now or datetime.now()
|
|
218
|
+
timestamp = message.get("timestamp")
|
|
219
|
+
msg_time = self.parse_timestamp(timestamp)
|
|
220
|
+
delta_days = (now - msg_time).total_seconds() / (3600 * 24)
|
|
221
|
+
decay = np.log(2) / self.settings.half_life_days
|
|
222
|
+
return float(np.exp(-decay * delta_days))
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def parse_timestamp(value):
|
|
226
|
+
if isinstance(value, datetime):
|
|
227
|
+
return value
|
|
228
|
+
return datetime.strptime(value, "%Y%m%dT%H%M%S.%f")
|
|
229
|
+
|
|
230
|
+
def filter_by_timestamp(self, messages, since=None, until=None):
|
|
231
|
+
if since is None and until is None:
|
|
232
|
+
return list(messages)
|
|
233
|
+
since_time = self.parse_timestamp(since) if since is not None else None
|
|
234
|
+
until_time = self.parse_timestamp(until) if until is not None else None
|
|
235
|
+
filtered = []
|
|
236
|
+
for message in messages:
|
|
237
|
+
msg_time = self.parse_timestamp(message.get("timestamp"))
|
|
238
|
+
if since_time is not None and msg_time < since_time:
|
|
239
|
+
continue
|
|
240
|
+
if until_time is not None and msg_time > until_time:
|
|
241
|
+
continue
|
|
242
|
+
filtered.append(message)
|
|
243
|
+
return filtered
|
|
244
|
+
|
|
245
|
+
def score(self, message, context, recent_is_better=True, now=None):
|
|
246
|
+
score = self.relevance_score(message, context=context)
|
|
247
|
+
if recent_is_better:
|
|
248
|
+
score += self.temporal_score(message, now=now)
|
|
249
|
+
return score
|
|
250
|
+
|
|
251
|
+
def rank(self, context, messages=None, recent_is_better=True, now=None):
|
|
252
|
+
messages = list(self.messages if messages is None else messages)
|
|
253
|
+
self.ensure_embeddings(context)
|
|
254
|
+
self.ensure_embeddings(messages)
|
|
255
|
+
return sorted(
|
|
256
|
+
messages,
|
|
257
|
+
key=lambda message: self.score(
|
|
258
|
+
message,
|
|
259
|
+
context=context,
|
|
260
|
+
recent_is_better=recent_is_better,
|
|
261
|
+
now=now,
|
|
262
|
+
),
|
|
263
|
+
reverse=True,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def retrieve(
|
|
267
|
+
self,
|
|
268
|
+
context,
|
|
269
|
+
messages=None,
|
|
270
|
+
limit=None,
|
|
271
|
+
max_tokens=None,
|
|
272
|
+
recent_is_better=True,
|
|
273
|
+
exclude_session_id=None,
|
|
274
|
+
since=None,
|
|
275
|
+
until=None,
|
|
276
|
+
now=None,
|
|
277
|
+
):
|
|
278
|
+
if messages is None:
|
|
279
|
+
messages = self.filter_by_timestamp(self.messages, since=since, until=until)
|
|
280
|
+
else:
|
|
281
|
+
messages = self.filter_by_timestamp(messages, since=since, until=until)
|
|
282
|
+
results = self.rank(
|
|
283
|
+
context,
|
|
284
|
+
messages=messages,
|
|
285
|
+
recent_is_better=recent_is_better,
|
|
286
|
+
now=now,
|
|
287
|
+
)
|
|
288
|
+
if exclude_session_id is not None:
|
|
289
|
+
results = [
|
|
290
|
+
entry for entry in results
|
|
291
|
+
if entry.get("session_id") != exclude_session_id
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
selected = []
|
|
295
|
+
total = 0
|
|
296
|
+
token_budget = self.settings.max_tokens if max_tokens is None else max_tokens
|
|
297
|
+
for entry in results:
|
|
298
|
+
if limit is not None and len(selected) >= limit:
|
|
299
|
+
break
|
|
300
|
+
if token_budget is not None:
|
|
301
|
+
tokens = token_count(str(entry.get("content") or ""))
|
|
302
|
+
if total + tokens > token_budget:
|
|
303
|
+
break
|
|
304
|
+
total += tokens
|
|
305
|
+
selected.append(entry)
|
|
306
|
+
return selected
|
|
307
|
+
|
|
308
|
+
def search(self, query, limit=5, recent_is_better=False, since=None, until=None, **kwargs):
|
|
309
|
+
query_message = Message(content=query)
|
|
310
|
+
self.ensure_embedding(query_message)
|
|
311
|
+
return self.retrieve(
|
|
312
|
+
[query_message],
|
|
313
|
+
limit=limit,
|
|
314
|
+
recent_is_better=recent_is_better,
|
|
315
|
+
since=since,
|
|
316
|
+
until=until,
|
|
317
|
+
**kwargs,
|
|
318
|
+
)
|