aidial-adapter-anthropic 0.1.0__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.
- aidial_adapter_anthropic/_utils/json.py +116 -0
- aidial_adapter_anthropic/_utils/list.py +84 -0
- aidial_adapter_anthropic/_utils/pydantic.py +6 -0
- aidial_adapter_anthropic/_utils/resource.py +54 -0
- aidial_adapter_anthropic/_utils/text.py +4 -0
- aidial_adapter_anthropic/adapter/__init__.py +4 -0
- aidial_adapter_anthropic/adapter/_base.py +95 -0
- aidial_adapter_anthropic/adapter/_claude/adapter.py +549 -0
- aidial_adapter_anthropic/adapter/_claude/blocks.py +128 -0
- aidial_adapter_anthropic/adapter/_claude/citations.py +63 -0
- aidial_adapter_anthropic/adapter/_claude/config.py +39 -0
- aidial_adapter_anthropic/adapter/_claude/converters.py +303 -0
- aidial_adapter_anthropic/adapter/_claude/params.py +25 -0
- aidial_adapter_anthropic/adapter/_claude/state.py +45 -0
- aidial_adapter_anthropic/adapter/_claude/tokenizer/__init__.py +10 -0
- aidial_adapter_anthropic/adapter/_claude/tokenizer/anthropic.py +57 -0
- aidial_adapter_anthropic/adapter/_claude/tokenizer/approximate.py +260 -0
- aidial_adapter_anthropic/adapter/_claude/tokenizer/base.py +26 -0
- aidial_adapter_anthropic/adapter/_claude/tools.py +98 -0
- aidial_adapter_anthropic/adapter/_decorator/base.py +53 -0
- aidial_adapter_anthropic/adapter/_decorator/preprocess.py +63 -0
- aidial_adapter_anthropic/adapter/_decorator/replicator.py +32 -0
- aidial_adapter_anthropic/adapter/_errors.py +71 -0
- aidial_adapter_anthropic/adapter/_tokenize.py +12 -0
- aidial_adapter_anthropic/adapter/_truncate_prompt.py +168 -0
- aidial_adapter_anthropic/adapter/claude.py +17 -0
- aidial_adapter_anthropic/dial/_attachments.py +238 -0
- aidial_adapter_anthropic/dial/_lazy_stage.py +40 -0
- aidial_adapter_anthropic/dial/_message.py +341 -0
- aidial_adapter_anthropic/dial/consumer.py +235 -0
- aidial_adapter_anthropic/dial/request.py +170 -0
- aidial_adapter_anthropic/dial/resource.py +189 -0
- aidial_adapter_anthropic/dial/storage.py +138 -0
- aidial_adapter_anthropic/dial/token_usage.py +19 -0
- aidial_adapter_anthropic/dial/tools.py +180 -0
- aidial_adapter_anthropic-0.1.0.dist-info/LICENSE +202 -0
- aidial_adapter_anthropic-0.1.0.dist-info/METADATA +121 -0
- aidial_adapter_anthropic-0.1.0.dist-info/RECORD +39 -0
- aidial_adapter_anthropic-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from logging import DEBUG
|
|
5
|
+
from typing import List, Optional, Tuple, Type, assert_never
|
|
6
|
+
|
|
7
|
+
from aidial_sdk.chat_completion import Message as DialMessage
|
|
8
|
+
from anthropic import (
|
|
9
|
+
AsyncAnthropic,
|
|
10
|
+
AsyncAnthropicBedrock,
|
|
11
|
+
AsyncAnthropicFoundry,
|
|
12
|
+
AsyncAnthropicVertex,
|
|
13
|
+
Omit,
|
|
14
|
+
omit,
|
|
15
|
+
)
|
|
16
|
+
from anthropic._resource import AsyncAPIResource
|
|
17
|
+
from anthropic.lib.streaming import BetaInputJsonEvent as InputJsonEvent
|
|
18
|
+
from anthropic.lib.streaming import BetaTextEvent as TextEvent
|
|
19
|
+
from anthropic.lib.streaming import (
|
|
20
|
+
ParsedBetaContentBlockStopEvent as ParsedContentBlockStopEvent,
|
|
21
|
+
)
|
|
22
|
+
from anthropic.lib.streaming._beta_types import (
|
|
23
|
+
BetaCitationEvent as CitationEvent,
|
|
24
|
+
)
|
|
25
|
+
from anthropic.lib.streaming._beta_types import (
|
|
26
|
+
BetaSignatureEvent as SignatureEvent,
|
|
27
|
+
)
|
|
28
|
+
from anthropic.lib.streaming._beta_types import (
|
|
29
|
+
BetaThinkingEvent as ThinkingEvent,
|
|
30
|
+
)
|
|
31
|
+
from anthropic.lib.streaming._beta_types import (
|
|
32
|
+
ParsedBetaMessageStopEvent as ParsedMessageStopEvent,
|
|
33
|
+
)
|
|
34
|
+
from anthropic.resources.beta import AsyncMessages as FirstPartyAsyncMessagesAPI
|
|
35
|
+
from anthropic.types.beta import (
|
|
36
|
+
BetaBashCodeExecutionToolResultBlock as BashCodeExecutionToolResultBlock,
|
|
37
|
+
)
|
|
38
|
+
from anthropic.types.beta import (
|
|
39
|
+
BetaCodeExecutionToolResultBlock as CodeExecutionToolResultBlock,
|
|
40
|
+
)
|
|
41
|
+
from anthropic.types.beta import (
|
|
42
|
+
BetaContainerUploadBlock as ContainerUploadBlock,
|
|
43
|
+
)
|
|
44
|
+
from anthropic.types.beta import BetaMCPToolResultBlock as MCPToolResultBlock
|
|
45
|
+
from anthropic.types.beta import BetaMCPToolUseBlock as MCPToolUseBlock
|
|
46
|
+
from anthropic.types.beta import BetaMessage as ClaudeResponseMessage
|
|
47
|
+
from anthropic.types.beta import BetaMessageParam as ClaudeMessageParam
|
|
48
|
+
from anthropic.types.beta import (
|
|
49
|
+
BetaRawContentBlockDeltaEvent as ContentBlockDeltaEvent,
|
|
50
|
+
)
|
|
51
|
+
from anthropic.types.beta import (
|
|
52
|
+
BetaRawContentBlockStartEvent as ContentBlockStartEvent,
|
|
53
|
+
)
|
|
54
|
+
from anthropic.types.beta import BetaRawMessageDeltaEvent as MessageDeltaEvent
|
|
55
|
+
from anthropic.types.beta import BetaRawMessageStartEvent as MessageStartEvent
|
|
56
|
+
from anthropic.types.beta import (
|
|
57
|
+
BetaRedactedThinkingBlock as RedactedThinkingBlock,
|
|
58
|
+
)
|
|
59
|
+
from anthropic.types.beta import BetaServerToolUseBlock as ServerToolUseBlock
|
|
60
|
+
from anthropic.types.beta import BetaTextBlock as TextBlock
|
|
61
|
+
from anthropic.types.beta import (
|
|
62
|
+
BetaTextEditorCodeExecutionToolResultBlock as TextEditorCodeExecutionToolResultBlock,
|
|
63
|
+
)
|
|
64
|
+
from anthropic.types.beta import BetaThinkingBlock as ThinkingBlock
|
|
65
|
+
from anthropic.types.beta import BetaThinkingConfigParam as ThinkingConfigParam
|
|
66
|
+
from anthropic.types.beta import (
|
|
67
|
+
BetaToolSearchToolResultBlock as ToolSearchToolResultBlock,
|
|
68
|
+
)
|
|
69
|
+
from anthropic.types.beta import BetaToolUseBlock as ToolUseBlock
|
|
70
|
+
from anthropic.types.beta import (
|
|
71
|
+
BetaWebFetchToolResultBlock as WebFetchToolResultBlock,
|
|
72
|
+
)
|
|
73
|
+
from anthropic.types.beta import (
|
|
74
|
+
BetaWebSearchToolResultBlock as WebSearchToolResultBlock,
|
|
75
|
+
)
|
|
76
|
+
from anthropic.types.beta.parsed_beta_message import (
|
|
77
|
+
ParsedBetaTextBlock as ParsedTextBlock,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
from aidial_adapter_anthropic._utils.json import json_dumps_short
|
|
81
|
+
from aidial_adapter_anthropic._utils.list import ListProjection
|
|
82
|
+
from aidial_adapter_anthropic.adapter._base import (
|
|
83
|
+
ChatCompletionAdapter,
|
|
84
|
+
default_preprocess_messages,
|
|
85
|
+
keep_last,
|
|
86
|
+
turn_based_partitioner,
|
|
87
|
+
)
|
|
88
|
+
from aidial_adapter_anthropic.adapter._claude.blocks import (
|
|
89
|
+
IMAGE_ATTACHMENT_PROCESSOR,
|
|
90
|
+
PDF_ATTACHMENT_PROCESSOR,
|
|
91
|
+
PLAIN_TEXT_ATTACHMENT_PROCESSOR,
|
|
92
|
+
create_text_block,
|
|
93
|
+
)
|
|
94
|
+
from aidial_adapter_anthropic.adapter._claude.citations import create_citations
|
|
95
|
+
from aidial_adapter_anthropic.adapter._claude.config import (
|
|
96
|
+
ClaudeConfiguration,
|
|
97
|
+
ClaudeConfigurationWithThinking,
|
|
98
|
+
)
|
|
99
|
+
from aidial_adapter_anthropic.adapter._claude.converters import (
|
|
100
|
+
to_claude_messages,
|
|
101
|
+
to_claude_tool_config,
|
|
102
|
+
to_dial_finish_reason,
|
|
103
|
+
to_dial_usage,
|
|
104
|
+
)
|
|
105
|
+
from aidial_adapter_anthropic.adapter._claude.params import ClaudeParameters
|
|
106
|
+
from aidial_adapter_anthropic.adapter._claude.state import MessageState
|
|
107
|
+
from aidial_adapter_anthropic.adapter._claude.tokenizer import (
|
|
108
|
+
AnthropicTokenizer,
|
|
109
|
+
ClaudeTokenizer,
|
|
110
|
+
create_tokenizer,
|
|
111
|
+
)
|
|
112
|
+
from aidial_adapter_anthropic.adapter._claude.tools import (
|
|
113
|
+
function_to_tool_messages,
|
|
114
|
+
process_tools_block,
|
|
115
|
+
)
|
|
116
|
+
from aidial_adapter_anthropic.adapter._decorator.base import compose_decorators
|
|
117
|
+
from aidial_adapter_anthropic.adapter._decorator.preprocess import (
|
|
118
|
+
preprocess_messages_decorator,
|
|
119
|
+
)
|
|
120
|
+
from aidial_adapter_anthropic.adapter._decorator.replicator import (
|
|
121
|
+
replicator_decorator,
|
|
122
|
+
)
|
|
123
|
+
from aidial_adapter_anthropic.adapter._errors import ValidationError
|
|
124
|
+
from aidial_adapter_anthropic.adapter._truncate_prompt import (
|
|
125
|
+
DiscardedMessages,
|
|
126
|
+
truncate_prompt,
|
|
127
|
+
)
|
|
128
|
+
from aidial_adapter_anthropic.dial._attachments import (
|
|
129
|
+
AttachmentProcessors,
|
|
130
|
+
WithResources,
|
|
131
|
+
)
|
|
132
|
+
from aidial_adapter_anthropic.dial._message import parse_dial_message
|
|
133
|
+
from aidial_adapter_anthropic.dial.consumer import Consumer, ToolUseMessage
|
|
134
|
+
from aidial_adapter_anthropic.dial.request import (
|
|
135
|
+
ModelParameters as DialParameters,
|
|
136
|
+
)
|
|
137
|
+
from aidial_adapter_anthropic.dial.resource import DialResource
|
|
138
|
+
from aidial_adapter_anthropic.dial.storage import FileStorage
|
|
139
|
+
from aidial_adapter_anthropic.dial.tools import ToolsMode
|
|
140
|
+
|
|
141
|
+
_log = logging.getLogger(__name__)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Beta AsyncMessages doesn't provide the 'stream' method,
|
|
145
|
+
# so we enabled it via the adapter.
|
|
146
|
+
class _AsyncMessagesAdapter(AsyncAPIResource):
|
|
147
|
+
create = FirstPartyAsyncMessagesAPI.create
|
|
148
|
+
stream = FirstPartyAsyncMessagesAPI.stream
|
|
149
|
+
|
|
150
|
+
def __init__(self, resource: AsyncAPIResource):
|
|
151
|
+
super().__init__(resource._client)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# NOTE: it's not pydantic BaseModel, because
|
|
155
|
+
# anthropic.types.MessageParam.content is of Iterable type and
|
|
156
|
+
# pydantic automatically converts lists into
|
|
157
|
+
# list iterators following the type.
|
|
158
|
+
# See https://github.com/anthropics/anthropic-sdk-python/issues/656 for details.
|
|
159
|
+
@dataclass
|
|
160
|
+
class ClaudeRequest:
|
|
161
|
+
params: ClaudeParameters
|
|
162
|
+
messages: ListProjection[WithResources[ClaudeMessageParam]]
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def claude_messages(self) -> List[ClaudeMessageParam]:
|
|
166
|
+
return [res.payload for res in self.messages.raw_list]
|
|
167
|
+
|
|
168
|
+
@cached_property
|
|
169
|
+
def resources(self) -> List[DialResource]:
|
|
170
|
+
return [r for res in self.messages.raw_list for r in res.resources]
|
|
171
|
+
|
|
172
|
+
def get_resource(self, index: int) -> DialResource | None:
|
|
173
|
+
if 0 <= index < len(self.resources):
|
|
174
|
+
return self.resources[index]
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
AnthropicClient = (
|
|
179
|
+
AsyncAnthropic
|
|
180
|
+
| AsyncAnthropicBedrock
|
|
181
|
+
| AsyncAnthropicVertex
|
|
182
|
+
| AsyncAnthropicFoundry
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def create_adapter(
|
|
187
|
+
*,
|
|
188
|
+
deployment: str,
|
|
189
|
+
storage: FileStorage | None,
|
|
190
|
+
client: AnthropicClient,
|
|
191
|
+
default_max_tokens: int,
|
|
192
|
+
supports_thinking: bool,
|
|
193
|
+
supports_documents: bool,
|
|
194
|
+
custom_tokenizer: ClaudeTokenizer | None = None,
|
|
195
|
+
) -> ChatCompletionAdapter:
|
|
196
|
+
tokenizer = custom_tokenizer or AnthropicTokenizer(deployment, client)
|
|
197
|
+
model = Adapter(
|
|
198
|
+
deployment=deployment,
|
|
199
|
+
storage=storage,
|
|
200
|
+
client=client,
|
|
201
|
+
tokenizer=tokenizer,
|
|
202
|
+
default_max_tokens=default_max_tokens,
|
|
203
|
+
supports_documents=supports_documents,
|
|
204
|
+
supports_thinking=supports_thinking,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return compose_decorators(
|
|
208
|
+
preprocess_messages_decorator(default_preprocess_messages),
|
|
209
|
+
replicator_decorator(),
|
|
210
|
+
)(model)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Adapter(ChatCompletionAdapter):
|
|
214
|
+
deployment: str
|
|
215
|
+
storage: Optional[FileStorage]
|
|
216
|
+
client: AnthropicClient
|
|
217
|
+
|
|
218
|
+
tokenizer: ClaudeTokenizer
|
|
219
|
+
default_max_tokens: int
|
|
220
|
+
supports_thinking: bool
|
|
221
|
+
supports_documents: bool
|
|
222
|
+
|
|
223
|
+
async def configuration(self) -> Type[ClaudeConfiguration]:
|
|
224
|
+
return ClaudeConfigurationWithThinking
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def attachment_processors(self) -> AttachmentProcessors:
|
|
228
|
+
# Document support: https://docs.anthropic.com/en/docs/build-with-claude/pdf-support#supported-platforms-and-models
|
|
229
|
+
document_processors = (
|
|
230
|
+
[PDF_ATTACHMENT_PROCESSOR, PLAIN_TEXT_ATTACHMENT_PROCESSOR]
|
|
231
|
+
if self.supports_documents
|
|
232
|
+
else []
|
|
233
|
+
)
|
|
234
|
+
return AttachmentProcessors(
|
|
235
|
+
text_handler=create_text_block,
|
|
236
|
+
attachment_processors=(
|
|
237
|
+
[IMAGE_ATTACHMENT_PROCESSOR] + document_processors
|
|
238
|
+
),
|
|
239
|
+
file_storage=self.storage,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def _prepare_claude_request(
|
|
243
|
+
self, params: DialParameters, messages: List[DialMessage]
|
|
244
|
+
) -> ClaudeRequest:
|
|
245
|
+
configuration = params.parse_configuration(await self.configuration())
|
|
246
|
+
|
|
247
|
+
if len(messages) == 0:
|
|
248
|
+
raise ValidationError("List of messages must not be empty")
|
|
249
|
+
|
|
250
|
+
tools_config = to_claude_tool_config(params.tool_config)
|
|
251
|
+
|
|
252
|
+
parsed_messages = [
|
|
253
|
+
function_to_tool_messages(parse_dial_message(m)) for m in messages
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
system_prompt, claude_messages = await to_claude_messages(
|
|
257
|
+
self.attachment_processors, parsed_messages
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
thinking: ThinkingConfigParam | Omit = omit
|
|
261
|
+
if (
|
|
262
|
+
isinstance(configuration, ClaudeConfigurationWithThinking)
|
|
263
|
+
and configuration.thinking is not None
|
|
264
|
+
):
|
|
265
|
+
thinking = configuration.thinking.to_claude()
|
|
266
|
+
|
|
267
|
+
temperature = omit
|
|
268
|
+
if params.temperature is not None:
|
|
269
|
+
# Mapping OpenAI temp [0,2] range to Anthropic temp [0,1] range
|
|
270
|
+
temperature = params.temperature / 2
|
|
271
|
+
|
|
272
|
+
if not isinstance(thinking, Omit) and thinking["type"] == "enabled":
|
|
273
|
+
# Thinking isn’t compatible with temperature, top_p, or top_k
|
|
274
|
+
# modifications as well as forced tool use.
|
|
275
|
+
temperature = omit
|
|
276
|
+
|
|
277
|
+
max_tokens = params.max_tokens or self.default_max_tokens
|
|
278
|
+
|
|
279
|
+
claude_params = ClaudeParameters(
|
|
280
|
+
max_tokens=max_tokens,
|
|
281
|
+
stop_sequences=params.stop,
|
|
282
|
+
system=system_prompt or omit,
|
|
283
|
+
temperature=temperature,
|
|
284
|
+
top_p=params.top_p or omit,
|
|
285
|
+
tools=(tools_config and tools_config.tools) or omit,
|
|
286
|
+
tool_choice=(tools_config and tools_config.tool_choice) or omit,
|
|
287
|
+
thinking=thinking,
|
|
288
|
+
betas=configuration.betas or omit,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return ClaudeRequest(params=claude_params, messages=claude_messages)
|
|
292
|
+
|
|
293
|
+
async def _compute_discarded_messages(
|
|
294
|
+
self, request: ClaudeRequest, max_prompt_tokens: int | None
|
|
295
|
+
) -> Tuple[DiscardedMessages | None, ClaudeRequest]:
|
|
296
|
+
if max_prompt_tokens is None:
|
|
297
|
+
return None, request
|
|
298
|
+
|
|
299
|
+
discarded_messages, messages = await truncate_prompt(
|
|
300
|
+
messages=request.messages.list,
|
|
301
|
+
tokenizer=create_tokenizer(self.tokenizer, request.params),
|
|
302
|
+
keep_message=keep_last,
|
|
303
|
+
partitioner=turn_based_partitioner,
|
|
304
|
+
model_limit=None,
|
|
305
|
+
user_limit=max_prompt_tokens,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
claude_messages = ListProjection(messages)
|
|
309
|
+
|
|
310
|
+
discarded_messages = list(
|
|
311
|
+
request.messages.to_original_indices(discarded_messages)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return discarded_messages, ClaudeRequest(
|
|
315
|
+
params=request.params,
|
|
316
|
+
messages=claude_messages,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
async def chat(
|
|
320
|
+
self,
|
|
321
|
+
consumer: Consumer,
|
|
322
|
+
params: DialParameters,
|
|
323
|
+
messages: List[DialMessage],
|
|
324
|
+
):
|
|
325
|
+
request = await self._prepare_claude_request(params, messages)
|
|
326
|
+
|
|
327
|
+
discarded_messages, request = await self._compute_discarded_messages(
|
|
328
|
+
request, params.max_prompt_tokens
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if params.stream:
|
|
332
|
+
await self.invoke_streaming(
|
|
333
|
+
consumer,
|
|
334
|
+
params.tools_mode,
|
|
335
|
+
request,
|
|
336
|
+
discarded_messages,
|
|
337
|
+
)
|
|
338
|
+
else:
|
|
339
|
+
await self.invoke_non_streaming(
|
|
340
|
+
consumer,
|
|
341
|
+
params.tools_mode,
|
|
342
|
+
request,
|
|
343
|
+
discarded_messages,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
async def count_prompt_tokens(
|
|
347
|
+
self, params: DialParameters, messages: List[DialMessage]
|
|
348
|
+
) -> int:
|
|
349
|
+
request = await self._prepare_claude_request(params, messages)
|
|
350
|
+
tokenizer = create_tokenizer(self.tokenizer, request.params)
|
|
351
|
+
return await tokenizer(request.messages.list)
|
|
352
|
+
|
|
353
|
+
async def count_completion_tokens(self, string: str) -> int:
|
|
354
|
+
return self.tokenizer.tokenize_text(string)
|
|
355
|
+
|
|
356
|
+
async def compute_discarded_messages(
|
|
357
|
+
self, params: DialParameters, messages: List[DialMessage]
|
|
358
|
+
) -> DiscardedMessages | None:
|
|
359
|
+
request = await self._prepare_claude_request(params, messages)
|
|
360
|
+
discarded_messages, _request = await self._compute_discarded_messages(
|
|
361
|
+
request, params.max_prompt_tokens
|
|
362
|
+
)
|
|
363
|
+
return discarded_messages
|
|
364
|
+
|
|
365
|
+
async def invoke_streaming(
|
|
366
|
+
self,
|
|
367
|
+
consumer: Consumer,
|
|
368
|
+
tools_mode: ToolsMode | None,
|
|
369
|
+
request: ClaudeRequest,
|
|
370
|
+
discarded_messages: DiscardedMessages | None,
|
|
371
|
+
):
|
|
372
|
+
if _log.isEnabledFor(DEBUG):
|
|
373
|
+
msg = json_dumps_short(
|
|
374
|
+
{"deployment": self.deployment, "request": request}
|
|
375
|
+
)
|
|
376
|
+
_log.debug(f"request: {msg}")
|
|
377
|
+
|
|
378
|
+
async with (
|
|
379
|
+
_AsyncMessagesAdapter(self.client.beta.messages).stream(
|
|
380
|
+
messages=request.claude_messages,
|
|
381
|
+
model=self.deployment,
|
|
382
|
+
**request.params,
|
|
383
|
+
) as stream,
|
|
384
|
+
consumer.create_stage("Thinking") as thinking_stage,
|
|
385
|
+
):
|
|
386
|
+
stop_reason = None
|
|
387
|
+
tool: ToolUseMessage | None = None
|
|
388
|
+
|
|
389
|
+
async for event in stream:
|
|
390
|
+
if _log.isEnabledFor(DEBUG):
|
|
391
|
+
_log.debug(f"response event: {json_dumps_short(event)}")
|
|
392
|
+
|
|
393
|
+
match event:
|
|
394
|
+
case MessageStartEvent():
|
|
395
|
+
pass
|
|
396
|
+
case TextEvent(text=text):
|
|
397
|
+
consumer.append_content(text)
|
|
398
|
+
|
|
399
|
+
case ThinkingEvent(thinking=thinking):
|
|
400
|
+
thinking_stage.append_content(thinking)
|
|
401
|
+
|
|
402
|
+
case SignatureEvent() | MessageDeltaEvent():
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
case ContentBlockStartEvent(content_block=content_block):
|
|
406
|
+
if isinstance(content_block, ToolUseBlock):
|
|
407
|
+
tool = process_tools_block(
|
|
408
|
+
consumer,
|
|
409
|
+
content_block,
|
|
410
|
+
tools_mode,
|
|
411
|
+
streaming=True,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
case InputJsonEvent(partial_json=partial_json):
|
|
415
|
+
if tool:
|
|
416
|
+
tool.append_arguments(partial_json)
|
|
417
|
+
else:
|
|
418
|
+
_log.warning(
|
|
419
|
+
"The model generated tool input before start using it"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
case ParsedContentBlockStopEvent(
|
|
423
|
+
content_block=content_block
|
|
424
|
+
):
|
|
425
|
+
match content_block:
|
|
426
|
+
case TextBlock(citations=citations):
|
|
427
|
+
# The text content is already handled in TextEvent handler.
|
|
428
|
+
for citation in citations or []:
|
|
429
|
+
create_citations(
|
|
430
|
+
consumer, request.get_resource, citation
|
|
431
|
+
)
|
|
432
|
+
case ToolUseBlock():
|
|
433
|
+
# Tool Use is processed in ContentBlockStartEvent and InputJsonEvent handlers
|
|
434
|
+
pass
|
|
435
|
+
case ThinkingBlock() | RedactedThinkingBlock():
|
|
436
|
+
# Thinking is processed in ThinkingEvent
|
|
437
|
+
pass
|
|
438
|
+
case (
|
|
439
|
+
ServerToolUseBlock()
|
|
440
|
+
| WebSearchToolResultBlock()
|
|
441
|
+
| CodeExecutionToolResultBlock()
|
|
442
|
+
| MCPToolUseBlock()
|
|
443
|
+
| MCPToolResultBlock()
|
|
444
|
+
| ContainerUploadBlock()
|
|
445
|
+
| BashCodeExecutionToolResultBlock()
|
|
446
|
+
| TextEditorCodeExecutionToolResultBlock()
|
|
447
|
+
| WebFetchToolResultBlock()
|
|
448
|
+
| ParsedTextBlock()
|
|
449
|
+
| BashCodeExecutionToolResultBlock()
|
|
450
|
+
| TextEditorCodeExecutionToolResultBlock()
|
|
451
|
+
):
|
|
452
|
+
_log.error(
|
|
453
|
+
f"Content block of type {content_block.type} isn't supported"
|
|
454
|
+
)
|
|
455
|
+
case _:
|
|
456
|
+
assert_never(content_block)
|
|
457
|
+
|
|
458
|
+
case ParsedMessageStopEvent(message=message):
|
|
459
|
+
consumer.add_usage(to_dial_usage(message.usage))
|
|
460
|
+
stop_reason = message.stop_reason
|
|
461
|
+
if self.supports_thinking:
|
|
462
|
+
consumer.choice.set_state(
|
|
463
|
+
MessageState(
|
|
464
|
+
claude_message_content=message.content
|
|
465
|
+
).to_dict()
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
case ContentBlockDeltaEvent() | CitationEvent():
|
|
469
|
+
pass
|
|
470
|
+
|
|
471
|
+
case _:
|
|
472
|
+
assert_never(event)
|
|
473
|
+
|
|
474
|
+
consumer.close_content(
|
|
475
|
+
to_dial_finish_reason(stop_reason, tools_mode)
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
consumer.set_discarded_messages(discarded_messages)
|
|
479
|
+
|
|
480
|
+
async def invoke_non_streaming(
|
|
481
|
+
self,
|
|
482
|
+
consumer: Consumer,
|
|
483
|
+
tools_mode: ToolsMode | None,
|
|
484
|
+
request: ClaudeRequest,
|
|
485
|
+
discarded_messages: DiscardedMessages | None,
|
|
486
|
+
):
|
|
487
|
+
|
|
488
|
+
if _log.isEnabledFor(DEBUG):
|
|
489
|
+
msg = json_dumps_short(
|
|
490
|
+
{"deployment": self.deployment, "request": request}
|
|
491
|
+
)
|
|
492
|
+
_log.debug(f"request: {msg}")
|
|
493
|
+
|
|
494
|
+
message: ClaudeResponseMessage = await self.client.beta.messages.create(
|
|
495
|
+
messages=request.claude_messages,
|
|
496
|
+
model=self.deployment,
|
|
497
|
+
**request.params,
|
|
498
|
+
stream=False,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
if _log.isEnabledFor(DEBUG):
|
|
502
|
+
_log.debug(f"response: {json_dumps_short(message)}")
|
|
503
|
+
|
|
504
|
+
for content in message.content:
|
|
505
|
+
match content:
|
|
506
|
+
case TextBlock(text=text, citations=citations):
|
|
507
|
+
consumer.append_content(text)
|
|
508
|
+
for citation in citations or []:
|
|
509
|
+
create_citations(
|
|
510
|
+
consumer, request.get_resource, citation
|
|
511
|
+
)
|
|
512
|
+
case ToolUseBlock():
|
|
513
|
+
process_tools_block(
|
|
514
|
+
consumer, content, tools_mode, streaming=False
|
|
515
|
+
)
|
|
516
|
+
case ThinkingBlock(thinking=thinking):
|
|
517
|
+
with consumer.create_stage("Thinking") as stage:
|
|
518
|
+
stage.append_content(thinking)
|
|
519
|
+
case RedactedThinkingBlock():
|
|
520
|
+
pass
|
|
521
|
+
case (
|
|
522
|
+
ServerToolUseBlock()
|
|
523
|
+
| WebSearchToolResultBlock()
|
|
524
|
+
| CodeExecutionToolResultBlock()
|
|
525
|
+
| MCPToolUseBlock()
|
|
526
|
+
| MCPToolResultBlock()
|
|
527
|
+
| ContainerUploadBlock()
|
|
528
|
+
| BashCodeExecutionToolResultBlock()
|
|
529
|
+
| TextEditorCodeExecutionToolResultBlock()
|
|
530
|
+
| WebFetchToolResultBlock()
|
|
531
|
+
| ToolSearchToolResultBlock()
|
|
532
|
+
):
|
|
533
|
+
_log.error(
|
|
534
|
+
f"Content block of type {content.type} isn't supported"
|
|
535
|
+
)
|
|
536
|
+
case _:
|
|
537
|
+
assert_never(content)
|
|
538
|
+
|
|
539
|
+
if self.supports_thinking:
|
|
540
|
+
consumer.choice.set_state(
|
|
541
|
+
MessageState(claude_message_content=message.content).to_dict()
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
consumer.close_content(
|
|
545
|
+
to_dial_finish_reason(message.stop_reason, tools_mode)
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
consumer.add_usage(to_dial_usage(message.usage))
|
|
549
|
+
consumer.set_discarded_messages(discarded_messages)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from aidial_sdk.chat_completion import ToolCall
|
|
4
|
+
from anthropic.types.beta import (
|
|
5
|
+
BetaBase64PDFSourceParam as Base64PDFSourceParam,
|
|
6
|
+
)
|
|
7
|
+
from anthropic.types.beta import (
|
|
8
|
+
BetaCitationsConfigParam as CitationsConfigParam,
|
|
9
|
+
)
|
|
10
|
+
from anthropic.types.beta import BetaContentBlockParam as ContentBlockParam
|
|
11
|
+
from anthropic.types.beta import BetaImageBlockParam as ImageBlockParam
|
|
12
|
+
from anthropic.types.beta import (
|
|
13
|
+
BetaPlainTextSourceParam as PlainTextSourceParam,
|
|
14
|
+
)
|
|
15
|
+
from anthropic.types.beta import (
|
|
16
|
+
BetaRequestDocumentBlockParam as RequestDocumentBlockParam,
|
|
17
|
+
)
|
|
18
|
+
from anthropic.types.beta import BetaTextBlockParam as TextBlockParam
|
|
19
|
+
from anthropic.types.beta import (
|
|
20
|
+
BetaToolResultBlockParam as ToolResultBlockParam,
|
|
21
|
+
)
|
|
22
|
+
from anthropic.types.beta import BetaToolUseBlockParam as ToolUseBlockParam
|
|
23
|
+
from anthropic.types.beta.beta_base64_image_source_param import (
|
|
24
|
+
BetaBase64ImageSourceParam as Base64ImageSourceParam,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from aidial_adapter_anthropic._utils.resource import Resource
|
|
28
|
+
from aidial_adapter_anthropic.dial._attachments import AttachmentProcessor
|
|
29
|
+
from aidial_adapter_anthropic.dial._message import HumanToolResultMessage
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_text_block(text: str) -> TextBlockParam:
|
|
33
|
+
return TextBlockParam(text=text, type="text")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_image_block(resource: Resource) -> ImageBlockParam:
|
|
37
|
+
return ImageBlockParam(
|
|
38
|
+
source=Base64ImageSourceParam(
|
|
39
|
+
data=resource.data_base64,
|
|
40
|
+
media_type=resource.type, # type: ignore
|
|
41
|
+
type="base64",
|
|
42
|
+
),
|
|
43
|
+
type="image",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_text_document_block(
|
|
48
|
+
resource: Resource, *, enable_citations: bool = False
|
|
49
|
+
) -> RequestDocumentBlockParam:
|
|
50
|
+
return RequestDocumentBlockParam(
|
|
51
|
+
source=PlainTextSourceParam(
|
|
52
|
+
data=resource.data.decode("utf-8"),
|
|
53
|
+
media_type="text/plain",
|
|
54
|
+
type="text",
|
|
55
|
+
),
|
|
56
|
+
type="document",
|
|
57
|
+
citations=CitationsConfigParam(enabled=enable_citations),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_pdf_document_block(
|
|
62
|
+
resource: Resource, *, enable_citations: bool = False
|
|
63
|
+
) -> RequestDocumentBlockParam:
|
|
64
|
+
return RequestDocumentBlockParam(
|
|
65
|
+
source=Base64PDFSourceParam(
|
|
66
|
+
data=resource.data_base64,
|
|
67
|
+
media_type="application/pdf",
|
|
68
|
+
type="base64",
|
|
69
|
+
),
|
|
70
|
+
type="document",
|
|
71
|
+
citations=CitationsConfigParam(enabled=enable_citations),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def create_tool_use_block(call: ToolCall) -> ContentBlockParam:
|
|
76
|
+
return ToolUseBlockParam(
|
|
77
|
+
id=call.id,
|
|
78
|
+
name=call.function.name,
|
|
79
|
+
input=json.loads(call.function.arguments),
|
|
80
|
+
type="tool_use",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def create_tool_result_block(
|
|
85
|
+
message: HumanToolResultMessage,
|
|
86
|
+
) -> ToolResultBlockParam:
|
|
87
|
+
return ToolResultBlockParam(
|
|
88
|
+
tool_use_id=message.id,
|
|
89
|
+
type="tool_result",
|
|
90
|
+
content=[create_text_block(message.content)],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
IMAGE_ATTACHMENT_PROCESSOR = AttachmentProcessor(
|
|
95
|
+
supported_types={
|
|
96
|
+
"image/png": {"png"},
|
|
97
|
+
"image/jpeg": {"jpeg", "jpg"},
|
|
98
|
+
"image/gif": {"gif"},
|
|
99
|
+
"image/webp": {"webp"},
|
|
100
|
+
},
|
|
101
|
+
handler=create_image_block,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
PDF_ATTACHMENT_PROCESSOR = AttachmentProcessor(
|
|
105
|
+
supported_types={"application/pdf": {"pdf"}},
|
|
106
|
+
handler=create_pdf_document_block,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
PLAIN_TEXT_ATTACHMENT_PROCESSOR = AttachmentProcessor(
|
|
110
|
+
supported_types={
|
|
111
|
+
"text/plain": {"txt"},
|
|
112
|
+
"text/html": {"html", "htm"},
|
|
113
|
+
"text/css": {"css"},
|
|
114
|
+
"text/javascript": {"js"},
|
|
115
|
+
"application/x-javascript": {"js"},
|
|
116
|
+
"text/x-typescript": {"ts"},
|
|
117
|
+
"application/x-typescript": {"ts"},
|
|
118
|
+
"text/csv": {"csv"},
|
|
119
|
+
"text/markdown": {"md"},
|
|
120
|
+
"text/x-python": {"py"},
|
|
121
|
+
"application/x-python-code": {"py"},
|
|
122
|
+
"application/json": {"json"},
|
|
123
|
+
"text/xml": {"xml"},
|
|
124
|
+
"application/rtf": {"rtf"},
|
|
125
|
+
"text/rtf": {"rtf"},
|
|
126
|
+
},
|
|
127
|
+
handler=create_text_document_block,
|
|
128
|
+
)
|