agent-framework-anthropic 1.0.0b251209__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-framework-anthropic
3
+ Version: 1.0.0b251209
4
+ Summary: Anthropic integration for Microsoft Agent Framework.
5
+ Author-email: Microsoft <af-support@microsoft.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ License-File: LICENSE
19
+ Requires-Dist: agent-framework-core
20
+ Requires-Dist: anthropic>=0.70.0,<1
21
+ Project-URL: homepage, https://aka.ms/agent-framework
22
+ Project-URL: issues, https://github.com/microsoft/agent-framework/issues
23
+ Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
24
+ Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
25
+
26
+ # Get Started with Microsoft Agent Framework Anthropic
27
+
28
+ Please install this package via pip:
29
+
30
+ ```bash
31
+ pip install agent-framework-anthropic --pre
32
+ ```
33
+
34
+ ## Anthropic Integration
35
+
36
+ The Anthropic integration enables communication with the Anthropic API, allowing your Agent Framework applications to leverage Anthropic's capabilities.
37
+
38
+ ### Basic Usage Example
39
+
40
+ See the [Anthropic agent examples](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/agents/anthropic/) which demonstrate:
41
+
42
+ - Connecting to a Anthropic endpoint with an agent
43
+ - Streaming and non-streaming responses
44
+
@@ -0,0 +1,18 @@
1
+ # Get Started with Microsoft Agent Framework Anthropic
2
+
3
+ Please install this package via pip:
4
+
5
+ ```bash
6
+ pip install agent-framework-anthropic --pre
7
+ ```
8
+
9
+ ## Anthropic Integration
10
+
11
+ The Anthropic integration enables communication with the Anthropic API, allowing your Agent Framework applications to leverage Anthropic's capabilities.
12
+
13
+ ### Basic Usage Example
14
+
15
+ See the [Anthropic agent examples](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/agents/anthropic/) which demonstrate:
16
+
17
+ - Connecting to a Anthropic endpoint with an agent
18
+ - Streaming and non-streaming responses
@@ -0,0 +1,15 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ import importlib.metadata
4
+
5
+ from ._chat_client import AnthropicClient
6
+
7
+ try:
8
+ __version__ = importlib.metadata.version(__name__)
9
+ except importlib.metadata.PackageNotFoundError:
10
+ __version__ = "0.0.0" # Fallback for development mode
11
+
12
+ __all__ = [
13
+ "AnthropicClient",
14
+ "__version__",
15
+ ]
@@ -0,0 +1,678 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ from collections.abc import AsyncIterable, MutableMapping, MutableSequence, Sequence
4
+ from typing import Any, ClassVar, Final, TypeVar
5
+
6
+ from agent_framework import (
7
+ AGENT_FRAMEWORK_USER_AGENT,
8
+ AIFunction,
9
+ Annotations,
10
+ BaseChatClient,
11
+ ChatMessage,
12
+ ChatOptions,
13
+ ChatResponse,
14
+ ChatResponseUpdate,
15
+ CitationAnnotation,
16
+ Contents,
17
+ FinishReason,
18
+ FunctionCallContent,
19
+ FunctionResultContent,
20
+ HostedCodeInterpreterTool,
21
+ HostedFileContent,
22
+ HostedMCPTool,
23
+ HostedWebSearchTool,
24
+ Role,
25
+ TextContent,
26
+ TextReasoningContent,
27
+ TextSpanRegion,
28
+ ToolProtocol,
29
+ UsageContent,
30
+ UsageDetails,
31
+ get_logger,
32
+ prepare_function_call_results,
33
+ use_chat_middleware,
34
+ use_function_invocation,
35
+ )
36
+ from agent_framework._pydantic import AFBaseSettings
37
+ from agent_framework.exceptions import ServiceInitializationError
38
+ from agent_framework.observability import use_observability
39
+ from anthropic import AsyncAnthropic
40
+ from anthropic.types.beta import (
41
+ BetaContentBlock,
42
+ BetaMessage,
43
+ BetaMessageDeltaUsage,
44
+ BetaRawContentBlockDelta,
45
+ BetaRawMessageStreamEvent,
46
+ BetaTextBlock,
47
+ BetaUsage,
48
+ )
49
+ from pydantic import SecretStr, ValidationError
50
+
51
+ logger = get_logger("agent_framework.anthropic")
52
+
53
+ ANTHROPIC_DEFAULT_MAX_TOKENS: Final[int] = 1024
54
+ BETA_FLAGS: Final[list[str]] = ["mcp-client-2025-04-04", "code-execution-2025-08-25"]
55
+
56
+ ROLE_MAP: dict[Role, str] = {
57
+ Role.USER: "user",
58
+ Role.ASSISTANT: "assistant",
59
+ Role.SYSTEM: "user",
60
+ Role.TOOL: "user",
61
+ }
62
+
63
+ FINISH_REASON_MAP: dict[str, FinishReason] = {
64
+ "stop_sequence": FinishReason.STOP,
65
+ "max_tokens": FinishReason.LENGTH,
66
+ "tool_use": FinishReason.TOOL_CALLS,
67
+ "end_turn": FinishReason.STOP,
68
+ "refusal": FinishReason.CONTENT_FILTER,
69
+ "pause_turn": FinishReason.STOP,
70
+ }
71
+
72
+
73
+ class AnthropicSettings(AFBaseSettings):
74
+ """Anthropic Project settings.
75
+
76
+ The settings are first loaded from environment variables with the prefix 'ANTHROPIC_'.
77
+ If the environment variables are not found, the settings can be loaded from a .env file
78
+ with the encoding 'utf-8'. If the settings are not found in the .env file, the settings
79
+ are ignored; however, validation will fail alerting that the settings are missing.
80
+
81
+ Keyword Args:
82
+ api_key: The Anthropic API key.
83
+ chat_model_id: The Anthropic chat model ID.
84
+ env_file_path: If provided, the .env settings are read from this file path location.
85
+ env_file_encoding: The encoding of the .env file, defaults to 'utf-8'.
86
+
87
+ Examples:
88
+ .. code-block:: python
89
+
90
+ from agent_framework.anthropic import AnthropicSettings
91
+
92
+ # Using environment variables
93
+ # Set ANTHROPIC_API_KEY=your_anthropic_api_key
94
+ # ANTHROPIC_CHAT_MODEL_ID=claude-sonnet-4-5-20250929
95
+
96
+ # Or passing parameters directly
97
+ settings = AnthropicSettings(chat_model_id="claude-sonnet-4-5-20250929")
98
+
99
+ # Or loading from a .env file
100
+ settings = AnthropicSettings(env_file_path="path/to/.env")
101
+ """
102
+
103
+ env_prefix: ClassVar[str] = "ANTHROPIC_"
104
+
105
+ api_key: SecretStr | None = None
106
+ chat_model_id: str | None = None
107
+
108
+
109
+ TAnthropicClient = TypeVar("TAnthropicClient", bound="AnthropicClient")
110
+
111
+
112
+ @use_function_invocation
113
+ @use_observability
114
+ @use_chat_middleware
115
+ class AnthropicClient(BaseChatClient):
116
+ """Anthropic Chat client."""
117
+
118
+ OTEL_PROVIDER_NAME: ClassVar[str] = "anthropic" # type: ignore[reportIncompatibleVariableOverride, misc]
119
+
120
+ def __init__(
121
+ self,
122
+ *,
123
+ api_key: str | None = None,
124
+ model_id: str | None = None,
125
+ anthropic_client: AsyncAnthropic | None = None,
126
+ additional_beta_flags: list[str] | None = None,
127
+ env_file_path: str | None = None,
128
+ env_file_encoding: str | None = None,
129
+ **kwargs: Any,
130
+ ) -> None:
131
+ """Initialize an Anthropic Agent client.
132
+
133
+ Keyword Args:
134
+ api_key: The Anthropic API key to use for authentication.
135
+ model_id: The ID of the model to use.
136
+ anthropic_client: An existing Anthropic client to use. If not provided, one will be created.
137
+ This can be used to further configure the client before passing it in.
138
+ For instance if you need to set a different base_url for testing or private deployments.
139
+ additional_beta_flags: Additional beta flags to enable on the client.
140
+ Default flags are: "mcp-client-2025-04-04", "code-execution-2025-08-25".
141
+ env_file_path: Path to environment file for loading settings.
142
+ env_file_encoding: Encoding of the environment file.
143
+ kwargs: Additional keyword arguments passed to the parent class.
144
+
145
+ Examples:
146
+ .. code-block:: python
147
+
148
+ from agent_framework.anthropic import AnthropicClient
149
+ from azure.identity.aio import DefaultAzureCredential
150
+
151
+ # Using environment variables
152
+ # Set ANTHROPIC_API_KEY=your_anthropic_api_key
153
+ # ANTHROPIC_CHAT_MODEL_ID=claude-sonnet-4-5-20250929
154
+
155
+ # Or passing parameters directly
156
+ client = AnthropicClient(
157
+ model_id="claude-sonnet-4-5-20250929",
158
+ api_key="your_anthropic_api_key",
159
+ )
160
+
161
+ # Or loading from a .env file
162
+ client = AnthropicClient(env_file_path="path/to/.env")
163
+
164
+ # Or passing in an existing client
165
+ from anthropic import AsyncAnthropic
166
+
167
+ anthropic_client = AsyncAnthropic(
168
+ api_key="your_anthropic_api_key", base_url="https://custom-anthropic-endpoint.com"
169
+ )
170
+ client = AnthropicClient(
171
+ model_id="claude-sonnet-4-5-20250929",
172
+ anthropic_client=anthropic_client,
173
+ )
174
+
175
+ """
176
+ try:
177
+ anthropic_settings = AnthropicSettings(
178
+ api_key=api_key, # type: ignore[arg-type]
179
+ chat_model_id=model_id,
180
+ env_file_path=env_file_path,
181
+ env_file_encoding=env_file_encoding,
182
+ )
183
+ except ValidationError as ex:
184
+ raise ServiceInitializationError("Failed to create Anthropic settings.", ex) from ex
185
+
186
+ if anthropic_client is None:
187
+ if not anthropic_settings.api_key:
188
+ raise ServiceInitializationError(
189
+ "Anthropic API key is required. Set via 'api_key' parameter "
190
+ "or 'ANTHROPIC_API_KEY' environment variable."
191
+ )
192
+
193
+ anthropic_client = AsyncAnthropic(
194
+ api_key=anthropic_settings.api_key.get_secret_value(),
195
+ default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
196
+ )
197
+
198
+ # Initialize parent
199
+ super().__init__(**kwargs)
200
+
201
+ # Initialize instance variables
202
+ self.anthropic_client = anthropic_client
203
+ self.additional_beta_flags = additional_beta_flags or []
204
+ self.model_id = anthropic_settings.chat_model_id
205
+ # streaming requires tracking the last function call ID and name
206
+ self._last_call_id_name: tuple[str, str] | None = None
207
+
208
+ # region Get response methods
209
+
210
+ async def _inner_get_response(
211
+ self,
212
+ *,
213
+ messages: MutableSequence[ChatMessage],
214
+ chat_options: ChatOptions,
215
+ **kwargs: Any,
216
+ ) -> ChatResponse:
217
+ # Extract necessary state from messages and options
218
+ run_options = self._create_run_options(messages, chat_options, **kwargs)
219
+ message = await self.anthropic_client.beta.messages.create(**run_options, stream=False)
220
+ return self._process_message(message)
221
+
222
+ async def _inner_get_streaming_response(
223
+ self,
224
+ *,
225
+ messages: MutableSequence[ChatMessage],
226
+ chat_options: ChatOptions,
227
+ **kwargs: Any,
228
+ ) -> AsyncIterable[ChatResponseUpdate]:
229
+ # Extract necessary state from messages and options
230
+ run_options = self._create_run_options(messages, chat_options, **kwargs)
231
+ async for chunk in await self.anthropic_client.beta.messages.create(**run_options, stream=True):
232
+ parsed_chunk = self._process_stream_event(chunk)
233
+ if parsed_chunk:
234
+ yield parsed_chunk
235
+
236
+ # region Create Run Options and Helpers
237
+
238
+ def _create_run_options(
239
+ self,
240
+ messages: MutableSequence[ChatMessage],
241
+ chat_options: ChatOptions,
242
+ **kwargs: Any,
243
+ ) -> dict[str, Any]:
244
+ """Create run options for the Anthropic client based on messages and chat options.
245
+
246
+ Args:
247
+ messages: The list of chat messages.
248
+ chat_options: The chat options.
249
+ kwargs: Additional keyword arguments.
250
+
251
+ Returns:
252
+ A dictionary of run options for the Anthropic client.
253
+ """
254
+ if chat_options.additional_properties and "additional_beta_flags" in chat_options.additional_properties:
255
+ betas = chat_options.additional_properties.pop("additional_beta_flags")
256
+ else:
257
+ betas = []
258
+ run_options: dict[str, Any] = {
259
+ "model": chat_options.model_id or self.model_id,
260
+ "messages": self._convert_messages_to_anthropic_format(messages),
261
+ "max_tokens": chat_options.max_tokens or ANTHROPIC_DEFAULT_MAX_TOKENS,
262
+ "extra_headers": {"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
263
+ "betas": {*BETA_FLAGS, *self.additional_beta_flags, *betas},
264
+ }
265
+
266
+ # Add any additional options from chat_options or kwargs
267
+ if chat_options.temperature is not None:
268
+ run_options["temperature"] = chat_options.temperature
269
+ if chat_options.top_p is not None:
270
+ run_options["top_p"] = chat_options.top_p
271
+ if chat_options.stop is not None:
272
+ run_options["stop_sequences"] = chat_options.stop
273
+ if messages and isinstance(messages[0], ChatMessage) and messages[0].role == Role.SYSTEM:
274
+ # first system message is passed as instructions
275
+ run_options["system"] = messages[0].text
276
+ if chat_options.tool_choice is not None:
277
+ match (
278
+ chat_options.tool_choice if isinstance(chat_options.tool_choice, str) else chat_options.tool_choice.mode
279
+ ):
280
+ case "auto":
281
+ run_options["tool_choice"] = {"type": "auto"}
282
+ if chat_options.allow_multiple_tool_calls is not None:
283
+ run_options["tool_choice"][ # type:ignore[reportArgumentType]
284
+ "disable_parallel_tool_use"
285
+ ] = not chat_options.allow_multiple_tool_calls
286
+ case "required":
287
+ if chat_options.tool_choice.required_function_name:
288
+ run_options["tool_choice"] = {
289
+ "type": "tool",
290
+ "name": chat_options.tool_choice.required_function_name,
291
+ }
292
+ if chat_options.allow_multiple_tool_calls is not None:
293
+ run_options["tool_choice"][ # type:ignore[reportArgumentType]
294
+ "disable_parallel_tool_use"
295
+ ] = not chat_options.allow_multiple_tool_calls
296
+ else:
297
+ run_options["tool_choice"] = {"type": "any"}
298
+ if chat_options.allow_multiple_tool_calls is not None:
299
+ run_options["tool_choice"][ # type:ignore[reportArgumentType]
300
+ "disable_parallel_tool_use"
301
+ ] = not chat_options.allow_multiple_tool_calls
302
+ case "none":
303
+ run_options["tool_choice"] = {"type": "none"}
304
+ case _:
305
+ logger.debug(f"Ignoring unsupported tool choice mode: {chat_options.tool_choice.mode} for now")
306
+ if tools_and_mcp := self._convert_tools_to_anthropic_format(chat_options.tools):
307
+ run_options.update(tools_and_mcp)
308
+ if chat_options.additional_properties:
309
+ run_options.update(chat_options.additional_properties)
310
+ run_options.update(kwargs)
311
+ return run_options
312
+
313
+ def _convert_messages_to_anthropic_format(self, messages: MutableSequence[ChatMessage]) -> list[dict[str, Any]]:
314
+ """Convert a list of ChatMessages to the format expected by the Anthropic client.
315
+
316
+ This skips the first message if it is a system message,
317
+ as Anthropic expects system instructions as a separate parameter.
318
+ """
319
+ # first system message is passed as instructions
320
+ if messages and isinstance(messages[0], ChatMessage) and messages[0].role == Role.SYSTEM:
321
+ return [self._convert_message_to_anthropic_format(msg) for msg in messages[1:]]
322
+ return [self._convert_message_to_anthropic_format(msg) for msg in messages]
323
+
324
+ def _convert_message_to_anthropic_format(self, message: ChatMessage) -> dict[str, Any]:
325
+ """Convert a ChatMessage to the format expected by the Anthropic client.
326
+
327
+ Args:
328
+ message: The ChatMessage to convert.
329
+
330
+ Returns:
331
+ A dictionary representing the message in Anthropic format.
332
+ """
333
+ a_content: list[dict[str, Any]] = []
334
+ for content in message.contents:
335
+ match content.type:
336
+ case "text":
337
+ a_content.append({"type": "text", "text": content.text})
338
+ case "data":
339
+ if content.has_top_level_media_type("image"):
340
+ a_content.append({
341
+ "type": "image",
342
+ "source": {
343
+ "data": content.get_data_bytes_as_str(),
344
+ "media_type": content.media_type,
345
+ "type": "base64",
346
+ },
347
+ })
348
+ else:
349
+ logger.debug(f"Ignoring unsupported data content media type: {content.media_type} for now")
350
+ case "uri":
351
+ if content.has_top_level_media_type("image"):
352
+ a_content.append({"type": "image", "source": {"type": "url", "url": content.uri}})
353
+ else:
354
+ logger.debug(f"Ignoring unsupported data content media type: {content.media_type} for now")
355
+ case "function_call":
356
+ a_content.append({
357
+ "type": "tool_use",
358
+ "id": content.call_id,
359
+ "name": content.name,
360
+ "input": content.parse_arguments(),
361
+ })
362
+ case "function_result":
363
+ a_content.append({
364
+ "type": "tool_result",
365
+ "tool_use_id": content.call_id,
366
+ "content": prepare_function_call_results(content.result),
367
+ "is_error": content.exception is not None,
368
+ })
369
+ case "text_reasoning":
370
+ a_content.append({"type": "thinking", "thinking": content.text})
371
+ case _:
372
+ logger.debug(f"Ignoring unsupported content type: {content.type} for now")
373
+
374
+ return {
375
+ "role": ROLE_MAP.get(message.role, "user"),
376
+ "content": a_content,
377
+ }
378
+
379
+ def _convert_tools_to_anthropic_format(
380
+ self, tools: list[ToolProtocol | MutableMapping[str, Any]] | None
381
+ ) -> dict[str, Any] | None:
382
+ if not tools:
383
+ return None
384
+ tool_list: list[MutableMapping[str, Any]] = []
385
+ mcp_server_list: list[MutableMapping[str, Any]] = []
386
+ for tool in tools:
387
+ match tool:
388
+ case MutableMapping():
389
+ tool_list.append(tool)
390
+ case AIFunction():
391
+ tool_list.append({
392
+ "type": "custom",
393
+ "name": tool.name,
394
+ "description": tool.description,
395
+ "input_schema": tool.parameters(),
396
+ })
397
+ case HostedWebSearchTool():
398
+ search_tool: dict[str, Any] = {
399
+ "type": "web_search_20250305",
400
+ "name": "web_search",
401
+ }
402
+ if tool.additional_properties:
403
+ search_tool.update(tool.additional_properties)
404
+ tool_list.append(search_tool)
405
+ case HostedCodeInterpreterTool():
406
+ code_tool: dict[str, Any] = {
407
+ "type": "code_execution_20250825",
408
+ "name": "code_execution",
409
+ }
410
+ tool_list.append(code_tool)
411
+ case HostedMCPTool():
412
+ server_def: dict[str, Any] = {
413
+ "type": "url",
414
+ "name": tool.name,
415
+ "url": str(tool.url),
416
+ }
417
+ if tool.allowed_tools:
418
+ server_def["tool_configuration"] = {"allowed_tools": list(tool.allowed_tools)}
419
+ if tool.headers and (auth := tool.headers.get("authorization")):
420
+ server_def["authorization_token"] = auth
421
+ mcp_server_list.append(server_def)
422
+ case _:
423
+ logger.debug(f"Ignoring unsupported tool type: {type(tool)} for now")
424
+
425
+ all_tools: dict[str, list[MutableMapping[str, Any]]] = {}
426
+ if tool_list:
427
+ all_tools["tools"] = tool_list
428
+ if mcp_server_list:
429
+ all_tools["mcp_servers"] = mcp_server_list
430
+ return all_tools
431
+
432
+ # region Response Processing Methods
433
+
434
+ def _process_message(self, message: BetaMessage) -> ChatResponse:
435
+ """Process the response from the Anthropic client.
436
+
437
+ Args:
438
+ message: The message returned by the Anthropic client.
439
+
440
+ Returns:
441
+ A ChatResponse object containing the processed response.
442
+ """
443
+ return ChatResponse(
444
+ response_id=message.id,
445
+ messages=[
446
+ ChatMessage(
447
+ role=Role.ASSISTANT,
448
+ contents=self._parse_message_contents(message.content),
449
+ raw_representation=message,
450
+ )
451
+ ],
452
+ usage_details=self._parse_message_usage(message.usage),
453
+ model_id=message.model,
454
+ finish_reason=FINISH_REASON_MAP.get(message.stop_reason) if message.stop_reason else None,
455
+ raw_response=message,
456
+ )
457
+
458
+ def _process_stream_event(self, event: BetaRawMessageStreamEvent) -> ChatResponseUpdate | None:
459
+ """Process a streaming event from the Anthropic client.
460
+
461
+ Args:
462
+ event: The streaming event returned by the Anthropic client.
463
+
464
+ Returns:
465
+ A ChatResponseUpdate object containing the processed update.
466
+ """
467
+ match event.type:
468
+ case "message_start":
469
+ usage_details: list[UsageContent] = []
470
+ if event.message.usage and (details := self._parse_message_usage(event.message.usage)):
471
+ usage_details.append(UsageContent(details=details))
472
+
473
+ return ChatResponseUpdate(
474
+ response_id=event.message.id,
475
+ contents=[*self._parse_message_contents(event.message.content), *usage_details],
476
+ model_id=event.message.model,
477
+ finish_reason=FINISH_REASON_MAP.get(event.message.stop_reason)
478
+ if event.message.stop_reason
479
+ else None,
480
+ raw_response=event,
481
+ )
482
+ case "message_delta":
483
+ usage = self._parse_message_usage(event.usage)
484
+ return ChatResponseUpdate(
485
+ contents=[UsageContent(details=usage, raw_representation=event.usage)] if usage else [],
486
+ raw_response=event,
487
+ )
488
+ case "message_stop":
489
+ logger.debug("Received message_stop event; no content to process.")
490
+ case "content_block_start":
491
+ contents = self._parse_message_contents([event.content_block])
492
+ return ChatResponseUpdate(
493
+ contents=contents,
494
+ raw_response=event,
495
+ )
496
+ case "content_block_delta":
497
+ contents = self._parse_message_contents([event.delta])
498
+ return ChatResponseUpdate(
499
+ contents=contents,
500
+ raw_response=event,
501
+ )
502
+ case "content_block_stop":
503
+ logger.debug("Received content_block_stop event; no content to process.")
504
+ case _:
505
+ logger.debug(f"Ignoring unsupported event type: {event.type}")
506
+ return None
507
+
508
+ def _parse_message_usage(self, usage: BetaUsage | BetaMessageDeltaUsage | None) -> UsageDetails | None:
509
+ """Parse usage details from the Anthropic message usage."""
510
+ if not usage:
511
+ return None
512
+ usage_details = UsageDetails(output_token_count=usage.output_tokens)
513
+ if usage.input_tokens is not None:
514
+ usage_details.input_token_count = usage.input_tokens
515
+ if usage.cache_creation_input_tokens is not None:
516
+ usage_details.additional_counts["anthropic.cache_creation_input_tokens"] = usage.cache_creation_input_tokens
517
+ if usage.cache_read_input_tokens is not None:
518
+ usage_details.additional_counts["anthropic.cache_read_input_tokens"] = usage.cache_read_input_tokens
519
+ return usage_details
520
+
521
+ def _parse_message_contents(
522
+ self, content: Sequence[BetaContentBlock | BetaRawContentBlockDelta | BetaTextBlock]
523
+ ) -> list[Contents]:
524
+ """Parse contents from the Anthropic message."""
525
+ contents: list[Contents] = []
526
+ for content_block in content:
527
+ match content_block.type:
528
+ case "text" | "text_delta":
529
+ contents.append(
530
+ TextContent(
531
+ text=content_block.text,
532
+ raw_representation=content_block,
533
+ annotations=self._parse_citations(content_block),
534
+ )
535
+ )
536
+ case "tool_use" | "mcp_tool_use" | "server_tool_use":
537
+ self._last_call_id_name = (content_block.id, content_block.name)
538
+ contents.append(
539
+ FunctionCallContent(
540
+ call_id=content_block.id,
541
+ name=content_block.name,
542
+ arguments=content_block.input,
543
+ raw_representation=content_block,
544
+ )
545
+ )
546
+ case "mcp_tool_result":
547
+ call_id, name = self._last_call_id_name or (None, None)
548
+ contents.append(
549
+ FunctionResultContent(
550
+ call_id=content_block.tool_use_id,
551
+ name=name if name and call_id == content_block.tool_use_id else "mcp_tool",
552
+ result=self._parse_message_contents(content_block.content)
553
+ if isinstance(content_block.content, list)
554
+ else content_block.content,
555
+ raw_representation=content_block,
556
+ )
557
+ )
558
+ case "web_search_tool_result" | "web_fetch_tool_result":
559
+ call_id, name = self._last_call_id_name or (None, None)
560
+ contents.append(
561
+ FunctionResultContent(
562
+ call_id=content_block.tool_use_id,
563
+ name=name if name and call_id == content_block.tool_use_id else "web_tool",
564
+ result=content_block.content,
565
+ raw_representation=content_block,
566
+ )
567
+ )
568
+ case (
569
+ "code_execution_tool_result"
570
+ | "bash_code_execution_tool_result"
571
+ | "text_editor_code_execution_tool_result"
572
+ ):
573
+ call_id, name = self._last_call_id_name or (None, None)
574
+ if (
575
+ content_block.content
576
+ and (
577
+ content_block.content.type == "bash_code_execution_result"
578
+ or content_block.content.type == "code_execution_result"
579
+ )
580
+ and content_block.content.content
581
+ ):
582
+ for result_content in content_block.content.content:
583
+ if hasattr(result_content, "file_id"):
584
+ contents.append(
585
+ HostedFileContent(file_id=result_content.file_id, raw_representation=result_content)
586
+ )
587
+ contents.append(
588
+ FunctionResultContent(
589
+ call_id=content_block.tool_use_id,
590
+ name=name if name and call_id == content_block.tool_use_id else "code_execution_tool",
591
+ result=content_block.content,
592
+ raw_representation=content_block,
593
+ )
594
+ )
595
+ case "input_json_delta":
596
+ call_id, name = self._last_call_id_name if self._last_call_id_name else ("", "")
597
+ contents.append(
598
+ FunctionCallContent(
599
+ call_id=call_id,
600
+ name=name,
601
+ arguments=content_block.partial_json,
602
+ raw_representation=content_block,
603
+ )
604
+ )
605
+ case "thinking" | "thinking_delta":
606
+ contents.append(TextReasoningContent(text=content_block.thinking, raw_representation=content_block))
607
+ case _:
608
+ logger.debug(f"Ignoring unsupported content type: {content_block.type} for now")
609
+ return contents
610
+
611
+ def _parse_citations(
612
+ self, content_block: BetaContentBlock | BetaRawContentBlockDelta | BetaTextBlock
613
+ ) -> list[Annotations] | None:
614
+ content_citations = getattr(content_block, "citations", None)
615
+ if not content_citations:
616
+ return None
617
+ annotations: list[Annotations] = []
618
+ for citation in content_citations:
619
+ cit = CitationAnnotation(raw_representation=citation)
620
+ match citation.type:
621
+ case "char_location":
622
+ cit.title = citation.title
623
+ cit.snippet = citation.cited_text
624
+ if citation.file_id:
625
+ cit.file_id = citation.file_id
626
+ if not cit.annotated_regions:
627
+ cit.annotated_regions = []
628
+ cit.annotated_regions.append(
629
+ TextSpanRegion(start_index=citation.start_char_index, end_index=citation.end_char_index)
630
+ )
631
+ case "page_location":
632
+ cit.title = citation.document_title
633
+ cit.snippet = citation.cited_text
634
+ if citation.file_id:
635
+ cit.file_id = citation.file_id
636
+ if not cit.annotated_regions:
637
+ cit.annotated_regions = []
638
+ cit.annotated_regions.append(
639
+ TextSpanRegion(
640
+ start_index=citation.start_page_number,
641
+ end_index=citation.end_page_number,
642
+ )
643
+ )
644
+ case "content_block_location":
645
+ cit.title = citation.document_title
646
+ cit.snippet = citation.cited_text
647
+ if citation.file_id:
648
+ cit.file_id = citation.file_id
649
+ if not cit.annotated_regions:
650
+ cit.annotated_regions = []
651
+ cit.annotated_regions.append(
652
+ TextSpanRegion(start_index=citation.start_block_index, end_index=citation.end_block_index)
653
+ )
654
+ case "web_search_result_location":
655
+ cit.title = citation.title
656
+ cit.snippet = citation.cited_text
657
+ cit.url = citation.url
658
+ case "search_result_location":
659
+ cit.title = citation.title
660
+ cit.snippet = citation.cited_text
661
+ cit.url = citation.source
662
+ if not cit.annotated_regions:
663
+ cit.annotated_regions = []
664
+ cit.annotated_regions.append(
665
+ TextSpanRegion(start_index=citation.start_block_index, end_index=citation.end_block_index)
666
+ )
667
+ case _:
668
+ logger.debug(f"Unknown citation type encountered: {citation.type}")
669
+ annotations.append(cit)
670
+ return annotations or None
671
+
672
+ def service_url(self) -> str:
673
+ """Get the service URL for the chat client.
674
+
675
+ Returns:
676
+ The service URL for the chat client, or None if not set.
677
+ """
678
+ return str(self.anthropic_client.base_url)
@@ -0,0 +1,89 @@
1
+ [project]
2
+ name = "agent-framework-anthropic"
3
+ description = "Anthropic integration for Microsoft Agent Framework."
4
+ authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ version = "1.0.0b251209"
8
+ license-files = ["LICENSE"]
9
+ urls.homepage = "https://aka.ms/agent-framework"
10
+ urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
11
+ urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
12
+ urls.issues = "https://github.com/microsoft/agent-framework/issues"
13
+ classifiers = [
14
+ "License :: OSI Approved :: MIT License",
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "agent-framework-core",
27
+ "anthropic>=0.70.0,<1",
28
+ ]
29
+
30
+ [tool.uv]
31
+ prerelease = "if-necessary-or-explicit"
32
+ environments = [
33
+ "sys_platform == 'darwin'",
34
+ "sys_platform == 'linux'",
35
+ "sys_platform == 'win32'"
36
+ ]
37
+
38
+ [tool.uv-dynamic-versioning]
39
+ fallback-version = "0.0.0"
40
+ [tool.pytest.ini_options]
41
+ testpaths = 'tests'
42
+ addopts = "-ra -q -r fEX"
43
+ asyncio_mode = "auto"
44
+ asyncio_default_fixture_loop_scope = "function"
45
+ filterwarnings = [
46
+ "ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
47
+ ]
48
+ timeout = 120
49
+
50
+ [tool.ruff]
51
+ extend = "../../pyproject.toml"
52
+
53
+ [tool.coverage.run]
54
+ omit = [
55
+ "**/__init__.py"
56
+ ]
57
+
58
+ [tool.pyright]
59
+ extends = "../../pyproject.toml"
60
+ exclude = ['tests']
61
+
62
+ [tool.mypy]
63
+ plugins = ['pydantic.mypy']
64
+ strict = true
65
+ python_version = "3.10"
66
+ ignore_missing_imports = true
67
+ disallow_untyped_defs = true
68
+ no_implicit_optional = true
69
+ check_untyped_defs = true
70
+ warn_return_any = true
71
+ show_error_codes = true
72
+ warn_unused_ignores = false
73
+ disallow_incomplete_defs = true
74
+ disallow_untyped_decorators = true
75
+
76
+ [tool.bandit]
77
+ targets = ["agent_framework_anthropic"]
78
+ exclude_dirs = ["tests"]
79
+
80
+ [tool.poe]
81
+ executor.type = "uv"
82
+ include = "../../shared_tasks.toml"
83
+ [tool.poe.tasks]
84
+ mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_anthropic"
85
+ test = "pytest --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered tests"
86
+
87
+ [build-system]
88
+ requires = ["flit-core >= 3.11,<4.0"]
89
+ build-backend = "flit_core.buildapi"