lm-deluge 0.0.67__py3-none-any.whl → 0.0.90__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.
Potentially problematic release.
This version of lm-deluge might be problematic. Click here for more details.
- lm_deluge/__init__.py +1 -2
- lm_deluge/api_requests/anthropic.py +117 -22
- lm_deluge/api_requests/base.py +84 -11
- lm_deluge/api_requests/bedrock.py +30 -6
- lm_deluge/api_requests/chat_reasoning.py +4 -0
- lm_deluge/api_requests/gemini.py +166 -20
- lm_deluge/api_requests/openai.py +145 -25
- lm_deluge/batches.py +15 -45
- lm_deluge/client.py +309 -50
- lm_deluge/config.py +15 -3
- lm_deluge/models/__init__.py +14 -1
- lm_deluge/models/anthropic.py +29 -14
- lm_deluge/models/arcee.py +16 -0
- lm_deluge/models/deepseek.py +36 -4
- lm_deluge/models/google.py +42 -0
- lm_deluge/models/grok.py +24 -0
- lm_deluge/models/kimi.py +36 -0
- lm_deluge/models/minimax.py +18 -0
- lm_deluge/models/openai.py +100 -0
- lm_deluge/models/openrouter.py +133 -7
- lm_deluge/models/together.py +11 -0
- lm_deluge/models/zai.py +50 -0
- lm_deluge/pipelines/gepa/__init__.py +95 -0
- lm_deluge/pipelines/gepa/core.py +354 -0
- lm_deluge/pipelines/gepa/docs/samples.py +705 -0
- lm_deluge/pipelines/gepa/examples/01_synthetic_keywords.py +140 -0
- lm_deluge/pipelines/gepa/examples/02_gsm8k_math.py +261 -0
- lm_deluge/pipelines/gepa/examples/03_hotpotqa_multihop.py +300 -0
- lm_deluge/pipelines/gepa/examples/04_batch_classification.py +271 -0
- lm_deluge/pipelines/gepa/examples/simple_qa.py +129 -0
- lm_deluge/pipelines/gepa/optimizer.py +435 -0
- lm_deluge/pipelines/gepa/proposer.py +235 -0
- lm_deluge/pipelines/gepa/util.py +165 -0
- lm_deluge/{llm_tools → pipelines}/score.py +2 -2
- lm_deluge/{llm_tools → pipelines}/translate.py +5 -3
- lm_deluge/prompt.py +537 -88
- lm_deluge/request_context.py +7 -2
- lm_deluge/server/__init__.py +24 -0
- lm_deluge/server/__main__.py +144 -0
- lm_deluge/server/adapters.py +369 -0
- lm_deluge/server/app.py +388 -0
- lm_deluge/server/auth.py +71 -0
- lm_deluge/server/model_policy.py +215 -0
- lm_deluge/server/models_anthropic.py +172 -0
- lm_deluge/server/models_openai.py +175 -0
- lm_deluge/tool/__init__.py +1130 -0
- lm_deluge/tool/builtin/anthropic/__init__.py +300 -0
- lm_deluge/tool/builtin/anthropic/bash.py +0 -0
- lm_deluge/tool/builtin/anthropic/computer_use.py +0 -0
- lm_deluge/tool/builtin/gemini.py +59 -0
- lm_deluge/tool/builtin/openai.py +74 -0
- lm_deluge/tool/cua/__init__.py +173 -0
- lm_deluge/tool/cua/actions.py +148 -0
- lm_deluge/tool/cua/base.py +27 -0
- lm_deluge/tool/cua/batch.py +215 -0
- lm_deluge/tool/cua/converters.py +466 -0
- lm_deluge/tool/cua/kernel.py +702 -0
- lm_deluge/tool/cua/trycua.py +989 -0
- lm_deluge/tool/prefab/__init__.py +45 -0
- lm_deluge/tool/prefab/batch_tool.py +156 -0
- lm_deluge/tool/prefab/docs.py +1119 -0
- lm_deluge/tool/prefab/email.py +294 -0
- lm_deluge/tool/prefab/filesystem.py +1711 -0
- lm_deluge/tool/prefab/full_text_search/__init__.py +285 -0
- lm_deluge/tool/prefab/full_text_search/tantivy_index.py +396 -0
- lm_deluge/tool/prefab/memory.py +458 -0
- lm_deluge/tool/prefab/otc/__init__.py +165 -0
- lm_deluge/tool/prefab/otc/executor.py +281 -0
- lm_deluge/tool/prefab/otc/parse.py +188 -0
- lm_deluge/tool/prefab/random.py +212 -0
- lm_deluge/tool/prefab/rlm/__init__.py +296 -0
- lm_deluge/tool/prefab/rlm/executor.py +349 -0
- lm_deluge/tool/prefab/rlm/parse.py +144 -0
- lm_deluge/tool/prefab/sandbox/__init__.py +19 -0
- lm_deluge/tool/prefab/sandbox/daytona_sandbox.py +483 -0
- lm_deluge/tool/prefab/sandbox/docker_sandbox.py +609 -0
- lm_deluge/tool/prefab/sandbox/fargate_sandbox.py +546 -0
- lm_deluge/tool/prefab/sandbox/modal_sandbox.py +469 -0
- lm_deluge/tool/prefab/sandbox/seatbelt_sandbox.py +827 -0
- lm_deluge/tool/prefab/sheets.py +385 -0
- lm_deluge/tool/prefab/skills.py +0 -0
- lm_deluge/tool/prefab/subagents.py +233 -0
- lm_deluge/tool/prefab/todos.py +342 -0
- lm_deluge/tool/prefab/tool_search.py +169 -0
- lm_deluge/tool/prefab/web_search.py +199 -0
- lm_deluge/tracker.py +16 -13
- lm_deluge/util/schema.py +412 -0
- lm_deluge/warnings.py +8 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/METADATA +23 -9
- lm_deluge-0.0.90.dist-info/RECORD +132 -0
- lm_deluge/built_in_tools/anthropic/__init__.py +0 -128
- lm_deluge/built_in_tools/openai.py +0 -28
- lm_deluge/presets/cerebras.py +0 -17
- lm_deluge/presets/meta.py +0 -13
- lm_deluge/tool.py +0 -849
- lm_deluge-0.0.67.dist-info/RECORD +0 -72
- lm_deluge/{llm_tools → pipelines}/__init__.py +1 -1
- /lm_deluge/{llm_tools → pipelines}/classify.py +0 -0
- /lm_deluge/{llm_tools → pipelines}/extract.py +0 -0
- /lm_deluge/{llm_tools → pipelines}/locate.py +0 -0
- /lm_deluge/{llm_tools → pipelines}/ocr.py +0 -0
- /lm_deluge/{built_in_tools/anthropic/bash.py → skills/anthropic.py} +0 -0
- /lm_deluge/{built_in_tools/anthropic/computer_use.py → skills/compat.py} +0 -0
- /lm_deluge/{built_in_tools → tool/builtin}/anthropic/editor.py +0 -0
- /lm_deluge/{built_in_tools → tool/builtin}/base.py +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/WHEEL +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/licenses/LICENSE +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/top_level.txt +0 -0
lm_deluge/prompt.py
CHANGED
|
@@ -23,16 +23,95 @@ CachePattern = Literal[
|
|
|
23
23
|
# 1. Low-level content blocks – either text or an image #
|
|
24
24
|
###############################################################################
|
|
25
25
|
Role = Literal["system", "user", "assistant", "tool"]
|
|
26
|
+
SignatureProvider = Literal["anthropic", "gemini"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class ThoughtSignature:
|
|
31
|
+
value: str
|
|
32
|
+
provider: SignatureProvider | None = None
|
|
33
|
+
|
|
34
|
+
def for_provider(self, provider: SignatureProvider) -> str | None:
|
|
35
|
+
if self.provider is None or self.provider == provider:
|
|
36
|
+
return self.value
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
ThoughtSignatureLike: TypeAlias = ThoughtSignature | str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _normalize_signature(
|
|
44
|
+
signature: ThoughtSignatureLike | None,
|
|
45
|
+
*,
|
|
46
|
+
provider: SignatureProvider | None = None,
|
|
47
|
+
) -> ThoughtSignature | None:
|
|
48
|
+
if signature is None:
|
|
49
|
+
return None
|
|
50
|
+
if isinstance(signature, ThoughtSignature):
|
|
51
|
+
if provider is not None and signature.provider is None:
|
|
52
|
+
return ThoughtSignature(signature.value, provider)
|
|
53
|
+
return signature
|
|
54
|
+
return ThoughtSignature(signature, provider)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _signature_for_provider(
|
|
58
|
+
signature: ThoughtSignatureLike | None, provider: SignatureProvider
|
|
59
|
+
) -> str | None:
|
|
60
|
+
if signature is None:
|
|
61
|
+
return None
|
|
62
|
+
if isinstance(signature, ThoughtSignature):
|
|
63
|
+
return signature.for_provider(provider)
|
|
64
|
+
return signature
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _signature_value(signature: ThoughtSignatureLike | None) -> str | None:
|
|
68
|
+
if signature is None:
|
|
69
|
+
return None
|
|
70
|
+
if isinstance(signature, ThoughtSignature):
|
|
71
|
+
return signature.value
|
|
72
|
+
return signature
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _serialize_signature(signature: ThoughtSignatureLike | None) -> str | dict | None:
|
|
76
|
+
if signature is None:
|
|
77
|
+
return None
|
|
78
|
+
if isinstance(signature, ThoughtSignature):
|
|
79
|
+
if signature.provider is None:
|
|
80
|
+
return signature.value
|
|
81
|
+
return {"value": signature.value, "provider": signature.provider}
|
|
82
|
+
return signature
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _deserialize_signature(payload: str | dict | None) -> ThoughtSignature | None:
|
|
86
|
+
if payload is None:
|
|
87
|
+
return None
|
|
88
|
+
if isinstance(payload, dict):
|
|
89
|
+
value = payload.get("value")
|
|
90
|
+
provider = payload.get("provider")
|
|
91
|
+
if isinstance(value, str):
|
|
92
|
+
if provider in ("anthropic", "gemini"):
|
|
93
|
+
return ThoughtSignature(value, provider)
|
|
94
|
+
return ThoughtSignature(value)
|
|
95
|
+
return None
|
|
96
|
+
if isinstance(payload, str):
|
|
97
|
+
return ThoughtSignature(payload)
|
|
98
|
+
return None
|
|
26
99
|
|
|
27
100
|
|
|
28
101
|
@dataclass(slots=True)
|
|
29
102
|
class Text:
|
|
30
103
|
text: str
|
|
31
104
|
type: str = field(init=False, default="text")
|
|
105
|
+
# for gemini 3 - thought signatures to maintain reasoning context
|
|
106
|
+
thought_signature: ThoughtSignatureLike | None = None
|
|
107
|
+
|
|
108
|
+
def __post_init__(self) -> None:
|
|
109
|
+
self.thought_signature = _normalize_signature(self.thought_signature)
|
|
32
110
|
|
|
33
111
|
@property
|
|
34
112
|
def fingerprint(self) -> str:
|
|
35
|
-
|
|
113
|
+
signature = _signature_value(self.thought_signature) or ""
|
|
114
|
+
return xxhash.xxh64(f"{self.text}:{signature}".encode()).hexdigest()
|
|
36
115
|
|
|
37
116
|
# ── provider-specific emission ────────────────────────────────────────────
|
|
38
117
|
def oa_chat(self) -> dict | str: # OpenAI Chat Completions
|
|
@@ -45,7 +124,11 @@ class Text:
|
|
|
45
124
|
return {"type": "text", "text": self.text}
|
|
46
125
|
|
|
47
126
|
def gemini(self) -> dict:
|
|
48
|
-
|
|
127
|
+
result = {"text": self.text}
|
|
128
|
+
signature = _signature_for_provider(self.thought_signature, "gemini")
|
|
129
|
+
if signature is not None:
|
|
130
|
+
result["thoughtSignature"] = signature
|
|
131
|
+
return result
|
|
49
132
|
|
|
50
133
|
def mistral(self) -> dict:
|
|
51
134
|
return {"type": "text", "text": self.text}
|
|
@@ -61,6 +144,11 @@ class ToolCall:
|
|
|
61
144
|
built_in: bool = False
|
|
62
145
|
built_in_type: str | None = None
|
|
63
146
|
extra_body: dict | None = None
|
|
147
|
+
# for gemini 3 - thought signatures to maintain reasoning context
|
|
148
|
+
thought_signature: ThoughtSignatureLike | None = None
|
|
149
|
+
|
|
150
|
+
def __post_init__(self) -> None:
|
|
151
|
+
self.thought_signature = _normalize_signature(self.thought_signature)
|
|
64
152
|
|
|
65
153
|
@property
|
|
66
154
|
def fingerprint(self) -> str:
|
|
@@ -93,7 +181,11 @@ class ToolCall:
|
|
|
93
181
|
}
|
|
94
182
|
|
|
95
183
|
def gemini(self) -> dict:
|
|
96
|
-
|
|
184
|
+
result = {"functionCall": {"name": self.name, "args": self.arguments}}
|
|
185
|
+
signature = _signature_for_provider(self.thought_signature, "gemini")
|
|
186
|
+
if signature is not None:
|
|
187
|
+
result["thoughtSignature"] = signature # type: ignore
|
|
188
|
+
return result
|
|
97
189
|
|
|
98
190
|
def mistral(self) -> dict:
|
|
99
191
|
return {
|
|
@@ -198,6 +290,8 @@ class ToolResult:
|
|
|
198
290
|
"call_id": self.tool_call_id,
|
|
199
291
|
}
|
|
200
292
|
if self.built_in_type == "computer_call":
|
|
293
|
+
# OpenAI expects "computer_call_output" for the result type
|
|
294
|
+
result["type"] = "computer_call_output"
|
|
201
295
|
result["output"] = output_data.get("output", {})
|
|
202
296
|
if "acknowledged_safety_checks" in output_data:
|
|
203
297
|
result["acknowledged_safety_checks"] = output_data[
|
|
@@ -230,15 +324,41 @@ class ToolResult:
|
|
|
230
324
|
raise ValueError("unsupported self.result type")
|
|
231
325
|
|
|
232
326
|
def gemini(self) -> dict:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
"functionResponse": {
|
|
237
|
-
"name": self.tool_call_id, # Gemini uses name field for ID
|
|
238
|
-
"response": {"result": self.result},
|
|
239
|
-
}
|
|
327
|
+
# Build the function response
|
|
328
|
+
func_response: dict = {
|
|
329
|
+
"name": self.tool_call_id, # Gemini uses name field for ID
|
|
240
330
|
}
|
|
241
331
|
|
|
332
|
+
# Handle different result types
|
|
333
|
+
if isinstance(self.result, str):
|
|
334
|
+
func_response["response"] = {"result": self.result}
|
|
335
|
+
elif isinstance(self.result, dict):
|
|
336
|
+
# Check for Gemini computer use format with inline screenshot
|
|
337
|
+
if self.built_in_type == "gemini_computer_use":
|
|
338
|
+
# Gemini CU expects response dict with optional inline_data parts
|
|
339
|
+
func_response["response"] = self.result.get("response", {})
|
|
340
|
+
# Include inline data (screenshot) if present
|
|
341
|
+
if "inline_data" in self.result:
|
|
342
|
+
func_response["parts"] = [
|
|
343
|
+
{
|
|
344
|
+
"inlineData": {
|
|
345
|
+
"mimeType": self.result["inline_data"].get(
|
|
346
|
+
"mime_type", "image/png"
|
|
347
|
+
),
|
|
348
|
+
"data": self.result["inline_data"]["data"],
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
]
|
|
352
|
+
else:
|
|
353
|
+
func_response["response"] = self.result
|
|
354
|
+
elif isinstance(self.result, list):
|
|
355
|
+
# Handle content blocks (images, etc.) - not yet implemented
|
|
356
|
+
raise ValueError("can't handle content blocks for gemini yet")
|
|
357
|
+
else:
|
|
358
|
+
func_response["response"] = {"result": str(self.result)}
|
|
359
|
+
|
|
360
|
+
return {"functionResponse": func_response}
|
|
361
|
+
|
|
242
362
|
def mistral(self) -> dict:
|
|
243
363
|
return {
|
|
244
364
|
"type": "tool_result",
|
|
@@ -253,6 +373,12 @@ class Thinking:
|
|
|
253
373
|
type: str = field(init=False, default="thinking")
|
|
254
374
|
# for openai - to keep conversation chain
|
|
255
375
|
raw_payload: dict | None = None
|
|
376
|
+
# for gemini 3 - thought signatures to maintain reasoning context
|
|
377
|
+
thought_signature: ThoughtSignatureLike | None = None
|
|
378
|
+
summary: str | None = None # to differentiate summary text from actual content
|
|
379
|
+
|
|
380
|
+
def __post_init__(self) -> None:
|
|
381
|
+
self.thought_signature = _normalize_signature(self.thought_signature)
|
|
256
382
|
|
|
257
383
|
@property
|
|
258
384
|
def fingerprint(self) -> str:
|
|
@@ -267,10 +393,20 @@ class Thinking:
|
|
|
267
393
|
return {"type": "reasoning", "content": self.content}
|
|
268
394
|
|
|
269
395
|
def anthropic(self) -> dict: # Anthropic Messages
|
|
270
|
-
|
|
396
|
+
if self.raw_payload:
|
|
397
|
+
return dict(self.raw_payload)
|
|
398
|
+
result = {"type": "thinking", "thinking": self.content}
|
|
399
|
+
signature = _signature_for_provider(self.thought_signature, "anthropic")
|
|
400
|
+
if signature is not None:
|
|
401
|
+
result["signature"] = signature
|
|
402
|
+
return result
|
|
271
403
|
|
|
272
404
|
def gemini(self) -> dict:
|
|
273
|
-
|
|
405
|
+
result = {"text": f"[Thinking: {self.content}]"}
|
|
406
|
+
signature = _signature_for_provider(self.thought_signature, "gemini")
|
|
407
|
+
if signature is not None:
|
|
408
|
+
result["thoughtSignature"] = signature
|
|
409
|
+
return result
|
|
274
410
|
|
|
275
411
|
def mistral(self) -> dict:
|
|
276
412
|
return {"type": "text", "text": f"[Thinking: {self.content}]"}
|
|
@@ -341,10 +477,15 @@ class Message:
|
|
|
341
477
|
# return {"type": "file", "tag": f"<File ({size} bytes)>"}
|
|
342
478
|
# return repr(value)
|
|
343
479
|
|
|
344
|
-
def to_log(self) -> dict:
|
|
480
|
+
def to_log(self, *, preserve_media: bool = False) -> dict:
|
|
345
481
|
"""
|
|
346
482
|
Return a JSON-serialisable dict that fully captures the message.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
preserve_media: If True, store full base64-encoded bytes for images and files.
|
|
486
|
+
If False (default), replace with placeholder tags.
|
|
347
487
|
"""
|
|
488
|
+
import base64
|
|
348
489
|
|
|
349
490
|
def _json_safe(value):
|
|
350
491
|
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
@@ -366,22 +507,52 @@ class Message:
|
|
|
366
507
|
content_blocks: list[dict] = []
|
|
367
508
|
for p in self.parts:
|
|
368
509
|
if isinstance(p, Text):
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
510
|
+
text_block: dict = {"type": "text", "text": p.text}
|
|
511
|
+
signature = _serialize_signature(p.thought_signature)
|
|
512
|
+
if signature is not None:
|
|
513
|
+
text_block["thought_signature"] = signature
|
|
514
|
+
content_blocks.append(text_block)
|
|
515
|
+
elif isinstance(p, Image):
|
|
516
|
+
if preserve_media:
|
|
517
|
+
content_blocks.append(
|
|
518
|
+
{
|
|
519
|
+
"type": "image",
|
|
520
|
+
"data": base64.b64encode(p._bytes()).decode("ascii"),
|
|
521
|
+
"media_type": p.media_type,
|
|
522
|
+
"detail": p.detail,
|
|
523
|
+
}
|
|
524
|
+
)
|
|
525
|
+
else:
|
|
526
|
+
w, h = p.size
|
|
527
|
+
content_blocks.append(
|
|
528
|
+
{"type": "image", "tag": f"<Image ({w}×{h})>"}
|
|
529
|
+
)
|
|
530
|
+
elif isinstance(p, File):
|
|
531
|
+
if preserve_media:
|
|
532
|
+
content_blocks.append(
|
|
533
|
+
{
|
|
534
|
+
"type": "file",
|
|
535
|
+
"data": base64.b64encode(p._bytes()).decode("ascii"),
|
|
536
|
+
"media_type": p.media_type,
|
|
537
|
+
"filename": p.filename,
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
else:
|
|
541
|
+
size = p.size
|
|
542
|
+
content_blocks.append(
|
|
543
|
+
{"type": "file", "tag": f"<File ({size} bytes)>"}
|
|
544
|
+
)
|
|
376
545
|
elif isinstance(p, ToolCall):
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
546
|
+
tool_call_block = {
|
|
547
|
+
"type": "tool_call",
|
|
548
|
+
"id": p.id,
|
|
549
|
+
"name": p.name,
|
|
550
|
+
"arguments": _json_safe(p.arguments),
|
|
551
|
+
}
|
|
552
|
+
signature = _serialize_signature(p.thought_signature)
|
|
553
|
+
if signature is not None:
|
|
554
|
+
tool_call_block["thought_signature"] = signature
|
|
555
|
+
content_blocks.append(tool_call_block)
|
|
385
556
|
elif isinstance(p, ToolResult):
|
|
386
557
|
content_blocks.append(
|
|
387
558
|
{
|
|
@@ -391,38 +562,82 @@ class Message:
|
|
|
391
562
|
}
|
|
392
563
|
)
|
|
393
564
|
elif isinstance(p, Thinking):
|
|
394
|
-
|
|
565
|
+
thinking_block: dict = {"type": "thinking", "content": p.content}
|
|
566
|
+
signature = _serialize_signature(p.thought_signature)
|
|
567
|
+
if signature is not None:
|
|
568
|
+
thinking_block["thought_signature"] = signature
|
|
569
|
+
content_blocks.append(thinking_block)
|
|
395
570
|
|
|
396
571
|
return {"role": self.role, "content": content_blocks}
|
|
397
572
|
|
|
398
573
|
@classmethod
|
|
399
574
|
def from_log(cls, data: dict) -> "Message":
|
|
400
575
|
"""Re-hydrate a Message previously produced by `to_log()`."""
|
|
401
|
-
|
|
402
|
-
|
|
576
|
+
import base64
|
|
577
|
+
|
|
403
578
|
role: Role = data["role"]
|
|
404
579
|
parts: list[Part] = []
|
|
405
580
|
|
|
406
581
|
for p in data["content"]:
|
|
407
582
|
if p["type"] == "text":
|
|
408
|
-
parts.append(
|
|
583
|
+
parts.append(
|
|
584
|
+
Text(
|
|
585
|
+
p["text"],
|
|
586
|
+
thought_signature=_deserialize_signature(
|
|
587
|
+
p.get("thought_signature")
|
|
588
|
+
),
|
|
589
|
+
)
|
|
590
|
+
)
|
|
409
591
|
elif p["type"] == "image":
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
592
|
+
if "data" in p:
|
|
593
|
+
# Full image data was preserved
|
|
594
|
+
parts.append(
|
|
595
|
+
Image(
|
|
596
|
+
data=base64.b64decode(p["data"]),
|
|
597
|
+
media_type=p.get("media_type"),
|
|
598
|
+
detail=p.get("detail", "auto"),
|
|
599
|
+
)
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
# Placeholder tag only
|
|
603
|
+
parts.append(Text(p["tag"]))
|
|
413
604
|
elif p["type"] == "file":
|
|
414
|
-
|
|
415
|
-
|
|
605
|
+
if "data" in p:
|
|
606
|
+
# Full file data was preserved
|
|
607
|
+
parts.append(
|
|
608
|
+
File(
|
|
609
|
+
data=base64.b64decode(p["data"]),
|
|
610
|
+
media_type=p.get("media_type"),
|
|
611
|
+
filename=p.get("filename"),
|
|
612
|
+
)
|
|
613
|
+
)
|
|
614
|
+
else:
|
|
615
|
+
# Placeholder tag only
|
|
616
|
+
parts.append(Text(p["tag"]))
|
|
416
617
|
elif p["type"] == "tool_call":
|
|
417
618
|
parts.append(
|
|
418
|
-
ToolCall(
|
|
619
|
+
ToolCall(
|
|
620
|
+
id=p["id"],
|
|
621
|
+
name=p["name"],
|
|
622
|
+
arguments=p["arguments"],
|
|
623
|
+
thought_signature=_deserialize_signature(
|
|
624
|
+
p.get("thought_signature")
|
|
625
|
+
),
|
|
626
|
+
)
|
|
419
627
|
)
|
|
420
628
|
elif p["type"] == "tool_result":
|
|
421
629
|
parts.append(
|
|
422
630
|
ToolResult(tool_call_id=p["tool_call_id"], result=p["result"])
|
|
423
631
|
)
|
|
424
632
|
elif p["type"] == "thinking":
|
|
425
|
-
parts.append(
|
|
633
|
+
parts.append(
|
|
634
|
+
Thinking(
|
|
635
|
+
content=p["content"],
|
|
636
|
+
thought_signature=_deserialize_signature(
|
|
637
|
+
p.get("thought_signature")
|
|
638
|
+
),
|
|
639
|
+
)
|
|
640
|
+
)
|
|
426
641
|
else:
|
|
427
642
|
raise ValueError(f"Unknown part type {p['type']!r}")
|
|
428
643
|
|
|
@@ -753,7 +968,15 @@ class Message:
|
|
|
753
968
|
# Anthropic: system message is *not* in the list
|
|
754
969
|
if self.role == "system":
|
|
755
970
|
raise ValueError("Anthropic keeps system outside message list")
|
|
756
|
-
content = [
|
|
971
|
+
content: list[dict] = []
|
|
972
|
+
for part in self.parts:
|
|
973
|
+
if isinstance(part, Thinking) and part.raw_payload is None:
|
|
974
|
+
signature = _signature_for_provider(part.thought_signature, "anthropic")
|
|
975
|
+
if signature is None:
|
|
976
|
+
continue
|
|
977
|
+
content.append(part.anthropic())
|
|
978
|
+
if not content:
|
|
979
|
+
content = [{"type": "text", "text": ""}]
|
|
757
980
|
# Shortcut: single text becomes a bare string
|
|
758
981
|
if len(content) == 1 and content[0].get("type") == "text":
|
|
759
982
|
content = content[0]["text"]
|
|
@@ -848,14 +1071,16 @@ class Conversation:
|
|
|
848
1071
|
if content is None:
|
|
849
1072
|
return parts
|
|
850
1073
|
if isinstance(content, str):
|
|
851
|
-
|
|
1074
|
+
if content.strip():
|
|
1075
|
+
parts.append(Text(content))
|
|
852
1076
|
return parts
|
|
853
1077
|
|
|
854
1078
|
for block in content:
|
|
855
1079
|
block_type = block.get("type")
|
|
856
1080
|
if block_type in text_types:
|
|
857
1081
|
text_value = block.get("text") or block.get(block_type) or ""
|
|
858
|
-
|
|
1082
|
+
if text_value.strip():
|
|
1083
|
+
parts.append(Text(text_value))
|
|
859
1084
|
elif block_type in image_types:
|
|
860
1085
|
parts.append(_to_image_from_url(block))
|
|
861
1086
|
elif block_type in file_types:
|
|
@@ -1001,7 +1226,8 @@ class Conversation:
|
|
|
1001
1226
|
)
|
|
1002
1227
|
)
|
|
1003
1228
|
|
|
1004
|
-
|
|
1229
|
+
if parts:
|
|
1230
|
+
conversation_messages.append(Message(mapped_role, parts))
|
|
1005
1231
|
|
|
1006
1232
|
return cls(conversation_messages)
|
|
1007
1233
|
|
|
@@ -1079,7 +1305,9 @@ class Conversation:
|
|
|
1079
1305
|
return result_parts
|
|
1080
1306
|
|
|
1081
1307
|
def _anthropic_content_to_parts(
|
|
1082
|
-
role: Role,
|
|
1308
|
+
role: Role,
|
|
1309
|
+
content: str | list[dict] | None,
|
|
1310
|
+
signature_state: dict[str, ThoughtSignature | None] | None = None,
|
|
1083
1311
|
) -> list[Part]:
|
|
1084
1312
|
parts: list[Part] = []
|
|
1085
1313
|
if content is None:
|
|
@@ -1108,15 +1336,38 @@ class Conversation:
|
|
|
1108
1336
|
raise ValueError("Anthropic tool_use block missing id")
|
|
1109
1337
|
name = block.get("name") or "tool"
|
|
1110
1338
|
arguments = block.get("input") or {}
|
|
1339
|
+
tool_call = ToolCall(
|
|
1340
|
+
id=tool_id,
|
|
1341
|
+
name=name,
|
|
1342
|
+
arguments=arguments
|
|
1343
|
+
if isinstance(arguments, dict)
|
|
1344
|
+
else {"value": arguments},
|
|
1345
|
+
)
|
|
1346
|
+
if signature_state is not None:
|
|
1347
|
+
pending_signature = signature_state.get("pending")
|
|
1348
|
+
if pending_signature:
|
|
1349
|
+
tool_call.thought_signature = pending_signature
|
|
1350
|
+
signature_state["pending"] = None
|
|
1351
|
+
parts.append(tool_call)
|
|
1352
|
+
elif block_type == "redacted_thinking":
|
|
1111
1353
|
parts.append(
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1354
|
+
Thinking(content=block.get("data", ""), raw_payload=block)
|
|
1355
|
+
)
|
|
1356
|
+
elif block_type == "thinking":
|
|
1357
|
+
thinking_content = block.get("thinking", "")
|
|
1358
|
+
signature = _normalize_signature(
|
|
1359
|
+
block.get("signature"),
|
|
1360
|
+
provider="anthropic",
|
|
1361
|
+
)
|
|
1362
|
+
parts.append(
|
|
1363
|
+
Thinking(
|
|
1364
|
+
content=thinking_content,
|
|
1365
|
+
raw_payload=block,
|
|
1366
|
+
thought_signature=signature,
|
|
1118
1367
|
)
|
|
1119
1368
|
)
|
|
1369
|
+
if signature_state is not None and signature is not None:
|
|
1370
|
+
signature_state["pending"] = signature
|
|
1120
1371
|
elif block_type == "tool_result":
|
|
1121
1372
|
tool_use_id = block.get("tool_use_id")
|
|
1122
1373
|
if tool_use_id is None:
|
|
@@ -1126,9 +1377,6 @@ class Conversation:
|
|
|
1126
1377
|
result = _anthropic_tool_result_content(block.get("content"))
|
|
1127
1378
|
tool_result = ToolResult(tool_call_id=tool_use_id, result=result)
|
|
1128
1379
|
parts.append(tool_result)
|
|
1129
|
-
elif block_type == "thinking":
|
|
1130
|
-
thinking_content = block.get("thinking", "")
|
|
1131
|
-
parts.append(Thinking(content=thinking_content, raw_payload=block))
|
|
1132
1380
|
else:
|
|
1133
1381
|
parts.append(Text(json.dumps(block)))
|
|
1134
1382
|
return parts
|
|
@@ -1158,6 +1406,9 @@ class Conversation:
|
|
|
1158
1406
|
content = message.get("content")
|
|
1159
1407
|
if isinstance(content, list):
|
|
1160
1408
|
buffer_parts: list[Part] = []
|
|
1409
|
+
signature_state: None | dict[str, ThoughtSignature | None] = (
|
|
1410
|
+
{"pending": None} if base_role == "assistant" else None
|
|
1411
|
+
)
|
|
1161
1412
|
for block in content:
|
|
1162
1413
|
block_type = block.get("type")
|
|
1163
1414
|
if block_type == "tool_result":
|
|
@@ -1179,7 +1430,11 @@ class Conversation:
|
|
|
1179
1430
|
)
|
|
1180
1431
|
)
|
|
1181
1432
|
else:
|
|
1182
|
-
block_parts = _anthropic_content_to_parts(
|
|
1433
|
+
block_parts = _anthropic_content_to_parts(
|
|
1434
|
+
base_role,
|
|
1435
|
+
[block],
|
|
1436
|
+
signature_state=signature_state,
|
|
1437
|
+
)
|
|
1183
1438
|
buffer_parts.extend(block_parts)
|
|
1184
1439
|
|
|
1185
1440
|
if buffer_parts:
|
|
@@ -1192,14 +1447,24 @@ class Conversation:
|
|
|
1192
1447
|
|
|
1193
1448
|
@classmethod
|
|
1194
1449
|
def from_unknown(
|
|
1195
|
-
cls, messages: list[dict], *, system: str | list[dict] | None = None
|
|
1450
|
+
cls, messages: list[dict] | dict, *, system: str | list[dict] | None = None
|
|
1196
1451
|
) -> tuple["Conversation", str]:
|
|
1197
1452
|
"""Attempt to convert provider-formatted messages without knowing the provider.
|
|
1198
1453
|
|
|
1199
1454
|
Returns the parsed conversation together with the provider label that succeeded
|
|
1200
|
-
("openai" or "
|
|
1455
|
+
("openai", "anthropic", or "log").
|
|
1201
1456
|
"""
|
|
1202
1457
|
|
|
1458
|
+
# Check if input is in log format (output from to_log())
|
|
1459
|
+
if isinstance(messages, dict) and "messages" in messages:
|
|
1460
|
+
return cls.from_log(messages), "log"
|
|
1461
|
+
|
|
1462
|
+
# Ensure messages is a list for provider detection
|
|
1463
|
+
if not isinstance(messages, list):
|
|
1464
|
+
raise ValueError(
|
|
1465
|
+
"messages must be a list of dicts or a dict with 'messages' key"
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1203
1468
|
def _detect_provider() -> str:
|
|
1204
1469
|
has_openai_markers = False
|
|
1205
1470
|
has_anthropic_markers = False
|
|
@@ -1330,14 +1595,14 @@ class Conversation:
|
|
|
1330
1595
|
# For assistant messages, extract computer calls as separate items
|
|
1331
1596
|
text_parts = []
|
|
1332
1597
|
for p in m.parts:
|
|
1333
|
-
if isinstance(p, ToolCall) and p.
|
|
1598
|
+
if isinstance(p, ToolCall) and p.built_in_type == "computer_call":
|
|
1334
1599
|
# Computer calls become separate items in the input array
|
|
1335
|
-
|
|
1600
|
+
# p.arguments already contains the full action dict with "type"
|
|
1336
1601
|
input_items.append(
|
|
1337
1602
|
{
|
|
1338
1603
|
"type": "computer_call",
|
|
1339
1604
|
"call_id": p.id,
|
|
1340
|
-
"action":
|
|
1605
|
+
"action": p.arguments,
|
|
1341
1606
|
}
|
|
1342
1607
|
)
|
|
1343
1608
|
elif isinstance(p, Text):
|
|
@@ -1511,36 +1776,68 @@ class Conversation:
|
|
|
1511
1776
|
hasher.update(json.dumps([m.fingerprint for m in self.messages]).encode())
|
|
1512
1777
|
return hasher.hexdigest()
|
|
1513
1778
|
|
|
1514
|
-
def to_log(self) -> dict:
|
|
1779
|
+
def to_log(self, *, preserve_media: bool = False) -> dict:
|
|
1515
1780
|
"""
|
|
1516
1781
|
Return a JSON-serialisable dict that fully captures the conversation.
|
|
1782
|
+
|
|
1783
|
+
Args:
|
|
1784
|
+
preserve_media: If True, store full base64-encoded bytes for images and files.
|
|
1785
|
+
If False (default), replace with placeholder tags.
|
|
1517
1786
|
"""
|
|
1787
|
+
import base64
|
|
1788
|
+
|
|
1518
1789
|
serialized: list[dict] = []
|
|
1519
1790
|
|
|
1520
1791
|
for msg in self.messages:
|
|
1521
1792
|
content_blocks: list[dict] = []
|
|
1522
1793
|
for p in msg.parts:
|
|
1523
1794
|
if isinstance(p, Text):
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1795
|
+
text_block: dict = {"type": "text", "text": p.text}
|
|
1796
|
+
signature = _serialize_signature(p.thought_signature)
|
|
1797
|
+
if signature is not None:
|
|
1798
|
+
text_block["thought_signature"] = signature
|
|
1799
|
+
content_blocks.append(text_block)
|
|
1800
|
+
elif isinstance(p, Image):
|
|
1801
|
+
if preserve_media:
|
|
1802
|
+
content_blocks.append(
|
|
1803
|
+
{
|
|
1804
|
+
"type": "image",
|
|
1805
|
+
"data": base64.b64encode(p._bytes()).decode("ascii"),
|
|
1806
|
+
"media_type": p.media_type,
|
|
1807
|
+
"detail": p.detail,
|
|
1808
|
+
}
|
|
1809
|
+
)
|
|
1810
|
+
else:
|
|
1811
|
+
w, h = p.size
|
|
1812
|
+
content_blocks.append(
|
|
1813
|
+
{"type": "image", "tag": f"<Image ({w}×{h})>"}
|
|
1814
|
+
)
|
|
1815
|
+
elif isinstance(p, File):
|
|
1816
|
+
if preserve_media:
|
|
1817
|
+
content_blocks.append(
|
|
1818
|
+
{
|
|
1819
|
+
"type": "file",
|
|
1820
|
+
"data": base64.b64encode(p._bytes()).decode("ascii"),
|
|
1821
|
+
"media_type": p.media_type,
|
|
1822
|
+
"filename": p.filename,
|
|
1823
|
+
}
|
|
1824
|
+
)
|
|
1825
|
+
else:
|
|
1826
|
+
size = p.size
|
|
1827
|
+
content_blocks.append(
|
|
1828
|
+
{"type": "file", "tag": f"<File ({size} bytes)>"}
|
|
1829
|
+
)
|
|
1535
1830
|
elif isinstance(p, ToolCall):
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1831
|
+
tool_call_block = {
|
|
1832
|
+
"type": "tool_call",
|
|
1833
|
+
"id": p.id,
|
|
1834
|
+
"name": p.name,
|
|
1835
|
+
"arguments": p.arguments,
|
|
1836
|
+
}
|
|
1837
|
+
signature = _serialize_signature(p.thought_signature)
|
|
1838
|
+
if signature is not None:
|
|
1839
|
+
tool_call_block["thought_signature"] = signature
|
|
1840
|
+
content_blocks.append(tool_call_block)
|
|
1544
1841
|
elif isinstance(p, ToolResult):
|
|
1545
1842
|
content_blocks.append(
|
|
1546
1843
|
{
|
|
@@ -1552,14 +1849,125 @@ class Conversation:
|
|
|
1552
1849
|
}
|
|
1553
1850
|
)
|
|
1554
1851
|
elif isinstance(p, Thinking):
|
|
1555
|
-
|
|
1852
|
+
thinking_block: dict = {"type": "thinking", "content": p.content}
|
|
1853
|
+
signature = _serialize_signature(p.thought_signature)
|
|
1854
|
+
if signature is not None:
|
|
1855
|
+
thinking_block["thought_signature"] = signature
|
|
1856
|
+
content_blocks.append(thinking_block)
|
|
1556
1857
|
serialized.append({"role": msg.role, "content": content_blocks})
|
|
1557
1858
|
|
|
1558
1859
|
return {"messages": serialized}
|
|
1559
1860
|
|
|
1861
|
+
def print(self, max_text_length: int = 500, indent: int = 2) -> None:
|
|
1862
|
+
"""Pretty-print the conversation to stdout.
|
|
1863
|
+
|
|
1864
|
+
Args:
|
|
1865
|
+
max_text_length: Truncate text content longer than this (default 500 chars)
|
|
1866
|
+
indent: JSON indentation for tool calls/results (default 2)
|
|
1867
|
+
"""
|
|
1868
|
+
ROLE_COLORS = {
|
|
1869
|
+
"system": "\033[95m", # magenta
|
|
1870
|
+
"user": "\033[94m", # blue
|
|
1871
|
+
"assistant": "\033[92m", # green
|
|
1872
|
+
"tool": "\033[93m", # yellow
|
|
1873
|
+
}
|
|
1874
|
+
RESET = "\033[0m"
|
|
1875
|
+
DIM = "\033[2m"
|
|
1876
|
+
BOLD = "\033[1m"
|
|
1877
|
+
|
|
1878
|
+
def truncate(text: str, max_len: int) -> str:
|
|
1879
|
+
if len(text) <= max_len:
|
|
1880
|
+
return text
|
|
1881
|
+
return (
|
|
1882
|
+
text[:max_len] + f"{DIM}... [{len(text) - max_len} more chars]{RESET}"
|
|
1883
|
+
)
|
|
1884
|
+
|
|
1885
|
+
def format_json(obj: dict | list, ind: int) -> str:
|
|
1886
|
+
return json.dumps(obj, indent=ind, ensure_ascii=False)
|
|
1887
|
+
|
|
1888
|
+
print(f"\n{BOLD}{'=' * 60}{RESET}")
|
|
1889
|
+
print(f"{BOLD}Conversation ({len(self.messages)} messages){RESET}")
|
|
1890
|
+
print(f"{BOLD}{'=' * 60}{RESET}\n")
|
|
1891
|
+
|
|
1892
|
+
for i, msg in enumerate(self.messages):
|
|
1893
|
+
role_color = ROLE_COLORS.get(msg.role, "")
|
|
1894
|
+
print(f"{role_color}{BOLD}[{msg.role.upper()}]{RESET}")
|
|
1895
|
+
|
|
1896
|
+
for part in msg.parts:
|
|
1897
|
+
if isinstance(part, Text):
|
|
1898
|
+
text = truncate(part.text, max_text_length)
|
|
1899
|
+
# Indent multiline text
|
|
1900
|
+
lines = text.split("\n")
|
|
1901
|
+
if len(lines) > 1:
|
|
1902
|
+
print(" " + "\n ".join(lines))
|
|
1903
|
+
else:
|
|
1904
|
+
print(f" {text}")
|
|
1905
|
+
|
|
1906
|
+
elif isinstance(part, Image):
|
|
1907
|
+
w, h = part.size
|
|
1908
|
+
print(f" {DIM}<Image ({w}x{h})>{RESET}")
|
|
1909
|
+
|
|
1910
|
+
elif isinstance(part, File):
|
|
1911
|
+
size = part.size
|
|
1912
|
+
filename = getattr(part, "filename", None)
|
|
1913
|
+
if filename:
|
|
1914
|
+
print(f" {DIM}<File: {filename} ({size} bytes)>{RESET}")
|
|
1915
|
+
else:
|
|
1916
|
+
print(f" {DIM}<File ({size} bytes)>{RESET}")
|
|
1917
|
+
|
|
1918
|
+
elif isinstance(part, ToolCall):
|
|
1919
|
+
print(
|
|
1920
|
+
f" {DIM}Tool Call:{RESET} {BOLD}{part.name}{RESET} (id: {part.id})"
|
|
1921
|
+
)
|
|
1922
|
+
if part.arguments:
|
|
1923
|
+
args_json = format_json(part.arguments, indent)
|
|
1924
|
+
# Indent the JSON
|
|
1925
|
+
indented = "\n".join(
|
|
1926
|
+
" " + line for line in args_json.split("\n")
|
|
1927
|
+
)
|
|
1928
|
+
print(indented)
|
|
1929
|
+
|
|
1930
|
+
elif isinstance(part, ToolResult):
|
|
1931
|
+
print(f" {DIM}Tool Result:{RESET} (call_id: {part.tool_call_id})")
|
|
1932
|
+
if isinstance(part.result, str):
|
|
1933
|
+
result_text = truncate(part.result, max_text_length)
|
|
1934
|
+
lines = result_text.split("\n")
|
|
1935
|
+
for line in lines:
|
|
1936
|
+
print(f" {line}")
|
|
1937
|
+
elif isinstance(part.result, dict):
|
|
1938
|
+
result_json = format_json(part.result, indent)
|
|
1939
|
+
indented = "\n".join(
|
|
1940
|
+
" " + line for line in result_json.split("\n")
|
|
1941
|
+
)
|
|
1942
|
+
print(indented)
|
|
1943
|
+
elif isinstance(part.result, list):
|
|
1944
|
+
print(f" {DIM}<{len(part.result)} content blocks>{RESET}")
|
|
1945
|
+
for block in part.result:
|
|
1946
|
+
if isinstance(block, Text):
|
|
1947
|
+
block_text = truncate(block.text, max_text_length // 2)
|
|
1948
|
+
print(f" [text] {block_text}")
|
|
1949
|
+
elif isinstance(block, Image):
|
|
1950
|
+
bw, bh = block.size
|
|
1951
|
+
print(f" {DIM}<Image ({bw}x{bh})>{RESET}")
|
|
1952
|
+
|
|
1953
|
+
elif isinstance(part, Thinking):
|
|
1954
|
+
print(f" {DIM}Thinking:{RESET}")
|
|
1955
|
+
thought = truncate(part.content, max_text_length)
|
|
1956
|
+
lines = thought.split("\n")
|
|
1957
|
+
for line in lines:
|
|
1958
|
+
print(f" {DIM}{line}{RESET}")
|
|
1959
|
+
|
|
1960
|
+
# Separator between messages
|
|
1961
|
+
if i < len(self.messages) - 1:
|
|
1962
|
+
print(f"\n{'-' * 40}\n")
|
|
1963
|
+
|
|
1964
|
+
print(f"\n{BOLD}{'=' * 60}{RESET}\n")
|
|
1965
|
+
|
|
1560
1966
|
@classmethod
|
|
1561
1967
|
def from_log(cls, payload: dict) -> "Conversation":
|
|
1562
1968
|
"""Re-hydrate a Conversation previously produced by `to_log()`."""
|
|
1969
|
+
import base64
|
|
1970
|
+
|
|
1563
1971
|
msgs: list[Message] = []
|
|
1564
1972
|
|
|
1565
1973
|
for m in payload.get("messages", []):
|
|
@@ -1568,23 +1976,64 @@ class Conversation:
|
|
|
1568
1976
|
|
|
1569
1977
|
for p in m["content"]:
|
|
1570
1978
|
if p["type"] == "text":
|
|
1571
|
-
parts.append(
|
|
1979
|
+
parts.append(
|
|
1980
|
+
Text(
|
|
1981
|
+
p["text"],
|
|
1982
|
+
thought_signature=_deserialize_signature(
|
|
1983
|
+
p.get("thought_signature")
|
|
1984
|
+
),
|
|
1985
|
+
)
|
|
1986
|
+
)
|
|
1572
1987
|
elif p["type"] == "image":
|
|
1573
|
-
|
|
1574
|
-
|
|
1988
|
+
if "data" in p:
|
|
1989
|
+
# Full image data was preserved
|
|
1990
|
+
parts.append(
|
|
1991
|
+
Image(
|
|
1992
|
+
data=base64.b64decode(p["data"]),
|
|
1993
|
+
media_type=p.get("media_type"),
|
|
1994
|
+
detail=p.get("detail", "auto"),
|
|
1995
|
+
)
|
|
1996
|
+
)
|
|
1997
|
+
else:
|
|
1998
|
+
# Placeholder tag only
|
|
1999
|
+
parts.append(Text(p["tag"]))
|
|
1575
2000
|
elif p["type"] == "file":
|
|
1576
|
-
|
|
1577
|
-
|
|
2001
|
+
if "data" in p:
|
|
2002
|
+
# Full file data was preserved
|
|
2003
|
+
parts.append(
|
|
2004
|
+
File(
|
|
2005
|
+
data=base64.b64decode(p["data"]),
|
|
2006
|
+
media_type=p.get("media_type"),
|
|
2007
|
+
filename=p.get("filename"),
|
|
2008
|
+
)
|
|
2009
|
+
)
|
|
2010
|
+
else:
|
|
2011
|
+
# Placeholder tag only
|
|
2012
|
+
parts.append(Text(p["tag"]))
|
|
1578
2013
|
elif p["type"] == "tool_call":
|
|
1579
2014
|
parts.append(
|
|
1580
|
-
ToolCall(
|
|
2015
|
+
ToolCall(
|
|
2016
|
+
id=p["id"],
|
|
2017
|
+
name=p["name"],
|
|
2018
|
+
arguments=p["arguments"],
|
|
2019
|
+
thought_signature=_deserialize_signature(
|
|
2020
|
+
p.get("thought_signature")
|
|
2021
|
+
),
|
|
2022
|
+
)
|
|
1581
2023
|
)
|
|
1582
2024
|
elif p["type"] == "tool_result":
|
|
1583
2025
|
parts.append(
|
|
1584
2026
|
ToolResult(tool_call_id=p["tool_call_id"], result=p["result"])
|
|
1585
2027
|
)
|
|
1586
2028
|
elif p["type"] == "thinking":
|
|
1587
|
-
parts.append(
|
|
2029
|
+
parts.append(
|
|
2030
|
+
Thinking(
|
|
2031
|
+
content=p["content"],
|
|
2032
|
+
thought_signature=_deserialize_signature(
|
|
2033
|
+
p.get("thought_signature")
|
|
2034
|
+
),
|
|
2035
|
+
)
|
|
2036
|
+
)
|
|
1588
2037
|
else:
|
|
1589
2038
|
raise ValueError(f"Unknown part type {p['type']!r}")
|
|
1590
2039
|
|
|
@@ -1596,7 +2045,7 @@ class Conversation:
|
|
|
1596
2045
|
Prompt: TypeAlias = str | list[dict] | Message | Conversation
|
|
1597
2046
|
|
|
1598
2047
|
|
|
1599
|
-
def prompts_to_conversations(prompts: Sequence[Prompt]) -> Sequence[
|
|
2048
|
+
def prompts_to_conversations(prompts: Sequence[Prompt]) -> Sequence[Conversation]:
|
|
1600
2049
|
converted = []
|
|
1601
2050
|
for prompt in prompts:
|
|
1602
2051
|
if isinstance(prompt, Conversation):
|