lm-deluge 0.0.67__py3-none-any.whl → 0.0.88__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 +25 -2
- lm_deluge/api_requests/anthropic.py +92 -17
- lm_deluge/api_requests/base.py +47 -11
- lm_deluge/api_requests/bedrock.py +7 -4
- lm_deluge/api_requests/chat_reasoning.py +4 -0
- lm_deluge/api_requests/gemini.py +138 -18
- lm_deluge/api_requests/openai.py +114 -21
- lm_deluge/client.py +282 -49
- lm_deluge/config.py +15 -3
- lm_deluge/mock_openai.py +643 -0
- lm_deluge/models/__init__.py +12 -1
- lm_deluge/models/anthropic.py +17 -2
- lm_deluge/models/arcee.py +16 -0
- lm_deluge/models/deepseek.py +36 -4
- lm_deluge/models/google.py +29 -0
- lm_deluge/models/grok.py +24 -0
- lm_deluge/models/kimi.py +36 -0
- lm_deluge/models/minimax.py +10 -0
- lm_deluge/models/openai.py +100 -0
- lm_deluge/models/openrouter.py +86 -8
- lm_deluge/models/together.py +11 -0
- lm_deluge/models/zai.py +1 -0
- lm_deluge/pipelines/gepa/__init__.py +95 -0
- lm_deluge/pipelines/gepa/core.py +354 -0
- lm_deluge/pipelines/gepa/docs/samples.py +696 -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 +224 -40
- lm_deluge/request_context.py +7 -2
- lm_deluge/tool/__init__.py +1118 -0
- lm_deluge/tool/builtin/anthropic/__init__.py +300 -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.py +1621 -0
- lm_deluge/tool/prefab/sheets.py +385 -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.88.dist-info}/METADATA +22 -9
- lm_deluge-0.0.88.dist-info/RECORD +117 -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 → tool/builtin}/anthropic/bash.py +0 -0
- /lm_deluge/{built_in_tools → tool/builtin}/anthropic/computer_use.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.88.dist-info}/WHEEL +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.88.dist-info}/licenses/LICENSE +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.88.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for GEPA.
|
|
3
|
+
|
|
4
|
+
Includes conversation formatting and text extraction helpers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from lm_deluge.prompt import Conversation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def format_conversation_compact(conversation: Conversation) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Format a Conversation for showing to the proposer LLM.
|
|
18
|
+
|
|
19
|
+
Goals:
|
|
20
|
+
- Show full user and assistant message content
|
|
21
|
+
- Show tool calls with their arguments
|
|
22
|
+
- Abbreviate tool results (just show placeholder, not full content)
|
|
23
|
+
- No decorative separators, keep it compact
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
conversation: The conversation to format
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A string representation suitable for including in a prompt
|
|
30
|
+
"""
|
|
31
|
+
lines: list[str] = []
|
|
32
|
+
|
|
33
|
+
# Check for system message (first message with role="system")
|
|
34
|
+
for msg in conversation.messages:
|
|
35
|
+
if msg.role == "system":
|
|
36
|
+
lines.append(f"[system]\n{msg.completion}")
|
|
37
|
+
lines.append("")
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
for msg in conversation.messages:
|
|
41
|
+
role = msg.role
|
|
42
|
+
|
|
43
|
+
if role == "system":
|
|
44
|
+
# Already handled above
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if role == "user":
|
|
48
|
+
text_content = msg.completion or ""
|
|
49
|
+
lines.append(f"[user]\n{text_content}")
|
|
50
|
+
|
|
51
|
+
elif role == "assistant":
|
|
52
|
+
# Handle text content
|
|
53
|
+
text_content = msg.completion or ""
|
|
54
|
+
if text_content:
|
|
55
|
+
lines.append(f"[assistant]\n{text_content}")
|
|
56
|
+
|
|
57
|
+
# Handle tool calls
|
|
58
|
+
if msg.tool_calls:
|
|
59
|
+
for tc in msg.tool_calls:
|
|
60
|
+
tool_name = tc.name
|
|
61
|
+
# Format arguments compactly
|
|
62
|
+
args_str = _format_tool_args(tc.arguments)
|
|
63
|
+
lines.append(f"[tool_call: {tool_name}]\n{args_str}")
|
|
64
|
+
|
|
65
|
+
elif role == "tool":
|
|
66
|
+
# Just show placeholder for tool results - content can be huge
|
|
67
|
+
# Try to get tool names from tool_results
|
|
68
|
+
if msg.tool_results:
|
|
69
|
+
for tr in msg.tool_results:
|
|
70
|
+
tool_id = getattr(tr, "tool_call_id", "unknown")
|
|
71
|
+
lines.append(f"[tool_result: {tool_id}] (content omitted)")
|
|
72
|
+
else:
|
|
73
|
+
lines.append("[tool_result] (content omitted)")
|
|
74
|
+
|
|
75
|
+
lines.append("")
|
|
76
|
+
|
|
77
|
+
return "\n".join(lines).strip()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _format_tool_args(arguments: dict[str, Any] | str | None) -> str:
|
|
81
|
+
"""Format tool call arguments compactly."""
|
|
82
|
+
if arguments is None:
|
|
83
|
+
return "(no arguments)"
|
|
84
|
+
|
|
85
|
+
if isinstance(arguments, str):
|
|
86
|
+
# Already a string (might be JSON string)
|
|
87
|
+
return arguments[:500] + "..." if len(arguments) > 500 else arguments
|
|
88
|
+
|
|
89
|
+
if isinstance(arguments, dict):
|
|
90
|
+
# Format as key=value pairs
|
|
91
|
+
parts = []
|
|
92
|
+
for key, value in arguments.items():
|
|
93
|
+
value_str = str(value)
|
|
94
|
+
# Truncate long values
|
|
95
|
+
if len(value_str) > 200:
|
|
96
|
+
value_str = value_str[:200] + "..."
|
|
97
|
+
parts.append(f" {key}: {value_str}")
|
|
98
|
+
return "\n".join(parts) if parts else "(no arguments)"
|
|
99
|
+
|
|
100
|
+
return str(arguments)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def extract_text_from_response(response: str) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Extract text from between ``` blocks in LLM response.
|
|
106
|
+
|
|
107
|
+
Handles various formats:
|
|
108
|
+
- ```text``` or ```language\ntext```
|
|
109
|
+
- Incomplete blocks
|
|
110
|
+
- No blocks (returns trimmed response)
|
|
111
|
+
"""
|
|
112
|
+
# Find content between first and last ```
|
|
113
|
+
start = response.find("```")
|
|
114
|
+
if start == -1:
|
|
115
|
+
return response.strip()
|
|
116
|
+
|
|
117
|
+
start += 3
|
|
118
|
+
end = response.rfind("```")
|
|
119
|
+
|
|
120
|
+
if end <= start:
|
|
121
|
+
# Handle incomplete blocks
|
|
122
|
+
stripped = response.strip()
|
|
123
|
+
if stripped.startswith("```"):
|
|
124
|
+
match = re.match(r"^```\S*\n?", response)
|
|
125
|
+
if match:
|
|
126
|
+
return response[match.end() :].strip()
|
|
127
|
+
elif stripped.endswith("```"):
|
|
128
|
+
return stripped[:-3].strip()
|
|
129
|
+
return stripped
|
|
130
|
+
|
|
131
|
+
# Skip language specifier (e.g., ```python\n)
|
|
132
|
+
content = response[start:end]
|
|
133
|
+
match = re.match(r"^\S*\n", content)
|
|
134
|
+
if match:
|
|
135
|
+
content = content[match.end() :]
|
|
136
|
+
|
|
137
|
+
return content.strip()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def format_components_for_prompt(
|
|
141
|
+
component_values: dict[str, str],
|
|
142
|
+
component_descriptions: dict[str, str],
|
|
143
|
+
) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Format components for showing to the proposer.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
component_values: Current text value for each component
|
|
149
|
+
component_descriptions: Description of what each component does
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Formatted string listing all components
|
|
153
|
+
"""
|
|
154
|
+
lines = []
|
|
155
|
+
for name, value in component_values.items():
|
|
156
|
+
description = component_descriptions.get(name, "")
|
|
157
|
+
lines.append(f"### {name}")
|
|
158
|
+
if description:
|
|
159
|
+
lines.append(f"*{description}*")
|
|
160
|
+
lines.append("```")
|
|
161
|
+
lines.append(value)
|
|
162
|
+
lines.append("```")
|
|
163
|
+
lines.append("")
|
|
164
|
+
|
|
165
|
+
return "\n".join(lines)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ..client import
|
|
1
|
+
from ..client import _LLMClient, APIResponse
|
|
2
2
|
from ..util.logprobs import extract_prob
|
|
3
3
|
|
|
4
4
|
# def extract_prob_yes(logprobs: list[dict]):
|
|
@@ -24,7 +24,7 @@ from ..util.logprobs import extract_prob
|
|
|
24
24
|
def score_llm(
|
|
25
25
|
scoring_prompt_template: str,
|
|
26
26
|
inputs: list[tuple | list | dict], # to format the template
|
|
27
|
-
scoring_model:
|
|
27
|
+
scoring_model: _LLMClient,
|
|
28
28
|
return_probabilities: bool,
|
|
29
29
|
yes_token: str = "yes",
|
|
30
30
|
) -> list[bool | None] | list[float | None]:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from ..client import
|
|
2
|
+
from ..client import _LLMClient
|
|
3
3
|
|
|
4
4
|
translation_prompt = (
|
|
5
5
|
"Translate the following text (enclosed in ```) into English. "
|
|
@@ -20,7 +20,9 @@ def is_english(text: str, low_memory: bool = True):
|
|
|
20
20
|
return True
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
async def translate_async(
|
|
23
|
+
async def translate_async(
|
|
24
|
+
texts: list[str], client: _LLMClient, low_memory: bool = True
|
|
25
|
+
):
|
|
24
26
|
to_translate_idxs = [
|
|
25
27
|
i for i, text in enumerate(texts) if not is_english(text, low_memory=low_memory)
|
|
26
28
|
]
|
|
@@ -40,5 +42,5 @@ async def translate_async(texts: list[str], client: LLMClient, low_memory: bool
|
|
|
40
42
|
return texts
|
|
41
43
|
|
|
42
44
|
|
|
43
|
-
def translate(texts: list[str], client:
|
|
45
|
+
def translate(texts: list[str], client: _LLMClient, low_memory: bool = True):
|
|
44
46
|
return asyncio.run(translate_async(texts, client, low_memory))
|
lm_deluge/prompt.py
CHANGED
|
@@ -61,6 +61,8 @@ class ToolCall:
|
|
|
61
61
|
built_in: bool = False
|
|
62
62
|
built_in_type: str | None = None
|
|
63
63
|
extra_body: dict | None = None
|
|
64
|
+
# for gemini 3 - thought signatures to maintain reasoning context
|
|
65
|
+
thought_signature: str | None = None
|
|
64
66
|
|
|
65
67
|
@property
|
|
66
68
|
def fingerprint(self) -> str:
|
|
@@ -93,7 +95,10 @@ class ToolCall:
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
def gemini(self) -> dict:
|
|
96
|
-
|
|
98
|
+
result = {"functionCall": {"name": self.name, "args": self.arguments}}
|
|
99
|
+
if self.thought_signature is not None:
|
|
100
|
+
result["thoughtSignature"] = self.thought_signature # type: ignore
|
|
101
|
+
return result
|
|
97
102
|
|
|
98
103
|
def mistral(self) -> dict:
|
|
99
104
|
return {
|
|
@@ -198,6 +203,8 @@ class ToolResult:
|
|
|
198
203
|
"call_id": self.tool_call_id,
|
|
199
204
|
}
|
|
200
205
|
if self.built_in_type == "computer_call":
|
|
206
|
+
# OpenAI expects "computer_call_output" for the result type
|
|
207
|
+
result["type"] = "computer_call_output"
|
|
201
208
|
result["output"] = output_data.get("output", {})
|
|
202
209
|
if "acknowledged_safety_checks" in output_data:
|
|
203
210
|
result["acknowledged_safety_checks"] = output_data[
|
|
@@ -230,15 +237,41 @@ class ToolResult:
|
|
|
230
237
|
raise ValueError("unsupported self.result type")
|
|
231
238
|
|
|
232
239
|
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
|
-
}
|
|
240
|
+
# Build the function response
|
|
241
|
+
func_response: dict = {
|
|
242
|
+
"name": self.tool_call_id, # Gemini uses name field for ID
|
|
240
243
|
}
|
|
241
244
|
|
|
245
|
+
# Handle different result types
|
|
246
|
+
if isinstance(self.result, str):
|
|
247
|
+
func_response["response"] = {"result": self.result}
|
|
248
|
+
elif isinstance(self.result, dict):
|
|
249
|
+
# Check for Gemini computer use format with inline screenshot
|
|
250
|
+
if self.built_in_type == "gemini_computer_use":
|
|
251
|
+
# Gemini CU expects response dict with optional inline_data parts
|
|
252
|
+
func_response["response"] = self.result.get("response", {})
|
|
253
|
+
# Include inline data (screenshot) if present
|
|
254
|
+
if "inline_data" in self.result:
|
|
255
|
+
func_response["parts"] = [
|
|
256
|
+
{
|
|
257
|
+
"inlineData": {
|
|
258
|
+
"mimeType": self.result["inline_data"].get(
|
|
259
|
+
"mime_type", "image/png"
|
|
260
|
+
),
|
|
261
|
+
"data": self.result["inline_data"]["data"],
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
else:
|
|
266
|
+
func_response["response"] = self.result
|
|
267
|
+
elif isinstance(self.result, list):
|
|
268
|
+
# Handle content blocks (images, etc.) - not yet implemented
|
|
269
|
+
raise ValueError("can't handle content blocks for gemini yet")
|
|
270
|
+
else:
|
|
271
|
+
func_response["response"] = {"result": str(self.result)}
|
|
272
|
+
|
|
273
|
+
return {"functionResponse": func_response}
|
|
274
|
+
|
|
242
275
|
def mistral(self) -> dict:
|
|
243
276
|
return {
|
|
244
277
|
"type": "tool_result",
|
|
@@ -253,6 +286,8 @@ class Thinking:
|
|
|
253
286
|
type: str = field(init=False, default="thinking")
|
|
254
287
|
# for openai - to keep conversation chain
|
|
255
288
|
raw_payload: dict | None = None
|
|
289
|
+
# for gemini 3 - thought signatures to maintain reasoning context
|
|
290
|
+
thought_signature: str | None = None
|
|
256
291
|
|
|
257
292
|
@property
|
|
258
293
|
def fingerprint(self) -> str:
|
|
@@ -270,7 +305,10 @@ class Thinking:
|
|
|
270
305
|
return {"type": "thinking", "thinking": self.content}
|
|
271
306
|
|
|
272
307
|
def gemini(self) -> dict:
|
|
273
|
-
|
|
308
|
+
result = {"text": f"[Thinking: {self.content}]"}
|
|
309
|
+
if self.thought_signature is not None:
|
|
310
|
+
result["thoughtSignature"] = self.thought_signature
|
|
311
|
+
return result
|
|
274
312
|
|
|
275
313
|
def mistral(self) -> dict:
|
|
276
314
|
return {"type": "text", "text": f"[Thinking: {self.content}]"}
|
|
@@ -374,14 +412,15 @@ class Message:
|
|
|
374
412
|
size = p.size
|
|
375
413
|
content_blocks.append({"type": "file", "tag": f"<File ({size} bytes)>"})
|
|
376
414
|
elif isinstance(p, ToolCall):
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
415
|
+
tool_call_block = {
|
|
416
|
+
"type": "tool_call",
|
|
417
|
+
"id": p.id,
|
|
418
|
+
"name": p.name,
|
|
419
|
+
"arguments": _json_safe(p.arguments),
|
|
420
|
+
}
|
|
421
|
+
if p.thought_signature is not None:
|
|
422
|
+
tool_call_block["thought_signature"] = p.thought_signature
|
|
423
|
+
content_blocks.append(tool_call_block)
|
|
385
424
|
elif isinstance(p, ToolResult):
|
|
386
425
|
content_blocks.append(
|
|
387
426
|
{
|
|
@@ -391,7 +430,10 @@ class Message:
|
|
|
391
430
|
}
|
|
392
431
|
)
|
|
393
432
|
elif isinstance(p, Thinking):
|
|
394
|
-
|
|
433
|
+
thinking_block = {"type": "thinking", "content": p.content}
|
|
434
|
+
if p.thought_signature is not None:
|
|
435
|
+
thinking_block["thought_signature"] = p.thought_signature
|
|
436
|
+
content_blocks.append(thinking_block)
|
|
395
437
|
|
|
396
438
|
return {"role": self.role, "content": content_blocks}
|
|
397
439
|
|
|
@@ -415,14 +457,24 @@ class Message:
|
|
|
415
457
|
parts.append(Text(p["tag"]))
|
|
416
458
|
elif p["type"] == "tool_call":
|
|
417
459
|
parts.append(
|
|
418
|
-
ToolCall(
|
|
460
|
+
ToolCall(
|
|
461
|
+
id=p["id"],
|
|
462
|
+
name=p["name"],
|
|
463
|
+
arguments=p["arguments"],
|
|
464
|
+
thought_signature=p.get("thought_signature"),
|
|
465
|
+
)
|
|
419
466
|
)
|
|
420
467
|
elif p["type"] == "tool_result":
|
|
421
468
|
parts.append(
|
|
422
469
|
ToolResult(tool_call_id=p["tool_call_id"], result=p["result"])
|
|
423
470
|
)
|
|
424
471
|
elif p["type"] == "thinking":
|
|
425
|
-
parts.append(
|
|
472
|
+
parts.append(
|
|
473
|
+
Thinking(
|
|
474
|
+
content=p["content"],
|
|
475
|
+
thought_signature=p.get("thought_signature"),
|
|
476
|
+
)
|
|
477
|
+
)
|
|
426
478
|
else:
|
|
427
479
|
raise ValueError(f"Unknown part type {p['type']!r}")
|
|
428
480
|
|
|
@@ -848,14 +900,16 @@ class Conversation:
|
|
|
848
900
|
if content is None:
|
|
849
901
|
return parts
|
|
850
902
|
if isinstance(content, str):
|
|
851
|
-
|
|
903
|
+
if content.strip():
|
|
904
|
+
parts.append(Text(content))
|
|
852
905
|
return parts
|
|
853
906
|
|
|
854
907
|
for block in content:
|
|
855
908
|
block_type = block.get("type")
|
|
856
909
|
if block_type in text_types:
|
|
857
910
|
text_value = block.get("text") or block.get(block_type) or ""
|
|
858
|
-
|
|
911
|
+
if text_value.strip():
|
|
912
|
+
parts.append(Text(text_value))
|
|
859
913
|
elif block_type in image_types:
|
|
860
914
|
parts.append(_to_image_from_url(block))
|
|
861
915
|
elif block_type in file_types:
|
|
@@ -1001,7 +1055,8 @@ class Conversation:
|
|
|
1001
1055
|
)
|
|
1002
1056
|
)
|
|
1003
1057
|
|
|
1004
|
-
|
|
1058
|
+
if parts:
|
|
1059
|
+
conversation_messages.append(Message(mapped_role, parts))
|
|
1005
1060
|
|
|
1006
1061
|
return cls(conversation_messages)
|
|
1007
1062
|
|
|
@@ -1192,14 +1247,24 @@ class Conversation:
|
|
|
1192
1247
|
|
|
1193
1248
|
@classmethod
|
|
1194
1249
|
def from_unknown(
|
|
1195
|
-
cls, messages: list[dict], *, system: str | list[dict] | None = None
|
|
1250
|
+
cls, messages: list[dict] | dict, *, system: str | list[dict] | None = None
|
|
1196
1251
|
) -> tuple["Conversation", str]:
|
|
1197
1252
|
"""Attempt to convert provider-formatted messages without knowing the provider.
|
|
1198
1253
|
|
|
1199
1254
|
Returns the parsed conversation together with the provider label that succeeded
|
|
1200
|
-
("openai" or "
|
|
1255
|
+
("openai", "anthropic", or "log").
|
|
1201
1256
|
"""
|
|
1202
1257
|
|
|
1258
|
+
# Check if input is in log format (output from to_log())
|
|
1259
|
+
if isinstance(messages, dict) and "messages" in messages:
|
|
1260
|
+
return cls.from_log(messages), "log"
|
|
1261
|
+
|
|
1262
|
+
# Ensure messages is a list for provider detection
|
|
1263
|
+
if not isinstance(messages, list):
|
|
1264
|
+
raise ValueError(
|
|
1265
|
+
"messages must be a list of dicts or a dict with 'messages' key"
|
|
1266
|
+
)
|
|
1267
|
+
|
|
1203
1268
|
def _detect_provider() -> str:
|
|
1204
1269
|
has_openai_markers = False
|
|
1205
1270
|
has_anthropic_markers = False
|
|
@@ -1330,14 +1395,14 @@ class Conversation:
|
|
|
1330
1395
|
# For assistant messages, extract computer calls as separate items
|
|
1331
1396
|
text_parts = []
|
|
1332
1397
|
for p in m.parts:
|
|
1333
|
-
if isinstance(p, ToolCall) and p.
|
|
1398
|
+
if isinstance(p, ToolCall) and p.built_in_type == "computer_call":
|
|
1334
1399
|
# Computer calls become separate items in the input array
|
|
1335
|
-
|
|
1400
|
+
# p.arguments already contains the full action dict with "type"
|
|
1336
1401
|
input_items.append(
|
|
1337
1402
|
{
|
|
1338
1403
|
"type": "computer_call",
|
|
1339
1404
|
"call_id": p.id,
|
|
1340
|
-
"action":
|
|
1405
|
+
"action": p.arguments,
|
|
1341
1406
|
}
|
|
1342
1407
|
)
|
|
1343
1408
|
elif isinstance(p, Text):
|
|
@@ -1533,14 +1598,15 @@ class Conversation:
|
|
|
1533
1598
|
{"type": "file", "tag": f"<File ({size} bytes)>"}
|
|
1534
1599
|
)
|
|
1535
1600
|
elif isinstance(p, ToolCall):
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1601
|
+
tool_call_block = {
|
|
1602
|
+
"type": "tool_call",
|
|
1603
|
+
"id": p.id,
|
|
1604
|
+
"name": p.name,
|
|
1605
|
+
"arguments": p.arguments,
|
|
1606
|
+
}
|
|
1607
|
+
if p.thought_signature is not None:
|
|
1608
|
+
tool_call_block["thought_signature"] = p.thought_signature
|
|
1609
|
+
content_blocks.append(tool_call_block)
|
|
1544
1610
|
elif isinstance(p, ToolResult):
|
|
1545
1611
|
content_blocks.append(
|
|
1546
1612
|
{
|
|
@@ -1552,11 +1618,119 @@ class Conversation:
|
|
|
1552
1618
|
}
|
|
1553
1619
|
)
|
|
1554
1620
|
elif isinstance(p, Thinking):
|
|
1555
|
-
|
|
1621
|
+
thinking_block = {"type": "thinking", "content": p.content}
|
|
1622
|
+
if p.thought_signature is not None:
|
|
1623
|
+
thinking_block["thought_signature"] = p.thought_signature
|
|
1624
|
+
content_blocks.append(thinking_block)
|
|
1556
1625
|
serialized.append({"role": msg.role, "content": content_blocks})
|
|
1557
1626
|
|
|
1558
1627
|
return {"messages": serialized}
|
|
1559
1628
|
|
|
1629
|
+
def print(self, max_text_length: int = 500, indent: int = 2) -> None:
|
|
1630
|
+
"""Pretty-print the conversation to stdout.
|
|
1631
|
+
|
|
1632
|
+
Args:
|
|
1633
|
+
max_text_length: Truncate text content longer than this (default 500 chars)
|
|
1634
|
+
indent: JSON indentation for tool calls/results (default 2)
|
|
1635
|
+
"""
|
|
1636
|
+
ROLE_COLORS = {
|
|
1637
|
+
"system": "\033[95m", # magenta
|
|
1638
|
+
"user": "\033[94m", # blue
|
|
1639
|
+
"assistant": "\033[92m", # green
|
|
1640
|
+
"tool": "\033[93m", # yellow
|
|
1641
|
+
}
|
|
1642
|
+
RESET = "\033[0m"
|
|
1643
|
+
DIM = "\033[2m"
|
|
1644
|
+
BOLD = "\033[1m"
|
|
1645
|
+
|
|
1646
|
+
def truncate(text: str, max_len: int) -> str:
|
|
1647
|
+
if len(text) <= max_len:
|
|
1648
|
+
return text
|
|
1649
|
+
return (
|
|
1650
|
+
text[:max_len] + f"{DIM}... [{len(text) - max_len} more chars]{RESET}"
|
|
1651
|
+
)
|
|
1652
|
+
|
|
1653
|
+
def format_json(obj: dict | list, ind: int) -> str:
|
|
1654
|
+
return json.dumps(obj, indent=ind, ensure_ascii=False)
|
|
1655
|
+
|
|
1656
|
+
print(f"\n{BOLD}{'=' * 60}{RESET}")
|
|
1657
|
+
print(f"{BOLD}Conversation ({len(self.messages)} messages){RESET}")
|
|
1658
|
+
print(f"{BOLD}{'=' * 60}{RESET}\n")
|
|
1659
|
+
|
|
1660
|
+
for i, msg in enumerate(self.messages):
|
|
1661
|
+
role_color = ROLE_COLORS.get(msg.role, "")
|
|
1662
|
+
print(f"{role_color}{BOLD}[{msg.role.upper()}]{RESET}")
|
|
1663
|
+
|
|
1664
|
+
for part in msg.parts:
|
|
1665
|
+
if isinstance(part, Text):
|
|
1666
|
+
text = truncate(part.text, max_text_length)
|
|
1667
|
+
# Indent multiline text
|
|
1668
|
+
lines = text.split("\n")
|
|
1669
|
+
if len(lines) > 1:
|
|
1670
|
+
print(" " + "\n ".join(lines))
|
|
1671
|
+
else:
|
|
1672
|
+
print(f" {text}")
|
|
1673
|
+
|
|
1674
|
+
elif isinstance(part, Image):
|
|
1675
|
+
w, h = part.size
|
|
1676
|
+
print(f" {DIM}<Image ({w}x{h})>{RESET}")
|
|
1677
|
+
|
|
1678
|
+
elif isinstance(part, File):
|
|
1679
|
+
size = part.size
|
|
1680
|
+
filename = getattr(part, "filename", None)
|
|
1681
|
+
if filename:
|
|
1682
|
+
print(f" {DIM}<File: {filename} ({size} bytes)>{RESET}")
|
|
1683
|
+
else:
|
|
1684
|
+
print(f" {DIM}<File ({size} bytes)>{RESET}")
|
|
1685
|
+
|
|
1686
|
+
elif isinstance(part, ToolCall):
|
|
1687
|
+
print(
|
|
1688
|
+
f" {DIM}Tool Call:{RESET} {BOLD}{part.name}{RESET} (id: {part.id})"
|
|
1689
|
+
)
|
|
1690
|
+
if part.arguments:
|
|
1691
|
+
args_json = format_json(part.arguments, indent)
|
|
1692
|
+
# Indent the JSON
|
|
1693
|
+
indented = "\n".join(
|
|
1694
|
+
" " + line for line in args_json.split("\n")
|
|
1695
|
+
)
|
|
1696
|
+
print(indented)
|
|
1697
|
+
|
|
1698
|
+
elif isinstance(part, ToolResult):
|
|
1699
|
+
print(f" {DIM}Tool Result:{RESET} (call_id: {part.tool_call_id})")
|
|
1700
|
+
if isinstance(part.result, str):
|
|
1701
|
+
result_text = truncate(part.result, max_text_length)
|
|
1702
|
+
lines = result_text.split("\n")
|
|
1703
|
+
for line in lines:
|
|
1704
|
+
print(f" {line}")
|
|
1705
|
+
elif isinstance(part.result, dict):
|
|
1706
|
+
result_json = format_json(part.result, indent)
|
|
1707
|
+
indented = "\n".join(
|
|
1708
|
+
" " + line for line in result_json.split("\n")
|
|
1709
|
+
)
|
|
1710
|
+
print(indented)
|
|
1711
|
+
elif isinstance(part.result, list):
|
|
1712
|
+
print(f" {DIM}<{len(part.result)} content blocks>{RESET}")
|
|
1713
|
+
for block in part.result:
|
|
1714
|
+
if isinstance(block, Text):
|
|
1715
|
+
block_text = truncate(block.text, max_text_length // 2)
|
|
1716
|
+
print(f" [text] {block_text}")
|
|
1717
|
+
elif isinstance(block, Image):
|
|
1718
|
+
bw, bh = block.size
|
|
1719
|
+
print(f" {DIM}<Image ({bw}x{bh})>{RESET}")
|
|
1720
|
+
|
|
1721
|
+
elif isinstance(part, Thinking):
|
|
1722
|
+
print(f" {DIM}Thinking:{RESET}")
|
|
1723
|
+
thought = truncate(part.content, max_text_length)
|
|
1724
|
+
lines = thought.split("\n")
|
|
1725
|
+
for line in lines:
|
|
1726
|
+
print(f" {DIM}{line}{RESET}")
|
|
1727
|
+
|
|
1728
|
+
# Separator between messages
|
|
1729
|
+
if i < len(self.messages) - 1:
|
|
1730
|
+
print(f"\n{'-' * 40}\n")
|
|
1731
|
+
|
|
1732
|
+
print(f"\n{BOLD}{'=' * 60}{RESET}\n")
|
|
1733
|
+
|
|
1560
1734
|
@classmethod
|
|
1561
1735
|
def from_log(cls, payload: dict) -> "Conversation":
|
|
1562
1736
|
"""Re-hydrate a Conversation previously produced by `to_log()`."""
|
|
@@ -1577,14 +1751,24 @@ class Conversation:
|
|
|
1577
1751
|
parts.append(Text(p["tag"]))
|
|
1578
1752
|
elif p["type"] == "tool_call":
|
|
1579
1753
|
parts.append(
|
|
1580
|
-
ToolCall(
|
|
1754
|
+
ToolCall(
|
|
1755
|
+
id=p["id"],
|
|
1756
|
+
name=p["name"],
|
|
1757
|
+
arguments=p["arguments"],
|
|
1758
|
+
thought_signature=p.get("thought_signature"),
|
|
1759
|
+
)
|
|
1581
1760
|
)
|
|
1582
1761
|
elif p["type"] == "tool_result":
|
|
1583
1762
|
parts.append(
|
|
1584
1763
|
ToolResult(tool_call_id=p["tool_call_id"], result=p["result"])
|
|
1585
1764
|
)
|
|
1586
1765
|
elif p["type"] == "thinking":
|
|
1587
|
-
parts.append(
|
|
1766
|
+
parts.append(
|
|
1767
|
+
Thinking(
|
|
1768
|
+
content=p["content"],
|
|
1769
|
+
thought_signature=p.get("thought_signature"),
|
|
1770
|
+
)
|
|
1771
|
+
)
|
|
1588
1772
|
else:
|
|
1589
1773
|
raise ValueError(f"Unknown part type {p['type']!r}")
|
|
1590
1774
|
|
|
@@ -1596,7 +1780,7 @@ class Conversation:
|
|
|
1596
1780
|
Prompt: TypeAlias = str | list[dict] | Message | Conversation
|
|
1597
1781
|
|
|
1598
1782
|
|
|
1599
|
-
def prompts_to_conversations(prompts: Sequence[Prompt]) -> Sequence[
|
|
1783
|
+
def prompts_to_conversations(prompts: Sequence[Prompt]) -> Sequence[Conversation]:
|
|
1600
1784
|
converted = []
|
|
1601
1785
|
for prompt in prompts:
|
|
1602
1786
|
if isinstance(prompt, Conversation):
|
lm_deluge/request_context.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from functools import cached_property
|
|
3
|
-
from typing import Any, Callable
|
|
3
|
+
from typing import Any, Callable, Sequence, TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from .config import SamplingParams
|
|
6
6
|
from .prompt import CachePattern, Conversation
|
|
7
7
|
from .tracker import StatusTracker
|
|
8
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
@dataclass
|
|
11
14
|
class RequestContext:
|
|
@@ -31,7 +34,8 @@ class RequestContext:
|
|
|
31
34
|
callback: Callable | None = None
|
|
32
35
|
|
|
33
36
|
# Optional features
|
|
34
|
-
tools:
|
|
37
|
+
tools: Sequence[Any] | None = None
|
|
38
|
+
output_schema: "type[BaseModel] | dict | None" = None
|
|
35
39
|
cache: CachePattern | None = None
|
|
36
40
|
use_responses_api: bool = False
|
|
37
41
|
background: bool = False
|
|
@@ -66,6 +70,7 @@ class RequestContext:
|
|
|
66
70
|
"results_arr": self.results_arr,
|
|
67
71
|
"callback": self.callback,
|
|
68
72
|
"tools": self.tools,
|
|
73
|
+
"output_schema": self.output_schema,
|
|
69
74
|
"cache": self.cache,
|
|
70
75
|
"use_responses_api": self.use_responses_api,
|
|
71
76
|
"background": self.background,
|