unique_toolkit 2026.20.0.dev0__py3-none-any.whl → 2026.20.0.dev2__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.
@@ -181,6 +181,10 @@ class OpenAICodeInterpreterConfig(BaseToolConfig):
181
181
  default=False,
182
182
  description="If set, use the `auto` container setting from OpenAI. Note that this will recreate the container on each call.",
183
183
  )
184
+ additional_uploaded_documents: list[str] = Field(
185
+ default=[],
186
+ description="Documents (content_ids) to always upload to the container from the Knowledge Base. Useful for example for templates.",
187
+ )
184
188
 
185
189
 
186
190
  @register_config()
@@ -1,9 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import logging
4
5
  from typing import Any, override
5
6
 
6
- from openai import AsyncOpenAI, BaseModel, NotFoundError
7
+ from openai import (
8
+ AsyncOpenAI,
9
+ BaseModel,
10
+ NotFoundError,
11
+ OpenAIError,
12
+ )
7
13
  from openai.types.responses import ResponseCodeInterpreterToolCall, ResponseIncludable
8
14
  from openai.types.responses.tool_param import CodeInterpreter
9
15
  from tenacity import (
@@ -14,6 +20,7 @@ from tenacity import (
14
20
  )
15
21
 
16
22
  from unique_toolkit import ContentService, ShortTermMemoryService
23
+ from unique_toolkit._common.execution import SafeTaskExecutor
17
24
  from unique_toolkit.agentic.feature_flags.feature_flags import feature_flags
18
25
  from unique_toolkit.agentic.short_term_memory_manager.persistent_short_term_memory_manager import (
19
26
  PersistentShortMemoryManager,
@@ -56,7 +63,7 @@ _SHORT_TERM_MEMORY_NAME = "container_code_execution"
56
63
 
57
64
 
58
65
  class CodeExecutionShortTermMemorySchema(BaseModel):
59
- container_id: str | None = None
66
+ container_id: str
60
67
  file_ids: dict[str, str] = {} # Mapping of unique file id to openai file id
61
68
 
62
69
 
@@ -82,47 +89,107 @@ def _get_container_code_execution_short_term_memory_manager(
82
89
  return short_term_memory_manager
83
90
 
84
91
 
85
- async def _create_container_if_not_exists(
92
+ async def _check_container_exists(
93
+ client: AsyncOpenAI,
94
+ memory: CodeExecutionShortTermMemorySchema,
95
+ ) -> bool:
96
+ try:
97
+ container = await client.containers.retrieve(memory.container_id)
98
+ except NotFoundError:
99
+ logger.info("Container %s not found", memory.container_id)
100
+ return False
101
+
102
+ if container.status not in ["active", "running"]:
103
+ logger.info(
104
+ "Container %s has status `%s`, recreating a new one",
105
+ memory.container_id,
106
+ container.status,
107
+ )
108
+ return False
109
+
110
+ logger.info("Container %s found in short term memory", memory.container_id)
111
+ return True
112
+
113
+
114
+ async def _create_container(
86
115
  client: AsyncOpenAI,
87
116
  chat_id: str,
88
117
  user_id: str,
89
118
  company_id: str,
90
119
  expires_after_minutes: int,
91
- memory: CodeExecutionShortTermMemorySchema | None = None,
92
- ) -> CodeExecutionShortTermMemorySchema:
93
- if memory is not None:
94
- logger.info("Container found in short term memory")
95
- else:
96
- logger.info("No Container in short term memory, creating a new container")
97
- memory = CodeExecutionShortTermMemorySchema()
120
+ ) -> str:
121
+ container = await client.containers.create(
122
+ name=f"code_execution_{company_id}_{user_id}_{chat_id}",
123
+ expires_after={
124
+ "anchor": "last_active_at",
125
+ "minutes": expires_after_minutes,
126
+ },
127
+ )
128
+ logger.info("Created new container %s", container.id)
129
+ return container.id
130
+
98
131
 
132
+ async def _check_file_already_uploaded(
133
+ client: AsyncOpenAI,
134
+ content_id: str,
135
+ memory: CodeExecutionShortTermMemorySchema,
136
+ ) -> bool:
99
137
  container_id = memory.container_id
100
138
 
101
- if container_id is not None:
102
- try:
103
- container = await client.containers.retrieve(container_id)
104
- if container.status not in ["active", "running"]:
105
- logger.info(
106
- "Container has status `%s`, recreating a new one", container.status
107
- )
108
- container_id = None
109
- except NotFoundError:
110
- container_id = None
111
-
112
- if container_id is None:
113
- memory = CodeExecutionShortTermMemorySchema()
114
-
115
- container = await client.containers.create(
116
- name=f"code_execution_{company_id}_{user_id}_{chat_id}",
117
- expires_after={
118
- "anchor": "last_active_at",
119
- "minutes": expires_after_minutes,
120
- },
139
+ if content_id not in memory.file_ids:
140
+ logger.info("File with id %s not in short term memory", content_id)
141
+ return False
142
+
143
+ file_id = memory.file_ids[content_id]
144
+ try:
145
+ await client.containers.files.retrieve(
146
+ container_id=container_id, file_id=file_id
121
147
  )
148
+ logger.info("File with id %s already uploaded to container", content_id)
149
+ return True
150
+ except OpenAIError: # Azure API Errors are not stable
151
+ logger.exception("Error while retrieving file %s", file_id)
152
+ return False
122
153
 
123
- memory.container_id = container.id
124
154
 
125
- return memory
155
+ async def _upload_file_to_container(
156
+ client: AsyncOpenAI,
157
+ content_id: str,
158
+ filename: str,
159
+ content_service: ContentService,
160
+ container_id: str,
161
+ ) -> str:
162
+ logger.info(
163
+ "Uploading file %s (%s) to container %s",
164
+ content_id,
165
+ filename,
166
+ container_id,
167
+ )
168
+
169
+ file_content = await _build_upload_retry()(
170
+ content_service.download_content_to_bytes_async,
171
+ content_id=content_id,
172
+ )
173
+ logger.info(
174
+ "Downloaded %d bytes for file %s; uploading to container %s",
175
+ len(file_content),
176
+ content_id,
177
+ container_id,
178
+ )
179
+
180
+ openai_file = await _build_upload_retry()(
181
+ client.containers.files.create,
182
+ container_id=container_id,
183
+ file=(filename, file_content),
184
+ )
185
+ logger.info(
186
+ "File %s successfully uploaded as OpenAI file %s in container %s",
187
+ content_id,
188
+ openai_file.id,
189
+ container_id,
190
+ )
191
+
192
+ return openai_file.id
126
193
 
127
194
 
128
195
  async def _upload_files_to_container(
@@ -130,58 +197,61 @@ async def _upload_files_to_container(
130
197
  uploaded_files: list[Content],
131
198
  memory: CodeExecutionShortTermMemorySchema,
132
199
  content_service: ContentService,
133
- chat_id: str,
134
- ) -> CodeExecutionShortTermMemorySchema:
135
- container_id = memory.container_id
200
+ ) -> tuple[CodeExecutionShortTermMemorySchema, bool]:
136
201
 
137
- assert container_id is not None
202
+ async def _check_and_upload(content: Content) -> str | None:
203
+ if await _check_file_already_uploaded(
204
+ client=client, content_id=content.id, memory=memory
205
+ ):
206
+ return None
138
207
 
139
- memory = memory.model_copy(deep=True)
208
+ return await _upload_file_to_container(
209
+ client=client,
210
+ content_id=content.id,
211
+ filename=content.key,
212
+ content_service=content_service,
213
+ container_id=memory.container_id,
214
+ )
140
215
 
141
- for file in uploaded_files:
142
- upload = True
143
- if file.id in memory.file_ids:
144
- try:
145
- _ = await client.containers.files.retrieve(
146
- container_id=container_id, file_id=memory.file_ids[file.id]
147
- )
148
- logger.info("File with id %s already uploaded to container", file.id)
149
- upload = False
150
- except NotFoundError:
151
- upload = True
152
-
153
- if upload:
154
- logger.info(
155
- "Uploading file %s (%s) to container %s",
156
- file.id,
157
- file.key,
158
- memory.container_id,
159
- )
160
- file_content = await _build_upload_retry()(
161
- content_service.download_content_to_bytes_async,
162
- content_id=file.id,
163
- chat_id=chat_id,
164
- )
165
- logger.info(
166
- "Downloaded %d bytes for file %s; uploading to container %s",
167
- len(file_content),
168
- file.id,
169
- container_id,
170
- )
171
- openai_file = await _build_upload_retry()(
172
- client.containers.files.create,
173
- container_id=container_id,
174
- file=(file.key, file_content),
175
- )
176
- memory.file_ids[file.id] = openai_file.id
177
- logger.info(
178
- "File %s successfully uploaded as OpenAI file %s in container %s",
179
- file.id,
180
- openai_file.id,
181
- container_id,
182
- )
216
+ # Deduplicate
217
+ unique_contents = {content.id: content for content in uploaded_files}.values()
218
+
219
+ executor = SafeTaskExecutor(logger=logger)
220
+
221
+ results = await asyncio.gather(
222
+ *(
223
+ executor.execute_async(_check_and_upload, content)
224
+ for content in unique_contents
225
+ ),
226
+ )
227
+
228
+ updated = False
229
+ for content, result in zip(unique_contents, results):
230
+ if result.success and (file_id := result.unpack()) is not None:
231
+ memory.file_ids[content.id] = file_id
232
+ updated = True
233
+
234
+ return memory, updated
235
+
236
+
237
+ async def _resolve_kb_contents(
238
+ content_service: ContentService,
239
+ content_ids: list[str],
240
+ ) -> list[Content]:
241
+ contents = await content_service.search_contents_async(
242
+ where={"id": {"in": content_ids}},
243
+ )
244
+
245
+ found_ids = {c.id for c in contents}
246
+ missing = [content_id for content_id in content_ids if content_id not in found_ids]
247
+ if missing:
248
+ logger.warning(
249
+ "additional_uploaded_documents: %d content ids not found or not accessible in KB: %s",
250
+ len(missing),
251
+ missing,
252
+ )
183
253
 
184
- return memory
254
+ return contents
185
255
 
186
256
 
187
257
  class OpenAICodeInterpreterTool(OpenAIBuiltInTool[CodeInterpreter]):
@@ -266,27 +336,43 @@ class OpenAICodeInterpreterTool(OpenAIBuiltInTool[CodeInterpreter]):
266
336
 
267
337
  memory = await memory_manager.load_async()
268
338
 
269
- memory = await _create_container_if_not_exists(
270
- client=client,
271
- memory=memory,
272
- chat_id=chat_id,
273
- user_id=user_id,
274
- company_id=company_id,
275
- expires_after_minutes=config.expires_after_minutes,
276
- )
339
+ container_updated = False
340
+ if memory is None or not await _check_container_exists(
341
+ client=client, memory=memory
342
+ ):
343
+ container_id = await _create_container(
344
+ client=client,
345
+ chat_id=chat_id,
346
+ user_id=user_id,
347
+ company_id=company_id,
348
+ expires_after_minutes=config.expires_after_minutes,
349
+ )
350
+ memory = CodeExecutionShortTermMemorySchema(container_id=container_id)
351
+ container_updated = True
277
352
 
353
+ files_to_upload: list[Content] = []
278
354
  if config.upload_files_in_chat_to_container:
279
- memory = await _upload_files_to_container(
355
+ files_to_upload.extend(uploaded_files)
356
+
357
+ if config.additional_uploaded_documents:
358
+ files_to_upload.extend(
359
+ await _resolve_kb_contents(
360
+ content_service=content_service,
361
+ content_ids=config.additional_uploaded_documents,
362
+ )
363
+ )
364
+
365
+ files_updated = False
366
+ if files_to_upload:
367
+ memory, files_updated = await _upload_files_to_container(
280
368
  client=client,
281
- uploaded_files=uploaded_files,
369
+ uploaded_files=files_to_upload,
282
370
  content_service=content_service,
283
- chat_id=chat_id,
284
371
  memory=memory,
285
372
  )
286
373
 
287
- await memory_manager.save_async(memory)
288
-
289
- assert memory.container_id is not None
374
+ if container_updated or files_updated:
375
+ await memory_manager.save_async(memory)
290
376
 
291
377
  return OpenAICodeInterpreterTool(
292
378
  config=config,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.20.0.dev0
3
+ Version: 2026.20.0.dev2
4
4
  Summary:
5
5
  Author: Cedric Klinkert, Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri, Ejiroghene Oki-Peter, Rami Azouz, Ricardo Visini, Sanzio Monti
6
6
  Author-email: Cedric Klinkert <cedric.klinkert@unique.ch>, Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>, Ejiroghene Oki-Peter <oki-peter.ejiroghene@unique.ai>, Rami Azouz <rami.ext@unique.ch>, Ricardo Visini <ricardo.visini@unique.ch>, Sanzio Monti <sanzio.monti@unique.ai>
@@ -16,7 +16,7 @@ Requires-Dist: sseclient>=0.0.27,<0.0.28
16
16
  Requires-Dist: openai>=1.105.0,<3
17
17
  Requires-Dist: platformdirs>=4.0.0,<5
18
18
  Requires-Dist: pillow>=12.1.1
19
- Requires-Dist: unique-sdk>=2026.20.0.dev0
19
+ Requires-Dist: unique-sdk>=2026.20.0.dev1
20
20
  Requires-Dist: jambo>=0.1.2,<0.2
21
21
  Requires-Dist: jinja2>=3.1.6,<4
22
22
  Requires-Dist: docxtpl>=0.20.1,<0.21
@@ -193,12 +193,12 @@ unique_toolkit/agentic/tools/mcp/tool_wrapper.py,sha256=X2-UY-hQ4wguPOcf0TD4YoSK
193
193
  unique_toolkit/agentic/tools/openai_builtin/__init__.py,sha256=Q6d9mhsLRVF3erT2yMxLld62D3FTZ8nbovI4vLv4Nso,701
194
194
  unique_toolkit/agentic/tools/openai_builtin/base.py,sha256=D9bcOaedHQhXOJZACRkDWJte4kY8QXEnsL-LccmJLB4,1621
195
195
  unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py,sha256=PIt8wxfSeNYqExWpvrjZxD5sntd5Wv8bXKfoeCB4XWs,764
196
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py,sha256=Sy8zWXsDdR4RbMk3CVjBNbGhYGkGtvnj3nobUO2JAH0,16546
196
+ unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py,sha256=qn3GB4GC5FoYUOGMBdPK3RgYyseh5eyMUUYUOqrY31M,16766
197
197
  unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/__init__.py,sha256=ZSNvtRDSRipcfJg9gEsXm9DZDU7ODsAcko9vEmKevmI,584
198
198
  unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/code_display.py,sha256=Q4crOkYG-C_uTSc7rW14cP1H4IWxsKpfuftafDHbjEo,2897
199
199
  unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/generated_files.py,sha256=cC3dZDhmTQgHJHHmqVbe7C7Y9yb6lWpkttlYXpqSaOg,59670
200
200
  unique_toolkit/agentic/tools/openai_builtin/code_interpreter/schemas.py,sha256=W7egCmYrPrzic0aXsu9wY5fvQ8y_cl4Tzq_cjVs0mZs,542
201
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py,sha256=RHAep2s8Cao7Mv3uK3TlT0O9DOQFW5v9Hv01quZXUDw,10484
201
+ unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py,sha256=GHBALmwxN6O2EBt5meiwjdLnS2_YCEoXH8Buly02SBs,12674
202
202
  unique_toolkit/agentic/tools/openai_builtin/manager.py,sha256=CkfMRsjrRDv_X-zfJHZnxxOdpCK1qzf6RGGxKtUHp9k,3519
203
203
  unique_toolkit/agentic/tools/schemas.py,sha256=4BTJ0q7fiAVQ2Uy9kVJnS0rcKeKKaYTkOFq8XGZwMJ0,5291
204
204
  unique_toolkit/agentic/tools/test/test_mcp_manager.py,sha256=uWrTmnenQ70s5hrTfMYVah2vDT-jZq4nQmvhutypEK8,19195
@@ -322,6 +322,6 @@ unique_toolkit/short_term_memory/service.py,sha256=Ncw0_0CNM6kmzJSLVJGG5GcKpBduI
322
322
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
323
323
  unique_toolkit/smart_rules/compile.py,sha256=Ozhh70qCn2yOzRWr9d8WmJeTo7AQurwd3tStgBMPFLA,1246
324
324
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
325
- unique_toolkit-2026.20.0.dev0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
326
- unique_toolkit-2026.20.0.dev0.dist-info/METADATA,sha256=W1etgRW-d_WlqznHHc4pSIvHgtFls66upeH4l83WjII,7378
327
- unique_toolkit-2026.20.0.dev0.dist-info/RECORD,,
325
+ unique_toolkit-2026.20.0.dev2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
326
+ unique_toolkit-2026.20.0.dev2.dist-info/METADATA,sha256=Px8faTVjHEaXlgVBEu_qENSjr2GSgtYPe-wld_36RCs,7378
327
+ unique_toolkit-2026.20.0.dev2.dist-info/RECORD,,