batchwork-ai 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.
batchwork/__init__.py ADDED
@@ -0,0 +1,261 @@
1
+ """Unified async batch API for AI providers."""
2
+
3
+ from .client import Batchwork
4
+ from .errors import (
5
+ BatchClosedError,
6
+ BatchStateError,
7
+ BatchTimeoutError,
8
+ BatchworkError,
9
+ MediaResolutionError,
10
+ MissingDependencyError,
11
+ UnsupportedProviderError,
12
+ )
13
+ from .job import BatchJob, PollCallback
14
+ from .media import DefaultMediaResolver, MediaResolver, ResolvedMedia
15
+ from .server import (
16
+ AtomicWebhookReplayStore,
17
+ BatchPoller,
18
+ BatchStore,
19
+ BatchWebhookEvent,
20
+ CompletionSink,
21
+ CredentialResolver,
22
+ ErrorHandler,
23
+ MemoryBatchStore,
24
+ MemoryWebhookReplayStore,
25
+ PinnedWebhookTransport,
26
+ RedisBatchStore,
27
+ TickFailure,
28
+ TickResult,
29
+ TrackedBatch,
30
+ TrackTarget,
31
+ UpstashRedis,
32
+ VerifiedWebhook,
33
+ WebhookEventType,
34
+ WebhookReplayStore,
35
+ WebhookResponse,
36
+ WebhookTransport,
37
+ WebhookUrlValidator,
38
+ create_batch_poller,
39
+ create_memory_store,
40
+ create_redis_store,
41
+ parse_webhook_url,
42
+ resolve_public_addresses,
43
+ sign_webhook,
44
+ validate_webhook_url,
45
+ verify_batch_webhook,
46
+ verify_webhook,
47
+ )
48
+ from .types import (
49
+ TERMINAL_STATUSES,
50
+ AssistantContentPart,
51
+ AssistantMessage,
52
+ BatchDefaults,
53
+ BatchEmbeddingRequest,
54
+ BatchImage,
55
+ BatchImageDefaults,
56
+ BatchImageRequest,
57
+ BatchLimits,
58
+ BatchProvider,
59
+ BatchRef,
60
+ BatchRequest,
61
+ BatchRequestCounts,
62
+ BatchRequestSettings,
63
+ BatchResult,
64
+ BatchResultError,
65
+ BatchResultStatus,
66
+ BatchSnapshot,
67
+ BatchStatus,
68
+ BatchUsage,
69
+ ContentToolOutput,
70
+ CustomPart,
71
+ DeprecatedToolOutputFileDataPart,
72
+ DeprecatedToolOutputFileIdPart,
73
+ DeprecatedToolOutputFileReferencePart,
74
+ DeprecatedToolOutputFileUrlPart,
75
+ DeprecatedToolOutputImageDataPart,
76
+ DeprecatedToolOutputImageFileIdPart,
77
+ DeprecatedToolOutputImageFileReferencePart,
78
+ DeprecatedToolOutputImageUrlPart,
79
+ ErrorJsonToolOutput,
80
+ ErrorTextToolOutput,
81
+ ExecutionDeniedToolOutput,
82
+ FilePart,
83
+ FunctionTool,
84
+ ImagePart,
85
+ JsonScalar,
86
+ JsonToolOutput,
87
+ JsonValue,
88
+ MediaSource,
89
+ ModelKind,
90
+ ModelMessage,
91
+ ModelSpec,
92
+ NamedToolChoice,
93
+ ProviderCredentials,
94
+ ProviderDefinedTool,
95
+ ProviderFileReference,
96
+ ProviderOptions,
97
+ ProviderToolChoice,
98
+ ReasoningFilePart,
99
+ ReasoningMediaSource,
100
+ ReasoningPart,
101
+ SystemMessage,
102
+ TaggedFileData,
103
+ TaggedFileDataData,
104
+ TaggedFileDataReference,
105
+ TaggedFileDataText,
106
+ TaggedFileDataUrl,
107
+ TaggedReasoningFileData,
108
+ TextPart,
109
+ TextToolOutput,
110
+ Tool,
111
+ ToolApprovalRequestPart,
112
+ ToolApprovalResponsePart,
113
+ ToolCallPart,
114
+ ToolChoice,
115
+ ToolContentPart,
116
+ ToolMessage,
117
+ ToolOutput,
118
+ ToolOutputContentPart,
119
+ ToolOutputCustomPart,
120
+ ToolOutputFilePart,
121
+ ToolOutputTextPart,
122
+ ToolResultPart,
123
+ UserContentPart,
124
+ UserMessage,
125
+ coerce_credentials,
126
+ is_terminal_status,
127
+ provider_from_ref,
128
+ resolve_model,
129
+ utc_datetime,
130
+ )
131
+
132
+ __version__ = "0.1.1"
133
+
134
+ __all__ = [
135
+ "TERMINAL_STATUSES",
136
+ "AssistantContentPart",
137
+ "AssistantMessage",
138
+ "AtomicWebhookReplayStore",
139
+ "BatchClosedError",
140
+ "BatchDefaults",
141
+ "BatchEmbeddingRequest",
142
+ "BatchImage",
143
+ "BatchImageDefaults",
144
+ "BatchImageRequest",
145
+ "BatchJob",
146
+ "BatchLimits",
147
+ "BatchPoller",
148
+ "BatchProvider",
149
+ "BatchRef",
150
+ "BatchRequest",
151
+ "BatchRequestCounts",
152
+ "BatchRequestSettings",
153
+ "BatchResult",
154
+ "BatchResultError",
155
+ "BatchResultStatus",
156
+ "BatchSnapshot",
157
+ "BatchStateError",
158
+ "BatchStatus",
159
+ "BatchStore",
160
+ "BatchTimeoutError",
161
+ "BatchUsage",
162
+ "BatchWebhookEvent",
163
+ "Batchwork",
164
+ "BatchworkError",
165
+ "CompletionSink",
166
+ "ContentToolOutput",
167
+ "CredentialResolver",
168
+ "CustomPart",
169
+ "DefaultMediaResolver",
170
+ "DeprecatedToolOutputFileDataPart",
171
+ "DeprecatedToolOutputFileIdPart",
172
+ "DeprecatedToolOutputFileReferencePart",
173
+ "DeprecatedToolOutputFileUrlPart",
174
+ "DeprecatedToolOutputImageDataPart",
175
+ "DeprecatedToolOutputImageFileIdPart",
176
+ "DeprecatedToolOutputImageFileReferencePart",
177
+ "DeprecatedToolOutputImageUrlPart",
178
+ "ErrorHandler",
179
+ "ErrorJsonToolOutput",
180
+ "ErrorTextToolOutput",
181
+ "ExecutionDeniedToolOutput",
182
+ "FilePart",
183
+ "FunctionTool",
184
+ "ImagePart",
185
+ "JsonScalar",
186
+ "JsonToolOutput",
187
+ "JsonValue",
188
+ "MediaResolutionError",
189
+ "MediaResolver",
190
+ "MediaSource",
191
+ "MemoryBatchStore",
192
+ "MemoryWebhookReplayStore",
193
+ "MissingDependencyError",
194
+ "ModelKind",
195
+ "ModelMessage",
196
+ "ModelSpec",
197
+ "NamedToolChoice",
198
+ "PinnedWebhookTransport",
199
+ "PollCallback",
200
+ "ProviderCredentials",
201
+ "ProviderDefinedTool",
202
+ "ProviderFileReference",
203
+ "ProviderOptions",
204
+ "ProviderToolChoice",
205
+ "ReasoningFilePart",
206
+ "ReasoningMediaSource",
207
+ "ReasoningPart",
208
+ "RedisBatchStore",
209
+ "ResolvedMedia",
210
+ "SystemMessage",
211
+ "TaggedFileData",
212
+ "TaggedFileDataData",
213
+ "TaggedFileDataReference",
214
+ "TaggedFileDataText",
215
+ "TaggedFileDataUrl",
216
+ "TaggedReasoningFileData",
217
+ "TextPart",
218
+ "TextToolOutput",
219
+ "TickFailure",
220
+ "TickResult",
221
+ "Tool",
222
+ "ToolApprovalRequestPart",
223
+ "ToolApprovalResponsePart",
224
+ "ToolCallPart",
225
+ "ToolChoice",
226
+ "ToolContentPart",
227
+ "ToolMessage",
228
+ "ToolOutput",
229
+ "ToolOutputContentPart",
230
+ "ToolOutputCustomPart",
231
+ "ToolOutputFilePart",
232
+ "ToolOutputTextPart",
233
+ "ToolResultPart",
234
+ "TrackTarget",
235
+ "TrackedBatch",
236
+ "UnsupportedProviderError",
237
+ "UpstashRedis",
238
+ "UserContentPart",
239
+ "UserMessage",
240
+ "VerifiedWebhook",
241
+ "WebhookEventType",
242
+ "WebhookReplayStore",
243
+ "WebhookResponse",
244
+ "WebhookTransport",
245
+ "WebhookUrlValidator",
246
+ "__version__",
247
+ "coerce_credentials",
248
+ "create_batch_poller",
249
+ "create_memory_store",
250
+ "create_redis_store",
251
+ "is_terminal_status",
252
+ "parse_webhook_url",
253
+ "provider_from_ref",
254
+ "resolve_model",
255
+ "resolve_public_addresses",
256
+ "sign_webhook",
257
+ "utc_datetime",
258
+ "validate_webhook_url",
259
+ "verify_batch_webhook",
260
+ "verify_webhook",
261
+ ]
@@ -0,0 +1,432 @@
1
+ """Anthropic-native prompt history serialization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+
7
+ from ._serialization import (
8
+ _anthropic_file_block,
9
+ _anthropic_tool_output,
10
+ _messages,
11
+ _provider_options,
12
+ _source,
13
+ )
14
+ from ._typing import is_string_mapping
15
+ from .types import BatchProvider
16
+
17
+ _PROVIDER_TOOL_NAMES = {
18
+ "anthropic.code_execution_20250522": "code_execution",
19
+ "anthropic.code_execution_20250825": "code_execution",
20
+ "anthropic.code_execution_20260120": "code_execution",
21
+ "anthropic.computer_20241022": "computer",
22
+ "anthropic.computer_20250124": "computer",
23
+ "anthropic.computer_20251124": "computer",
24
+ "anthropic.text_editor_20241022": "str_replace_editor",
25
+ "anthropic.text_editor_20250124": "str_replace_editor",
26
+ "anthropic.text_editor_20250429": "str_replace_based_edit_tool",
27
+ "anthropic.text_editor_20250728": "str_replace_based_edit_tool",
28
+ "anthropic.bash_20241022": "bash",
29
+ "anthropic.bash_20250124": "bash",
30
+ "anthropic.memory_20250818": "memory",
31
+ "anthropic.web_fetch_20250910": "web_fetch",
32
+ "anthropic.web_fetch_20260209": "web_fetch",
33
+ "anthropic.web_search_20250305": "web_search",
34
+ "anthropic.web_search_20260209": "web_search",
35
+ "anthropic.tool_search_regex_20251119": "tool_search_tool_regex",
36
+ "anthropic.tool_search_bm25_20251119": "tool_search_tool_bm25",
37
+ "anthropic.advisor_20260301": "advisor",
38
+ }
39
+
40
+
41
+ def _value(item: Mapping[str, object], snake: str, camel: str) -> object | None:
42
+ return item[snake] if snake in item else item.get(camel)
43
+
44
+
45
+ def _parts(content: object) -> list[object]:
46
+ if isinstance(content, str):
47
+ return [{"type": "text", "text": content}]
48
+ if isinstance(content, Sequence) and not isinstance(content, (str, bytes, bytearray)):
49
+ return list(content)
50
+ return []
51
+
52
+
53
+ def _cache_control(item: Mapping[str, object]) -> object | None:
54
+ return _provider_options(item, BatchProvider.ANTHROPIC).get("cacheControl")
55
+
56
+
57
+ def _part_cache_control(
58
+ part: Mapping[str, object], message: Mapping[str, object], *, last: bool
59
+ ) -> object | None:
60
+ cache_control = _cache_control(part)
61
+ return _cache_control(message) if cache_control is None and last else cache_control
62
+
63
+
64
+ def _with_cache(block: dict[str, object], cache_control: object | None) -> dict[str, object]:
65
+ if cache_control is not None:
66
+ block["cache_control"] = cache_control
67
+ return block
68
+
69
+
70
+ def _system_content(message: Mapping[str, object]) -> list[dict[str, object]]:
71
+ content = message.get("content")
72
+ if not isinstance(content, str):
73
+ return []
74
+ return [_with_cache({"type": "text", "text": content}, _cache_control(message))]
75
+
76
+
77
+ def _file_block(
78
+ part: Mapping[str, object], cache_control: object | None, *, tool_output: bool = False
79
+ ) -> dict[str, object] | None:
80
+ block = _anthropic_file_block(part, tool_output=tool_output)
81
+ if block is None:
82
+ return None
83
+ if block.get("type") == "document" and not tool_output:
84
+ options = _provider_options(part, BatchProvider.ANTHROPIC)
85
+ filename = part.get("filename")
86
+ title = options.get("title", filename)
87
+ if title is not None:
88
+ block["title"] = title
89
+ context = options.get("context")
90
+ if context is not None:
91
+ block["context"] = context
92
+ citations = options.get("citations")
93
+ if is_string_mapping(citations) and citations.get("enabled") is True:
94
+ block["citations"] = {"enabled": True}
95
+ return _with_cache(block, cache_control)
96
+
97
+
98
+ def _user_part(
99
+ part: object, message: Mapping[str, object], *, last: bool
100
+ ) -> dict[str, object] | None:
101
+ if not is_string_mapping(part):
102
+ return None
103
+ cache_control = _part_cache_control(part, message, last=last)
104
+ kind = part.get("type")
105
+ if kind == "text":
106
+ return _with_cache({"type": "text", "text": part.get("text", "")}, cache_control)
107
+ if kind == "image":
108
+ data, inline = _source(part.get("image", part.get("data")), BatchProvider.ANTHROPIC)
109
+ if data is None:
110
+ return None
111
+ media_type = _value(part, "media_type", "mediaType")
112
+ source: dict[str, object]
113
+ if inline:
114
+ source = {
115
+ "type": "base64",
116
+ "media_type": media_type if isinstance(media_type, str) else "image/png",
117
+ "data": data,
118
+ }
119
+ else:
120
+ source = {"type": "url", "url": data}
121
+ return _with_cache({"type": "image", "source": source}, cache_control)
122
+ if kind in {"file", "reasoning-file"}:
123
+ return _file_block(part, cache_control)
124
+ return None
125
+
126
+
127
+ def _output_cache_control(output: Mapping[str, object]) -> object | None:
128
+ cache_control = _cache_control(output)
129
+ if cache_control is not None or output.get("type") != "content":
130
+ return cache_control
131
+ content = output.get("value")
132
+ if not isinstance(content, Sequence) or isinstance(content, (str, bytes, bytearray)):
133
+ return None
134
+ for part in content:
135
+ if is_string_mapping(part) and (cache_control := _cache_control(part)) is not None:
136
+ return cache_control
137
+ return None
138
+
139
+
140
+ def _tool_result(
141
+ part: Mapping[str, object], message: Mapping[str, object], *, last: bool
142
+ ) -> dict[str, object]:
143
+ output = part.get("output", part.get("content", ""))
144
+ serialized, is_error = _anthropic_tool_output(output)
145
+ result: dict[str, object] = {
146
+ "type": "tool_result",
147
+ "tool_use_id": _value(part, "tool_call_id", "toolCallId"),
148
+ "content": serialized,
149
+ }
150
+ if is_error:
151
+ result["is_error"] = True
152
+ cache_control = _cache_control(part)
153
+ if cache_control is None and is_string_mapping(output):
154
+ cache_control = _output_cache_control(output)
155
+ if cache_control is None and last:
156
+ cache_control = _cache_control(message)
157
+ return _with_cache(result, cache_control)
158
+
159
+
160
+ def _user_block(messages: Sequence[Mapping[str, object]]) -> dict[str, object]:
161
+ content: list[object] = []
162
+ for message in messages:
163
+ parts = _parts(message.get("content"))
164
+ for index, part in enumerate(parts):
165
+ last = index == len(parts) - 1
166
+ if message.get("role") == "tool":
167
+ if not is_string_mapping(part) or part.get("type") != "tool-result":
168
+ continue
169
+ content.append(_tool_result(part, message, last=last))
170
+ continue
171
+ converted = _user_part(part, message, last=last)
172
+ if converted is not None:
173
+ content.append(converted)
174
+ return {"role": "user", "content": content}
175
+
176
+
177
+ def _provider_tool_name_mapping(item: Mapping[str, object]) -> dict[str, str]:
178
+ tools = item.get("tools")
179
+ if not isinstance(tools, Sequence) or isinstance(tools, (str, bytes, bytearray)):
180
+ return {}
181
+ result: dict[str, str] = {}
182
+ for tool in tools:
183
+ if not is_string_mapping(tool) or tool.get("type") not in {
184
+ "provider",
185
+ "provider-defined",
186
+ }:
187
+ continue
188
+ identifier = tool.get("id")
189
+ name = tool.get("name")
190
+ if isinstance(identifier, str) and isinstance(name, str):
191
+ provider_name = _PROVIDER_TOOL_NAMES.get(identifier)
192
+ if provider_name is not None:
193
+ result[name] = provider_name
194
+ return result
195
+
196
+
197
+ def _caller(options: Mapping[str, object]) -> dict[str, object] | None:
198
+ caller = options.get("caller")
199
+ if not is_string_mapping(caller):
200
+ return None
201
+ caller_type = caller.get("type")
202
+ if caller_type == "direct":
203
+ return {"type": "direct"}
204
+ tool_id = _value(caller, "tool_id", "toolId")
205
+ if caller_type in {"code_execution_20250825", "code_execution_20260120"} and isinstance(
206
+ tool_id, str
207
+ ):
208
+ return {"type": caller_type, "tool_id": tool_id}
209
+ return None
210
+
211
+
212
+ def _provider_tool_use(
213
+ part: Mapping[str, object], provider_name: str, cache_control: object | None
214
+ ) -> dict[str, object] | None:
215
+ options = _provider_options(part, BatchProvider.ANTHROPIC)
216
+ call_id = _value(part, "tool_call_id", "toolCallId")
217
+ tool_name = _value(part, "tool_name", "toolName")
218
+ tool_input = part.get("input", {})
219
+ if options.get("type") == "mcp-tool-use":
220
+ server_name = _value(options, "server_name", "serverName")
221
+ if not isinstance(server_name, str):
222
+ return None
223
+ return _with_cache(
224
+ {
225
+ "type": "mcp_tool_use",
226
+ "id": call_id,
227
+ "name": tool_name,
228
+ "input": tool_input,
229
+ "server_name": server_name,
230
+ },
231
+ cache_control,
232
+ )
233
+ if provider_name == "code_execution" and is_string_mapping(tool_input):
234
+ input_type = tool_input.get("type")
235
+ if input_type in {"bash_code_execution", "text_editor_code_execution"}:
236
+ return _with_cache(
237
+ {
238
+ "type": "server_tool_use",
239
+ "id": call_id,
240
+ "name": input_type,
241
+ "input": dict(tool_input),
242
+ },
243
+ cache_control,
244
+ )
245
+ if input_type == "programmatic-tool-call":
246
+ return _with_cache(
247
+ {
248
+ "type": "server_tool_use",
249
+ "id": call_id,
250
+ "name": "code_execution",
251
+ "input": {key: value for key, value in tool_input.items() if key != "type"},
252
+ },
253
+ cache_control,
254
+ )
255
+ if provider_name not in {
256
+ "code_execution",
257
+ "web_fetch",
258
+ "web_search",
259
+ "tool_search_tool_regex",
260
+ "tool_search_tool_bm25",
261
+ "advisor",
262
+ }:
263
+ return None
264
+ return _with_cache(
265
+ {
266
+ "type": "server_tool_use",
267
+ "id": call_id,
268
+ "name": provider_name,
269
+ "input": {} if provider_name == "advisor" else tool_input,
270
+ },
271
+ cache_control,
272
+ )
273
+
274
+
275
+ def _tool_use(
276
+ part: Mapping[str, object],
277
+ message: Mapping[str, object],
278
+ tool_names: Mapping[str, str],
279
+ *,
280
+ last: bool,
281
+ ) -> dict[str, object] | None:
282
+ cache_control = _part_cache_control(part, message, last=last)
283
+ tool_name = _value(part, "tool_name", "toolName")
284
+ provider_name = tool_names.get(tool_name, tool_name) if isinstance(tool_name, str) else ""
285
+ provider_executed = _value(part, "provider_executed", "providerExecuted")
286
+ if provider_executed is True:
287
+ return _provider_tool_use(part, provider_name, cache_control)
288
+ result: dict[str, object] = {
289
+ "type": "tool_use",
290
+ "id": _value(part, "tool_call_id", "toolCallId"),
291
+ "name": tool_name,
292
+ "input": part.get("input", {}),
293
+ }
294
+ caller = _caller(_provider_options(part, BatchProvider.ANTHROPIC))
295
+ if caller is not None:
296
+ result["caller"] = caller
297
+ return _with_cache(result, cache_control)
298
+
299
+
300
+ def _reasoning(part: Mapping[str, object]) -> dict[str, object] | None:
301
+ options = _provider_options(part, BatchProvider.ANTHROPIC)
302
+ signature = options.get("signature")
303
+ if isinstance(signature, str):
304
+ return {"type": "thinking", "thinking": part.get("text", ""), "signature": signature}
305
+ redacted_data = _value(options, "redacted_data", "redactedData")
306
+ if isinstance(redacted_data, str):
307
+ return {"type": "redacted_thinking", "data": redacted_data}
308
+ return None
309
+
310
+
311
+ def _assistant_part(
312
+ part: object,
313
+ message: Mapping[str, object],
314
+ tool_names: Mapping[str, str],
315
+ *,
316
+ last: bool,
317
+ trim: bool,
318
+ ) -> dict[str, object] | None:
319
+ if not is_string_mapping(part):
320
+ return None
321
+ kind = part.get("type")
322
+ if kind == "reasoning":
323
+ return _reasoning(part)
324
+ if kind == "tool-call":
325
+ return _tool_use(part, message, tool_names, last=last)
326
+ if kind == "tool-result":
327
+ return _tool_result(part, message, last=last)
328
+ cache_control = _part_cache_control(part, message, last=last)
329
+ if kind == "text":
330
+ text = part.get("text", "")
331
+ if trim and isinstance(text, str):
332
+ text = text.strip()
333
+ options = _provider_options(part, BatchProvider.ANTHROPIC)
334
+ block_type = "compaction" if options.get("type") == "compaction" else "text"
335
+ key = "content" if block_type == "compaction" else "text"
336
+ return _with_cache({"type": block_type, key: text}, cache_control)
337
+ if kind in {"file", "reasoning-file"}:
338
+ return _file_block(part, cache_control)
339
+ return None
340
+
341
+
342
+ def _move_tool_uses_to_end(content: Sequence[dict[str, object]]) -> list[dict[str, object]]:
343
+ result: list[dict[str, object]] = []
344
+ segment: list[dict[str, object]] = []
345
+
346
+ def flush() -> None:
347
+ result.extend(part for part in segment if part.get("type") != "tool_use")
348
+ result.extend(part for part in segment if part.get("type") == "tool_use")
349
+ segment.clear()
350
+
351
+ for part in content:
352
+ if part.get("type") in {"thinking", "redacted_thinking"}:
353
+ flush()
354
+ result.append(part)
355
+ else:
356
+ segment.append(part)
357
+ flush()
358
+ return result
359
+
360
+
361
+ def _assistant_block(
362
+ messages: Sequence[Mapping[str, object]],
363
+ tool_names: Mapping[str, str],
364
+ *,
365
+ final: bool,
366
+ ) -> dict[str, object]:
367
+ content: list[dict[str, object]] = []
368
+ for message_index, message in enumerate(messages):
369
+ parts = _parts(message.get("content"))
370
+ for part_index, part in enumerate(parts):
371
+ last = part_index == len(parts) - 1
372
+ converted = _assistant_part(
373
+ part,
374
+ message,
375
+ tool_names,
376
+ last=last,
377
+ trim=final and message_index == len(messages) - 1 and last,
378
+ )
379
+ if converted is not None:
380
+ content.append(converted)
381
+ return {"role": "assistant", "content": _move_tool_uses_to_end(content)}
382
+
383
+
384
+ def _grouped_messages(item: Mapping[str, object]) -> list[tuple[str, list[dict[str, object]]]]:
385
+ groups: list[tuple[str, list[dict[str, object]]]] = []
386
+ for message in _messages(item):
387
+ role = message.get("role")
388
+ block_type = "user" if role in {"user", "tool"} else str(role)
389
+ if groups and groups[-1][0] == block_type:
390
+ groups[-1][1].append(message)
391
+ else:
392
+ groups.append((block_type, [message]))
393
+ return groups
394
+
395
+
396
+ def anthropic_prompt(
397
+ item: Mapping[str, object],
398
+ ) -> tuple[list[dict[str, object]] | None, list[dict[str, object]]]:
399
+ """Return Anthropic API ``system`` blocks and conversation messages."""
400
+
401
+ top_level_system = item.get("system")
402
+ system: list[dict[str, object]] | None = None
403
+ if isinstance(top_level_system, str):
404
+ system = [{"type": "text", "text": top_level_system}]
405
+ messages: list[dict[str, object]] = []
406
+ groups = _grouped_messages(item)
407
+ tool_names = _provider_tool_name_mapping(item)
408
+ system_seen = system is not None
409
+ for index, (block_type, block_messages) in enumerate(groups):
410
+ if block_type == "system":
411
+ content = [part for message in block_messages for part in _system_content(message)]
412
+ if not system_seen or (not messages and index == 0):
413
+ system = [*(system or []), *content]
414
+ else:
415
+ messages.append({"role": "system", "content": content})
416
+ system_seen = True
417
+ elif block_type == "assistant":
418
+ messages.append(
419
+ _assistant_block(block_messages, tool_names, final=index == len(groups) - 1)
420
+ )
421
+ elif block_type == "user":
422
+ messages.append(_user_block(block_messages))
423
+ return system, messages
424
+
425
+
426
+ def anthropic_messages(item: Mapping[str, object]) -> list[dict[str, object]]:
427
+ """Return only Anthropic conversation messages for body serializers."""
428
+
429
+ return anthropic_prompt(item)[1]
430
+
431
+
432
+ __all__ = ["anthropic_messages", "anthropic_prompt"]