From d3257bcb033a08f5fd95ed1cb912a632722fb4f0 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 20 Mar 2026 17:21:57 +0100 Subject: [PATCH 01/13] Python: Provider-leading client design & OpenAI package extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Major refactoring of the Python Agent Framework client architecture: - Extract OpenAI clients into new `agent-framework-openai` package - Core package no longer depends on openai, azure-identity, azure-ai-projects - Rename clients for discoverability: OpenAIResponsesClient → OpenAIChatClient, OpenAIChatClient → OpenAIChatCompletionClient - Unify `model_id`/`deployment_name`/`model_deployment_name` → `model` param - New FoundryChatClient for Azure AI Foundry Responses API - New FoundryAgent/FoundryAgentClient for connecting to pre-configured Foundry agents - Remove OpenAIBase/OpenAIConfigMixin from non-deprecated client MRO - Deprecate AzureOpenAI* clients, AzureAIClient, OpenAIAssistantsClient - Reorganize samples: azure_openai+azure_ai+azure_ai_agent → azure/ - ADR-0020: Provider-Leading Client Design Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../0020-provider-leading-clients.md | 72 ++ .../_context_provider.py | 5 +- .../agent_framework_azure_ai/__init__.py | 35 +- .../_agent_provider.py | 2 +- .../agent_framework_azure_ai/_chat_client.py | 2 +- .../agent_framework_azure_ai/_client.py | 16 +- .../_deprecated_azure_openai.py | 895 ++++++++++++++++++ .../_entra_id_authentication.py | 3 +- .../_foundry_agent.py | 226 +++++ .../_foundry_agent_client.py | 351 +++++++ .../_foundry_chat_client.py | 481 ++++++++++ .../_foundry_memory_provider.py | 2 +- .../_project_provider.py | 2 +- .../tests/azure_openai}/conftest.py | 3 +- .../test_azure_assistants_client.py | 35 +- .../azure_openai}/test_azure_chat_client.py | 29 +- .../test_azure_embedding_client.py | 9 +- .../test_azure_responses_client.py | 35 +- .../test_entra_id_authentication.py | 4 +- .../azure-ai/tests/test_azure_ai_client.py | 47 +- .../azure-ai/tests/test_foundry_agent.py | 344 +++++++ .../_history_provider.py | 5 +- .../packages/core/agent_framework/_types.py | 54 +- .../core/agent_framework/azure/__init__.py | 38 +- .../core/agent_framework/azure/__init__.pyi | 55 +- .../azure/_assistants_client.py | 194 ---- .../agent_framework/azure/_chat_client.py | 349 ------- .../azure/_embedding_client.py | 141 --- .../azure/_responses_client.py | 277 ------ .../core/agent_framework/azure/_shared.py | 223 ----- .../core/agent_framework/openai/__init__.py | 89 +- .../core/agent_framework/openai/__init__.pyi | 7 + .../packages/core/tests/core/test_agents.py | 6 +- .../packages/core/tests/core/test_clients.py | 28 +- python/packages/core/tests/core/test_types.py | 26 +- python/packages/core/tests/openai/conftest.py | 51 - .../_foundry_local_client.py | 26 +- .../packages/foundry_local/tests/conftest.py | 2 +- .../tests/test_foundry_local_client.py | 44 +- .../lab/lightning/tests/test_lightning.py | 8 +- python/packages/openai/AGENTS.md | 34 + python/packages/openai/LICENSE | 21 + python/packages/openai/README.md | 17 + .../openai/agent_framework_openai/__init__.py | 87 ++ .../_assistant_provider.py | 13 +- .../_assistants_client.py | 90 +- .../agent_framework_openai/_chat_client.py} | 325 ++++--- .../_chat_completion_client.py} | 283 ++++-- .../_embedding_client.py | 172 +++- .../agent_framework_openai}/_exceptions.py | 3 +- .../agent_framework_openai}/_shared.py | 91 +- .../openai/agent_framework_openai/py.typed | 0 python/packages/openai/pyproject.toml | 97 ++ .../openai/tests/assets/sample_image.jpg | Bin 0 -> 182161 bytes .../packages/openai/tests/openai/conftest.py | 127 +++ .../tests/openai/test_assistant_provider.py | 15 +- .../openai/test_openai_assistants_client.py | 92 +- .../tests/openai/test_openai_chat_client.py} | 392 ++++---- .../test_openai_chat_completion_client.py} | 156 +-- ...est_openai_chat_completion_client_base.py} | 48 +- .../openai/test_openai_embedding_client.py | 30 +- .../agent_framework_purview/_client.py | 7 +- .../agent_framework_purview/_middleware.py | 7 +- .../samples/01-get-started/01_hello_agent.py | 26 +- python/samples/01-get-started/02_add_tools.py | 27 +- .../samples/01-get-started/03_multi_turn.py | 24 +- python/samples/01-get-started/04_memory.py | 25 +- .../01-get-started/06_host_your_agent.py | 27 +- python/samples/02-agents/auto_retry.py | 17 +- .../samples/02-agents/background_responses.py | 2 +- .../chat_client/built_in_chat_clients.py | 23 +- .../chat_client/chat_response_cancellation.py | 6 +- .../chat_client/custom_chat_client.py | 4 +- .../azure_ai_foundry_memory.py | 12 +- .../azure_ai_with_search_context_agentic.py | 14 +- .../azure_ai_with_search_context_semantic.py | 14 +- .../context_providers/mem0/mem0_basic.py | 20 +- .../context_providers/mem0/mem0_oss.py | 20 +- .../context_providers/mem0/mem0_sessions.py | 16 +- .../redis/azure_redis_conversation.py | 15 +- .../context_providers/redis/redis_basics.py | 18 +- .../redis/redis_conversation.py | 11 +- .../context_providers/redis/redis_sessions.py | 21 +- .../simple_context_provider.py | 8 +- .../conversations/custom_history_provider.py | 5 +- .../conversations/redis_history_provider.py | 17 +- .../conversations/suspend_resume_session.py | 13 +- .../azure_openai_responses_agent.py | 2 - .../declarative/get_weather_agent.py | 7 +- .../02-agents/declarative/inline_yaml.py | 4 +- .../02-agents/declarative/mcp_tool_yaml.py | 4 +- .../declarative/microsoft_learn_agent.py | 2 - .../declarative/openai_responses_agent.py | 3 - .../devui/azure_responses_agent/agent.py | 18 +- .../02-agents/devui/foundry_agent/__init__.py | 1 - .../02-agents/devui/foundry_agent/agent.py | 8 +- .../samples/02-agents/devui/in_memory_mode.py | 6 +- .../02-agents/devui/spam_workflow/__init__.py | 1 - .../devui/weather_agent_azure/__init__.py | 1 - .../devui/weather_agent_azure/agent.py | 4 +- .../devui/workflow_agents/__init__.py | 1 - .../devui/workflow_agents/workflow.py | 16 +- .../02-agents/embeddings/openai_embeddings.py | 2 +- .../02-agents/mcp/agent_as_mcp_server.py | 7 +- .../samples/02-agents/mcp/mcp_github_pat.py | 2 +- .../agent_and_run_level_middleware.py | 6 +- .../02-agents/middleware/chat_middleware.py | 12 +- .../middleware/class_based_middleware.py | 6 +- .../middleware/decorator_middleware.py | 6 +- .../exception_handling_with_middleware.py | 7 +- .../middleware/function_based_middleware.py | 6 +- .../middleware/middleware_termination.py | 9 +- .../override_result_with_middleware.py | 8 +- .../middleware/runtime_context_delegation.py | 22 +- .../middleware/session_behavior_middleware.py | 6 +- .../middleware/shared_state_middleware.py | 6 +- .../multimodal_input/azure_chat_multimodal.py | 10 +- .../azure_responses_multimodal.py | 10 +- .../openai_chat_multimodal.py | 8 +- .../advanced_manual_setup_console_output.py | 4 +- .../observability/agent_observability.py | 4 +- .../agent_with_foundry_tracing.py | 4 +- .../azure_ai_agent_observability.py | 78 -- .../providers/anthropic/anthropic_advanced.py | 4 +- .../providers/anthropic/anthropic_basic.py | 8 +- .../providers/anthropic/anthropic_foundry.py | 4 +- .../providers/anthropic/anthropic_skills.py | 5 +- .../02-agents/providers/azure/README.md | 269 ++++++ .../providers/azure/foundry_agent_basic.py | 42 + .../azure/foundry_agent_custom_client.py | 62 ++ .../providers/azure/foundry_agent_hosted.py | 33 + .../azure/foundry_agent_with_env_vars.py | 40 + .../foundry_agent_with_function_tools.py | 50 + .../foundry_chat_client.py} | 32 +- .../foundry_chat_client_basic.py} | 16 +- ...dry_chat_client_code_interpreter_files.py} | 4 +- .../foundry_chat_client_image_analysis.py} | 9 +- ...ndry_chat_client_with_code_interpreter.py} | 4 +- ...dry_chat_client_with_explicit_settings.py} | 14 +- .../foundry_chat_client_with_file_search.py} | 10 +- ...oundry_chat_client_with_function_tools.py} | 8 +- .../foundry_chat_client_with_hosted_mcp.py} | 10 +- .../foundry_chat_client_with_local_mcp.py} | 8 +- .../foundry_chat_client_with_session.py} | 10 +- ...nai_chat_completion_client_azure_basic.py} | 12 +- ...on_client_azure_with_explicit_settings.py} | 20 +- ...etion_client_azure_with_function_tools.py} | 14 +- ...t_completion_client_azure_with_session.py} | 94 +- .../02-agents/providers/azure_ai/README.md | 95 -- .../providers/azure_ai/azure_ai_basic.py | 91 -- .../azure_ai/azure_ai_provider_methods.py | 258 ----- .../azure_ai/azure_ai_use_latest_version.py | 73 -- .../azure_ai/azure_ai_with_agent_as_tool.py | 74 -- .../azure_ai/azure_ai_with_agent_to_agent.py | 57 -- .../azure_ai_with_application_endpoint.py | 44 - .../azure_ai/azure_ai_with_azure_ai_search.py | 110 --- .../azure_ai_with_bing_custom_search.py | 54 -- .../azure_ai/azure_ai_with_bing_grounding.py | 60 -- .../azure_ai_with_browser_automation.py | 58 -- .../azure_ai_with_code_interpreter.py | 66 -- ..._ai_with_code_interpreter_file_download.py | 236 ----- ...i_with_code_interpreter_file_generation.py | 123 --- .../azure_ai_with_content_filtering.py | 70 -- .../azure_ai/azure_ai_with_existing_agent.py | 70 -- .../azure_ai_with_existing_conversation.py | 108 --- .../azure_ai_with_explicit_settings.py | 61 -- .../azure_ai/azure_ai_with_file_search.py | 80 -- .../azure_ai/azure_ai_with_hosted_mcp.py | 135 --- .../azure_ai_with_image_generation.py | 111 --- .../azure_ai/azure_ai_with_local_mcp.py | 60 -- .../azure_ai/azure_ai_with_memory_search.py | 92 -- .../azure_ai_with_microsoft_fabric.py | 52 - .../azure_ai/azure_ai_with_openapi.py | 58 -- .../azure_ai/azure_ai_with_reasoning.py | 98 -- .../azure_ai/azure_ai_with_response_format.py | 59 -- .../azure_ai_with_runtime_json_schema.py | 68 -- .../azure_ai/azure_ai_with_session.py | 165 ---- .../azure_ai/azure_ai_with_sharepoint.py | 53 -- .../azure_ai/azure_ai_with_web_search.py | 57 -- .../providers/azure_ai_agent/README.md | 114 --- .../azure_ai_agent/azure_ai_basic.py | 87 -- .../azure_ai_provider_methods.py | 149 --- .../azure_ai_with_azure_ai_search.py | 120 --- .../azure_ai_with_bing_custom_search.py | 67 -- .../azure_ai_with_bing_grounding.py | 62 -- .../azure_ai_with_bing_grounding_citations.py | 90 -- .../azure_ai_with_code_interpreter.py | 67 -- ...i_with_code_interpreter_file_generation.py | 106 --- .../azure_ai_with_existing_agent.py | 52 - .../azure_ai_with_existing_session.py | 67 -- .../azure_ai_with_explicit_settings.py | 60 -- .../azure_ai_with_file_search.py | 84 -- .../azure_ai_with_function_tools.py | 155 --- .../azure_ai_with_hosted_mcp.py | 82 -- .../azure_ai_agent/azure_ai_with_local_mcp.py | 95 -- .../azure_ai_with_multiple_tools.py | 113 --- .../azure_ai_with_openapi_tools.py | 97 -- .../azure_ai_with_response_format.py | 91 -- .../azure_ai_agent/azure_ai_with_session.py | 170 ---- .../providers/azure_openai/README.md | 60 -- .../azure_openai/azure_assistants_basic.py | 79 -- .../azure_assistants_with_code_interpreter.py | 76 -- ...zure_assistants_with_existing_assistant.py | 68 -- ...azure_assistants_with_explicit_settings.py | 55 -- .../azure_assistants_with_function_tools.py | 140 --- .../azure_assistants_with_session.py | 150 --- .../foundry_local/foundry_local_agent.py | 4 +- .../providers/ollama/ollama_agent_basic.py | 8 +- .../ollama/ollama_agent_reasoning.py | 4 +- .../ollama/ollama_with_openai_chat_client.py | 22 +- .../openai/openai_assistants_basic.py | 4 +- .../openai_assistants_provider_methods.py | 16 +- ...openai_assistants_with_code_interpreter.py | 2 +- ...enai_assistants_with_existing_assistant.py | 11 +- ...penai_assistants_with_explicit_settings.py | 2 +- .../openai_assistants_with_file_search.py | 2 +- .../openai_assistants_with_function_tools.py | 6 +- .../openai_assistants_with_response_format.py | 2 +- .../openai/openai_assistants_with_session.py | 6 +- .../openai/openai_chat_client_basic.py | 8 +- ...enai_chat_client_with_explicit_settings.py | 10 +- .../openai_chat_client_with_local_mcp.py | 3 +- ...ai_chat_client_with_runtime_json_schema.py | 7 +- .../openai_chat_client_with_web_search.py | 2 +- .../openai_responses_client_image_analysis.py | 5 +- ...penai_responses_client_image_generation.py | 4 +- .../openai_responses_client_reasoning.py | 4 +- ...onses_client_streaming_image_generation.py | 19 +- ...nai_responses_client_with_agent_as_tool.py | 6 +- ...responses_client_with_explicit_settings.py | 10 +- ...sponses_client_with_runtime_json_schema.py | 7 +- ...responses_client_with_structured_output.py | 8 +- .../code_defined_skill/code_defined_skill.py | 10 +- .../file_based_skill/file_based_skill.py | 10 +- .../skills/unit-converter/scripts/convert.py | 5 - .../skills/mixed_skills/mixed_skills.py | 10 +- .../skills/unit-converter/scripts/convert.py | 5 - .../skills/script_approval/script_approval.py | 14 +- .../skills/subprocess_script_runner.py | 13 - .../agent_as_tool_with_session_propagation.py | 6 +- .../tools/control_total_tool_executions.py | 20 +- .../function_invocation_configuration.py | 4 +- .../tools/function_tool_declaration_only.py | 5 +- ...ool_from_dict_with_dependency_injection.py | 5 +- .../function_tool_recover_from_failures.py | 5 +- ...unction_tool_with_approval_and_sessions.py | 6 +- .../function_tool_with_explicit_schema.py | 5 +- .../tools/function_tool_with_kwargs.py | 5 +- .../function_tool_with_max_exceptions.py | 5 +- .../function_tool_with_max_invocations.py | 5 +- .../function_tool_with_session_injection.py | 5 +- .../samples/02-agents/tools/tool_in_class.py | 5 +- python/samples/02-agents/typed_options.py | 6 +- .../_start-here/step2_agents_in_a_workflow.py | 20 +- .../_start-here/step3_streaming.py | 20 +- .../agents/azure_ai_agents_streaming.py | 16 +- .../azure_ai_agents_with_shared_session.py | 19 +- .../agents/azure_chat_agents_and_executor.py | 25 +- .../agents/azure_chat_agents_streaming.py | 26 +- ...re_chat_agents_tool_calls_with_feedback.py | 28 +- .../agents/concurrent_workflow_as_agent.py | 28 +- .../agents/custom_agent_executors.py | 28 +- .../agents/group_chat_workflow_as_agent.py | 33 +- .../agents/handoff_workflow_as_agent.py | 57 +- .../agents/magentic_workflow_as_agent.py | 28 +- .../agents/sequential_workflow_as_agent.py | 22 +- .../workflow_as_agent_human_in_the_loop.py | 26 +- .../agents/workflow_as_agent_kwargs.py | 24 +- .../workflow_as_agent_reflection_pattern.py | 32 +- .../agents/workflow_as_agent_with_session.py | 32 +- .../checkpoint_with_human_in_the_loop.py | 12 +- .../workflow_as_agent_checkpoint.py | 43 +- .../composition/sub_workflow_kwargs.py | 14 +- .../control-flow/edge_condition.py | 24 +- .../multi_selection_edge_group.py | 31 +- .../03-workflows/control-flow/simple_loop.py | 15 +- .../control-flow/switch_case_edge_group.py | 24 +- .../agent_to_function_tool/main.py | 11 +- .../declarative/customer_support/main.py | 21 +- .../customer_support/ticketing_plugin.py | 4 - .../declarative/deep_research/main.py | 23 +- .../declarative/function_tools/main.py | 12 +- .../declarative/marketing/main.py | 15 +- .../declarative/simple_workflow/main.py | 8 - .../declarative/student_teacher/main.py | 15 +- .../human-in-the-loop/agents_with_HITL.py | 25 +- .../agents_with_approval_requests.py | 14 +- .../agents_with_declaration_only_tools.py | 16 +- .../concurrent_request_info.py | 24 +- .../group_chat_request_info.py | 25 +- .../guessing_game_with_human_input.py | 16 +- .../sequential_request_info.py | 22 +- .../orchestrations/concurrent_agents.py | 22 +- .../concurrent_custom_agent_executors.py | 29 +- .../concurrent_custom_aggregator.py | 22 +- .../group_chat_agent_manager.py | 12 +- .../group_chat_philosophical_debate.py | 14 +- .../group_chat_simple_selector.py | 12 +- .../orchestrations/handoff_autonomous.py | 23 +- .../orchestrations/handoff_simple.py | 28 +- .../handoff_with_code_interpreter_file.py | 17 +- ...ff_with_tool_approval_checkpoint_resume.py | 23 +- .../03-workflows/orchestrations/magentic.py | 12 +- .../orchestrations/magentic_checkpoint.py | 24 +- .../magentic_human_plan_review.py | 12 +- .../orchestrations/sequential_agents.py | 18 +- .../sequential_custom_executors.py | 16 +- .../parallelism/fan_out_fan_in_edges.py | 34 +- .../state-management/state_with_agents.py | 24 +- .../state-management/workflow_kwargs.py | 14 +- .../concurrent_builder_tool_approval.py | 17 +- .../group_chat_builder_tool_approval.py | 17 +- .../sequential_builder_tool_approval.py | 14 +- .../concurrent_with_visualization.py | 34 +- python/samples/04-hosting/a2a/a2a_server.py | 18 +- .../04-hosting/a2a/agent_definitions.py | 14 +- .../01_single_agent/function_app.py | 10 +- .../02_multi_agent/function_app.py | 14 +- .../03_reliable_streaming/function_app.py | 9 +- .../function_app.py | 10 +- .../function_app.py | 14 +- .../function_app.py | 13 +- .../function_app.py | 10 +- .../08_mcp_server/function_app.py | 13 +- .../09_workflow_shared_state/function_app.py | 15 +- .../function_app.py | 13 +- .../11_workflow_parallel/function_app.py | 16 +- .../12_workflow_hitl/function_app.py | 10 +- .../durabletask/01_single_agent/client.py | 6 +- .../durabletask/01_single_agent/sample.py | 12 +- .../durabletask/01_single_agent/worker.py | 11 +- .../durabletask/02_multi_agent/client.py | 6 +- .../durabletask/02_multi_agent/sample.py | 11 +- .../durabletask/02_multi_agent/worker.py | 16 +- .../03_single_agent_streaming/client.py | 6 +- .../03_single_agent_streaming/sample.py | 13 +- .../03_single_agent_streaming/worker.py | 11 +- .../client.py | 6 +- .../sample.py | 15 +- .../worker.py | 11 +- .../client.py | 6 +- .../sample.py | 13 +- .../worker.py | 14 +- .../client.py | 6 +- .../sample.py | 2 +- .../worker.py | 14 +- .../client.py | 6 +- .../sample.py | 13 +- .../worker.py | 11 +- .../05-end-to-end/chatkit-integration/app.py | 4 +- .../red_teaming/red_team_agent_sample.py | 9 +- .../self_reflection/self_reflection.py | 12 +- .../agent_with_hosted_mcp/main.py | 10 +- .../agent_with_local_tools/main.py | 9 +- .../agent_with_text_search_rag/main.py | 7 +- .../hosted_agents/agents_in_workflow/main.py | 14 +- .../main.py | 14 +- .../m365-agent/m365_agent_demo/app.py | 5 +- .../purview_agent/sample_purview_agent.py | 12 +- .../workflow_evaluation/create_workflow.py | 45 +- .../workflow_evaluation/run_evaluation.py | 12 +- python/samples/SAMPLE_GUIDELINES.md | 64 +- .../01_round_robin_group_chat.py | 19 +- .../orchestrations/02_selector_group_chat.py | 12 +- .../orchestrations/03_swarm.py | 10 +- .../orchestrations/04_magentic_one.py | 13 +- .../single_agent/01_basic_assistant_agent.py | 6 +- .../02_assistant_agent_with_tool.py | 6 +- .../03_assistant_agent_thread_and_stream.py | 6 +- .../single_agent/04_agent_as_tool.py | 8 +- .../ag_ui_workflow_handoff/backend/server.py | 8 +- .../azure_ai_agent/01_basic_azure_ai_agent.py | 65 -- ...02_azure_ai_agent_with_code_interpreter.py | 76 -- ...03_azure_ai_agent_threads_and_followups.py | 81 -- .../01_basic_chat_completion.py | 4 +- .../02_chat_completion_with_tool.py | 5 +- .../03_chat_completion_thread_and_stream.py | 4 +- .../01_basic_openai_assistant.py | 12 +- ..._openai_assistant_with_code_interpreter.py | 4 +- .../03_openai_assistant_function_tool.py | 4 +- .../01_basic_responses_agent.py | 2 +- .../02_responses_agent_with_tool.py | 2 +- .../03_responses_agent_structured_output.py | 2 +- .../orchestrations/concurrent_basic.py | 6 +- .../orchestrations/group_chat.py | 8 +- .../orchestrations/handoff.py | 19 +- .../orchestrations/magentic.py | 2 +- .../orchestrations/sequential.py | 6 +- python/uv.lock | 18 + 389 files changed, 6323 insertions(+), 9490 deletions(-) create mode 100644 docs/decisions/0020-provider-leading-clients.md create mode 100644 python/packages/azure-ai/agent_framework_azure_ai/_deprecated_azure_openai.py rename python/packages/{core/agent_framework/azure => azure-ai/agent_framework_azure_ai}/_entra_id_authentication.py (97%) create mode 100644 python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py create mode 100644 python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent_client.py create mode 100644 python/packages/azure-ai/agent_framework_azure_ai/_foundry_chat_client.py rename python/packages/{core/tests/azure => azure-ai/tests/azure_openai}/conftest.py (99%) rename python/packages/{core/tests/azure => azure-ai/tests/azure_openai}/test_azure_assistants_client.py (95%) rename python/packages/{core/tests/azure => azure-ai/tests/azure_openai}/test_azure_chat_client.py (98%) rename python/packages/{core/tests/azure => azure-ai/tests/azure_openai}/test_azure_embedding_client.py (97%) rename python/packages/{core/tests/azure => azure-ai/tests/azure_openai}/test_azure_responses_client.py (95%) rename python/packages/{core/tests/azure => azure-ai/tests/azure_openai}/test_entra_id_authentication.py (97%) create mode 100644 python/packages/azure-ai/tests/test_foundry_agent.py delete mode 100644 python/packages/core/agent_framework/azure/_assistants_client.py delete mode 100644 python/packages/core/agent_framework/azure/_chat_client.py delete mode 100644 python/packages/core/agent_framework/azure/_embedding_client.py delete mode 100644 python/packages/core/agent_framework/azure/_responses_client.py delete mode 100644 python/packages/core/agent_framework/azure/_shared.py create mode 100644 python/packages/core/agent_framework/openai/__init__.pyi delete mode 100644 python/packages/core/tests/openai/conftest.py create mode 100644 python/packages/openai/AGENTS.md create mode 100644 python/packages/openai/LICENSE create mode 100644 python/packages/openai/README.md create mode 100644 python/packages/openai/agent_framework_openai/__init__.py rename python/packages/{core/agent_framework/openai => openai/agent_framework_openai}/_assistant_provider.py (98%) rename python/packages/{core/agent_framework/openai => openai/agent_framework_openai}/_assistants_client.py (96%) rename python/packages/{core/agent_framework/openai/_responses_client.py => openai/agent_framework_openai/_chat_client.py} (92%) rename python/packages/{core/agent_framework/openai/_chat_client.py => openai/agent_framework_openai/_chat_completion_client.py} (82%) rename python/packages/{core/agent_framework/openai => openai/agent_framework_openai}/_embedding_client.py (51%) rename python/packages/{core/agent_framework/openai => openai/agent_framework_openai}/_exceptions.py (97%) rename python/packages/{core/agent_framework/openai => openai/agent_framework_openai}/_shared.py (81%) create mode 100644 python/packages/openai/agent_framework_openai/py.typed create mode 100644 python/packages/openai/pyproject.toml create mode 100644 python/packages/openai/tests/assets/sample_image.jpg create mode 100644 python/packages/openai/tests/openai/conftest.py rename python/packages/{core => openai}/tests/openai/test_assistant_provider.py (98%) rename python/packages/{core => openai}/tests/openai/test_openai_assistants_client.py (96%) rename python/packages/{core/tests/openai/test_openai_responses_client.py => openai/tests/openai/test_openai_chat_client.py} (91%) rename python/packages/{core/tests/openai/test_openai_chat_client.py => openai/tests/openai/test_openai_chat_completion_client.py} (93%) rename python/packages/{core/tests/openai/test_openai_chat_client_base.py => openai/tests/openai/test_openai_chat_completion_client_base.py} (92%) rename python/packages/{core => openai}/tests/openai/test_openai_embedding_client.py (89%) delete mode 100644 python/samples/02-agents/observability/azure_ai_agent_observability.py create mode 100644 python/samples/02-agents/providers/azure/README.md create mode 100644 python/samples/02-agents/providers/azure/foundry_agent_basic.py create mode 100644 python/samples/02-agents/providers/azure/foundry_agent_custom_client.py create mode 100644 python/samples/02-agents/providers/azure/foundry_agent_hosted.py create mode 100644 python/samples/02-agents/providers/azure/foundry_agent_with_env_vars.py create mode 100644 python/samples/02-agents/providers/azure/foundry_agent_with_function_tools.py rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_with_foundry.py => azure/foundry_chat_client.py} (78%) rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_basic.py => azure/foundry_chat_client_basic.py} (83%) rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_code_interpreter_files.py => azure/foundry_chat_client_code_interpreter_files.py} (96%) rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_image_analysis.py => azure/foundry_chat_client_image_analysis.py} (81%) rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_with_code_interpreter.py => azure/foundry_chat_client_with_code_interpreter.py} (93%) rename python/samples/02-agents/providers/{azure_openai/azure_chat_client_with_explicit_settings.py => azure/foundry_chat_client_with_explicit_settings.py} (87%) rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_with_file_search.py => azure/foundry_chat_client_with_file_search.py} (85%) rename python/samples/02-agents/providers/{azure_openai/azure_chat_client_with_function_tools.py => azure/foundry_chat_client_with_function_tools.py} (94%) rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_with_hosted_mcp.py => azure/foundry_chat_client_with_hosted_mcp.py} (97%) rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_with_local_mcp.py => azure/foundry_chat_client_with_local_mcp.py} (90%) rename python/samples/02-agents/providers/{azure_openai/azure_chat_client_with_session.py => azure/foundry_chat_client_with_session.py} (94%) rename python/samples/02-agents/providers/{azure_openai/azure_chat_client_basic.py => azure/openai_chat_completion_client_azure_basic.py} (86%) rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_with_explicit_settings.py => azure/openai_chat_completion_client_azure_with_explicit_settings.py} (75%) rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_with_function_tools.py => azure/openai_chat_completion_client_azure_with_function_tools.py} (91%) rename python/samples/02-agents/providers/{azure_openai/azure_responses_client_with_session.py => azure/openai_chat_completion_client_azure_with_session.py} (56%) delete mode 100644 python/samples/02-agents/providers/azure_ai/README.md delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_basic.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_provider_methods.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_use_latest_version.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_agent_as_tool.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_agent_to_agent.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_application_endpoint.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_azure_ai_search.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_bing_custom_search.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_bing_grounding.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_browser_automation.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter_file_download.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter_file_generation.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_content_filtering.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_existing_agent.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_existing_conversation.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_explicit_settings.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_file_search.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_hosted_mcp.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_image_generation.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_local_mcp.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_memory_search.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_microsoft_fabric.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_openapi.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_reasoning.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_response_format.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_runtime_json_schema.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_session.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_sharepoint.py delete mode 100644 python/samples/02-agents/providers/azure_ai/azure_ai_with_web_search.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/README.md delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_basic.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_provider_methods.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_azure_ai_search.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_custom_search.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_grounding.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_grounding_citations.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_existing_agent.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_existing_session.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_explicit_settings.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_file_search.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_function_tools.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_hosted_mcp.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_local_mcp.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_multiple_tools.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_openapi_tools.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_response_format.py delete mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_session.py delete mode 100644 python/samples/02-agents/providers/azure_openai/README.md delete mode 100644 python/samples/02-agents/providers/azure_openai/azure_assistants_basic.py delete mode 100644 python/samples/02-agents/providers/azure_openai/azure_assistants_with_code_interpreter.py delete mode 100644 python/samples/02-agents/providers/azure_openai/azure_assistants_with_existing_assistant.py delete mode 100644 python/samples/02-agents/providers/azure_openai/azure_assistants_with_explicit_settings.py delete mode 100644 python/samples/02-agents/providers/azure_openai/azure_assistants_with_function_tools.py delete mode 100644 python/samples/02-agents/providers/azure_openai/azure_assistants_with_session.py delete mode 100644 python/samples/semantic-kernel-migration/azure_ai_agent/01_basic_azure_ai_agent.py delete mode 100644 python/samples/semantic-kernel-migration/azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py delete mode 100644 python/samples/semantic-kernel-migration/azure_ai_agent/03_azure_ai_agent_threads_and_followups.py diff --git a/docs/decisions/0020-provider-leading-clients.md b/docs/decisions/0020-provider-leading-clients.md new file mode 100644 index 0000000000..1dcc334209 --- /dev/null +++ b/docs/decisions/0020-provider-leading-clients.md @@ -0,0 +1,72 @@ +--- +status: accepted +contact: eavanvalkenburg +date: 2026-03-20 +deciders: eavanvalkenburg, sphenry, chetantoshnival +consulted: taochenosu, moonbox3, dmytrostruk, giles17, alliscode +--- + +# Provider-Leading Client Design & OpenAI Package Extraction + +## Context and Problem Statement + +The `agent-framework-core` package currently bundles OpenAI and Azure OpenAI client implementations along with their dependencies (`openai`, `azure-identity`, `azure-ai-projects`, `packaging`). This makes core heavier than necessary for users who don't use OpenAI, and it conflates the core abstractions with a specific provider implementation. Additionally, the current class naming (`OpenAIResponsesClient`, `OpenAIChatClient`) is based on the underlying OpenAI API names rather than what users actually want to do, making discoverability harder for newcomers. + +## Decision Drivers + +- **Lightweight core**: Core should only contain abstractions, middleware infrastructure, and telemetry — no provider-specific code or dependencies. +- **Discoverability-first**: Import namespaces should guide users to the right client. `from agent_framework.openai import ...` should surface all OpenAI-related clients; `from agent_framework.azure import ...` should surface Foundry, Azure AI, and other Azure-specific classes. +- **Provider-leading naming**: The primary client name should reflect the provider, not the underlying API. The Responses API is now the recommended default for OpenAI, so its client should be called `OpenAIChatClient` (not `OpenAIResponsesClient`). +- **Clean separation of concerns**: Azure-specific deprecated wrappers belong in the azure-ai package, not in the OpenAI package. + +## Considered Options + +- **Keep OpenAI in core**: Simpler but keeps core heavy; doesn't help discoverability. +- **Extract OpenAI with Azure wrappers in the OpenAI package**: Keeps Azure OpenAI wrappers alongside OpenAI code, but pollutes the OpenAI package with Azure concerns. +- **Extract OpenAI, place Azure wrappers in azure-ai**: Clean separation; the OpenAI package has zero Azure dependencies; deprecated Azure wrappers live in a single file in azure-ai for easy future deletion. + +## Decision Outcome + +Chosen option: "Extract OpenAI, place Azure wrappers in azure-ai", because it achieves the lightest core, cleanest OpenAI package, and the most maintainable deprecation path. + +Key changes: + +1. **New `agent-framework-openai` package** with dependencies on `agent-framework-core`, `openai`, and `packaging` only. +2. **Class renames**: `OpenAIResponsesClient` → `OpenAIChatClient` (Responses API), `OpenAIChatClient` → `OpenAIChatCompletionClient` (Chat Completions API). Old names remain as deprecated aliases. +3. **Deprecated classes**: `OpenAIAssistantsClient`, all `AzureOpenAI*Client` classes, `AzureAIClient`, `AzureAIAgentClient`, and `AzureAIProjectAgentProvider` are marked deprecated. +4. **New `FoundryChatClient`** in azure-ai for Azure AI Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`. +5. **All deprecated `AzureOpenAI*` classes** consolidated into a single file (`_deprecated_azure_openai.py`) in the azure-ai package for clean future deletion. +6. **Core's `agent_framework.openai` and `agent_framework.azure` namespaces** become lazy-loading gateways, preserving backward-compatible import paths while removing hard dependencies. +7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_RESPONSES_MODEL_ID` / `OPENAI_CHAT_MODEL_ID`). +8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Azure AI Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale. + +### Foundry Agent Design: `FoundryAgentClient` vs `FoundryAgent` + +The existing `AzureAIClient` combines two concerns: CRUD lifecycle management (creating/deleting agents on the service) and runtime communication (sending messages via the Responses API). The new design removes CRUD entirely — users connect to agents that already exist in Foundry. + +**Two approaches were considered:** + +**Option A — `FoundryAgentClient` only (public ChatClient):** +Users compose `Agent(client=FoundryAgentClient(...), tools=[...])`. This follows the universal `Agent(client=X)` pattern used by every other provider. However, a "client" that wraps a named remote agent (with `agent_name` as a constructor param) is semantically odd — clients typically wrap a model endpoint, not a specific agent. + +**Option B — `FoundryAgent` (Agent subclass) + private `_FoundryAgentChatClient` and public `RawFoundryAgentChatClient`:** +Users write `FoundryAgent(agent_name="my-agent", ...)` for the common case. Internally, `FoundryAgent` creates a `_FoundryAgentChatClient` and passes it to the standard `Agent` base class. For advanced customization, users pass `client_type=RawFoundryAgentChatClient` (or a custom subclass) to control the client middleware layers. The `Agent(client=RawFoundryAgentChatClient(...))` composition pattern still works for users who prefer it. + +**Chosen option: Option B**, because: +- The common case (`FoundryAgent(...)`) is a single object with no boilerplate. +- `client_type=` gives full control over client middleware without parameter duplication — the agent forwards connection params to the client internally. +- `RawFoundryAgent(RawAgent)` and `FoundryAgent(Agent)` mirror the established `RawAgent`/`Agent` pattern. +- Runtime validation (only `FunctionTool` allowed) lives in `RawFoundryAgentChatClient._prepare_options`, ensuring it applies regardless of how the client is used — through `FoundryAgent`, `Agent(client=...)`, or any custom composition. + +**Public classes:** +- `RawFoundryAgentChatClient(RawOpenAIChatClient)` — Responses API client that injects agent reference and validates tools. Extension point for custom client middleware. +- `RawFoundryAgent(RawAgent)` — Agent without agent-level middleware/telemetry. +- `FoundryAgent(AgentTelemetryLayer, AgentMiddlewareLayer, RawFoundryAgent)` — Recommended production agent. + +**Internal (private):** +- `_FoundryAgentChatClient` — Full client with function invocation, chat middleware, and telemetry layers. Created automatically by `FoundryAgent`; users customize via `client_type=RawFoundryAgentChatClient` or a custom subclass. + +**Deprecated:** +- `AzureAIClient` — replaced by `FoundryAgent` (which uses `FoundryAgentClient` internally). +- `AzureAIAgentClient` — refers to V1 Agents Service API, no direct replacement. +- `AzureAIProjectAgentProvider` — replaced by `FoundryAgent`. diff --git a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py index b2eb41e03f..0338b13416 100644 --- a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py +++ b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py @@ -16,8 +16,7 @@ from agent_framework import AGENT_FRAMEWORK_USER_AGENT, Annotation, Content, Message, SupportsGetEmbeddings from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext from agent_framework._settings import SecretString, load_settings -from agent_framework.azure._entra_id_authentication import AzureCredentialTypes -from azure.core.credentials import AzureKeyCredential +from azure.core.credentials import AzureKeyCredential, TokenCredential from azure.core.credentials_async import AsyncTokenCredential from azure.core.exceptions import ResourceNotFoundError from azure.search.documents.aio import SearchClient @@ -111,6 +110,8 @@ except ImportError: _agentic_retrieval_available = False +AzureCredentialTypes = TokenCredential | AsyncTokenCredential + logger = logging.getLogger("agent_framework.azure_ai_search") _DEFAULT_AGENTIC_MESSAGE_HISTORY_COUNT = 10 diff --git a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py index 46b1ed5b3b..5eb19edc63 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py @@ -5,12 +5,28 @@ from ._agent_provider import AzureAIAgentsProvider from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions from ._client import AzureAIClient, AzureAIProjectAgentOptions, RawAzureAIClient +from ._deprecated_azure_openai import ( + AzureOpenAIAssistantsClient, # pyright: ignore[reportDeprecated] + AzureOpenAIAssistantsOptions, + AzureOpenAIChatClient, # pyright: ignore[reportDeprecated] + AzureOpenAIChatOptions, + AzureOpenAIConfigMixin, + AzureOpenAIEmbeddingClient, # pyright: ignore[reportDeprecated] + AzureOpenAIResponsesClient, # pyright: ignore[reportDeprecated] + AzureOpenAIResponsesOptions, + AzureOpenAISettings, + AzureUserSecurityContext, +) from ._embedding_client import ( AzureAIInferenceEmbeddingClient, AzureAIInferenceEmbeddingOptions, AzureAIInferenceEmbeddingSettings, RawAzureAIInferenceEmbeddingClient, ) +from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider +from ._foundry_agent import FoundryAgent, RawFoundryAgent +from ._foundry_agent_client import RawFoundryAgentChatClient +from ._foundry_chat_client import FoundryChatClient, RawFoundryChatClient from ._foundry_memory_provider import FoundryMemoryProvider from ._project_provider import AzureAIProjectAgentProvider from ._shared import AzureAISettings @@ -18,7 +34,7 @@ try: __version__ = importlib.metadata.version(__name__) except importlib.metadata.PackageNotFoundError: - __version__ = "0.0.0" # Fallback for development mode + __version__ = "0.0.0" __all__ = [ "AzureAIAgentClient", @@ -31,8 +47,25 @@ "AzureAIProjectAgentOptions", "AzureAIProjectAgentProvider", "AzureAISettings", + "AzureCredentialTypes", + "AzureOpenAIAssistantsClient", + "AzureOpenAIAssistantsOptions", + "AzureOpenAIChatClient", + "AzureOpenAIChatOptions", + "AzureOpenAIConfigMixin", + "AzureOpenAIEmbeddingClient", + "AzureOpenAIResponsesClient", + "AzureOpenAIResponsesOptions", + "AzureOpenAISettings", + "AzureTokenProvider", + "AzureUserSecurityContext", + "FoundryAgent", + "FoundryChatClient", "FoundryMemoryProvider", "RawAzureAIClient", "RawAzureAIInferenceEmbeddingClient", + "RawFoundryAgent", + "RawFoundryAgentChatClient", + "RawFoundryChatClient", "__version__", ] diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py index 9b2a72cd2f..043819c6a6 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py @@ -18,13 +18,13 @@ from agent_framework._mcp import MCPTool from agent_framework._settings import load_settings from agent_framework._tools import ToolTypes -from agent_framework.azure._entra_id_authentication import AzureCredentialTypes from azure.ai.agents.aio import AgentsClient from azure.ai.agents.models import Agent as AzureAgent from azure.ai.agents.models import ResponseFormatJsonSchema, ResponseFormatJsonSchemaType from pydantic import BaseModel from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions +from ._entra_id_authentication import AzureCredentialTypes from ._shared import AzureAISettings, to_azure_ai_agent_tools if sys.version_info >= (3, 13): diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index 818338a861..d533a46f11 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -36,7 +36,6 @@ ) from agent_framework._settings import load_settings from agent_framework._tools import ToolTypes -from agent_framework.azure._entra_id_authentication import AzureCredentialTypes from agent_framework.exceptions import ( ChatClientException, ChatClientInvalidRequestException, @@ -92,6 +91,7 @@ ) from pydantic import BaseModel +from ._entra_id_authentication import AzureCredentialTypes from ._shared import AzureAISettings, resolve_file_ids, to_azure_ai_agent_tools if sys.version_info >= (3, 13): diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 34ac6f29a5..5c217caee9 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -30,10 +30,9 @@ ) from agent_framework._settings import load_settings from agent_framework._tools import ToolTypes -from agent_framework.azure._entra_id_authentication import AzureCredentialTypes from agent_framework.observability import ChatTelemetryLayer from agent_framework.openai import OpenAIResponsesOptions -from agent_framework.openai._responses_client import RawOpenAIResponsesClient +from agent_framework_openai._chat_client import RawOpenAIChatClient from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( ApproximateLocation, @@ -50,6 +49,7 @@ from azure.ai.projects.models import FileSearchTool as ProjectsFileSearchTool from azure.core.exceptions import ResourceNotFoundError +from ._entra_id_authentication import AzureCredentialTypes from ._shared import AzureAISettings, create_text_format_config, resolve_file_ids if sys.version_info >= (3, 13): @@ -68,7 +68,7 @@ logger = logging.getLogger("agent_framework.azure") -class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False): +class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False): # type: ignore[misc] """Azure AI Project Agent options.""" rai_config: RaiConfig @@ -88,7 +88,7 @@ class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False): _DOC_INDEX_PATTERN = re.compile(r"doc_(\d+)") -class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[AzureAIClientOptionsT]): +class RawAzureAIClient(RawOpenAIChatClient[AzureAIClientOptionsT], Generic[AzureAIClientOptionsT]): """Raw Azure AI client without middleware, telemetry, or function invocation layers. Warning: @@ -215,8 +215,10 @@ class MyOptions(ChatOptions, total=False): project_client = AIProjectClient(**project_client_kwargs) should_close_client = True - # Initialize parent + # Initialize parent with OpenAI client from project super().__init__( + async_client=project_client.get_openai_client(), + model=azure_ai_settings.get("model"), additional_properties=additional_properties, ) @@ -680,10 +682,6 @@ def _prepare_messages_for_azure_ai(self, messages: Sequence[Message]) -> tuple[l return result, instructions - async def _initialize_client(self) -> None: - """Initialize OpenAI client.""" - self.client = self.project_client.get_openai_client() # type: ignore - def _update_agent_name_and_description(self, agent_name: str | None, description: str | None = None) -> None: """Update the agent name in the chat client. diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_deprecated_azure_openai.py b/python/packages/azure-ai/agent_framework_azure_ai/_deprecated_azure_openai.py new file mode 100644 index 0000000000..3e437cca6f --- /dev/null +++ b/python/packages/azure-ai/agent_framework_azure_ai/_deprecated_azure_openai.py @@ -0,0 +1,895 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Deprecated Azure OpenAI client classes. + +All classes in this module are deprecated and will be removed in a future release. +Migrate to the ``agent_framework_openai`` package equivalents with an ``AsyncAzureOpenAI`` client, +or use ``FoundryChatClient`` for Azure AI Foundry projects. +""" + +from __future__ import annotations + +import json +import logging +import sys +from collections.abc import Mapping, Sequence +from copy import copy +from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, cast +from urllib.parse import urljoin, urlparse + +from agent_framework._middleware import ChatMiddlewareLayer +from agent_framework._settings import SecretString, load_settings +from agent_framework._telemetry import AGENT_FRAMEWORK_USER_AGENT, APP_INFO, prepend_agent_framework_to_user_agent +from agent_framework._tools import FunctionInvocationConfiguration, FunctionInvocationLayer +from agent_framework._types import Annotation, Content +from agent_framework.observability import ChatTelemetryLayer, EmbeddingTelemetryLayer +from agent_framework_openai._assistants_client import OpenAIAssistantsClient, OpenAIAssistantsOptions +from agent_framework_openai._chat_client import OpenAIChatOptions, RawOpenAIChatClient +from agent_framework_openai._chat_completion_client import OpenAIChatCompletionOptions, RawOpenAIChatCompletionClient +from agent_framework_openai._embedding_client import OpenAIEmbeddingOptions, RawOpenAIEmbeddingClient +from agent_framework_openai._shared import OpenAIBase +from azure.ai.projects.aio import AIProjectClient +from openai import AsyncOpenAI +from openai.lib.azure import AsyncAzureOpenAI +from pydantic import BaseModel + +from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider, resolve_credential_to_token_provider + +if sys.version_info >= (3, 13): + from typing import TypeVar # type: ignore # pragma: no cover + from warnings import deprecated # type: ignore # pragma: no cover +else: + from typing_extensions import TypeVar, deprecated # type: ignore # pragma: no cover +if sys.version_info >= (3, 12): + from typing import override # type: ignore # pragma: no cover +else: + from typing_extensions import override # type: ignore # pragma: no cover +if sys.version_info >= (3, 11): + from typing import TypedDict # type: ignore # pragma: no cover +else: + from typing_extensions import TypedDict # type: ignore # pragma: no cover + +if TYPE_CHECKING: + from agent_framework._middleware import MiddlewareTypes + from openai.types.chat.chat_completion import Choice + from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice + +logger: logging.Logger = logging.getLogger(__name__) + + +# region Constants and Settings + +DEFAULT_AZURE_API_VERSION: Final[str] = "2024-10-21" +DEFAULT_AZURE_TOKEN_ENDPOINT: Final[str] = "https://cognitiveservices.azure.com/.default" # noqa: S105 + + +class AzureOpenAISettings(TypedDict, total=False): + """AzureOpenAI model settings. + + Settings are resolved in this order: explicit keyword arguments, values from an + explicitly provided .env file, then environment variables with the prefix + 'AZURE_OPENAI_'. If settings are missing after resolution, validation will fail. + + Keyword Args: + endpoint: The endpoint of the Azure deployment. + chat_deployment_name: The name of the Azure Chat deployment. + responses_deployment_name: The name of the Azure Responses deployment. + embedding_deployment_name: The name of the Azure Embedding deployment. + api_key: The API key for the Azure deployment. + api_version: The API version to use. + base_url: The url of the Azure deployment. + token_endpoint: The token endpoint to use to retrieve the authentication token. + """ + + chat_deployment_name: str | None + responses_deployment_name: str | None + embedding_deployment_name: str | None + endpoint: str | None + base_url: str | None + api_key: SecretString | None + api_version: str | None + token_endpoint: str | None + + +def _apply_azure_defaults( + settings: AzureOpenAISettings, + default_api_version: str = DEFAULT_AZURE_API_VERSION, + default_token_endpoint: str = DEFAULT_AZURE_TOKEN_ENDPOINT, +) -> None: + """Apply default values for api_version and token_endpoint after loading settings. + + Args: + settings: The loaded Azure OpenAI settings dict. + default_api_version: The default API version to use if not set. + default_token_endpoint: The default token endpoint to use if not set. + """ + if not settings.get("api_version"): + settings["api_version"] = default_api_version + if not settings.get("token_endpoint"): + settings["token_endpoint"] = default_token_endpoint + + +# endregion + + +# region AzureOpenAIConfigMixin + + +class AzureOpenAIConfigMixin(OpenAIBase): + """Internal class for configuring a connection to an Azure OpenAI service.""" + + OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.openai" + + def __init__( + self, + deployment_name: str, + endpoint: str | None = None, + base_url: str | None = None, + api_version: str = DEFAULT_AZURE_API_VERSION, + api_key: str | None = None, + token_endpoint: str | None = None, + credential: AzureCredentialTypes | AzureTokenProvider | None = None, + default_headers: Mapping[str, str] | None = None, + client: AsyncOpenAI | None = None, + instruction_role: str | None = None, + **kwargs: Any, + ) -> None: + """Configure a connection to an Azure OpenAI service. + + Args: + deployment_name: Name of the deployment. + endpoint: The specific endpoint URL for the deployment. + base_url: The base URL for Azure services. + api_version: Azure API version. + api_key: API key for Azure services. + token_endpoint: Azure AD token scope. + credential: Azure credential or token provider for authentication. + default_headers: Default headers for HTTP requests. + client: An existing client to use. + instruction_role: The role to use for 'instruction' messages. + kwargs: Additional keyword arguments. + """ + merged_headers = dict(copy(default_headers)) if default_headers else {} + if APP_INFO: + merged_headers.update(APP_INFO) + merged_headers = prepend_agent_framework_to_user_agent(merged_headers) + if not client: + ad_token_provider = None + if not api_key and credential: + ad_token_provider = resolve_credential_to_token_provider(credential, token_endpoint) + + if not api_key and not ad_token_provider: + raise ValueError("Please provide either api_key, credential, or a client.") + + if not endpoint and not base_url: + raise ValueError("Please provide an endpoint or a base_url") + + args: dict[str, Any] = { + "default_headers": merged_headers, + } + if api_version: + args["api_version"] = api_version + if ad_token_provider: + args["azure_ad_token_provider"] = ad_token_provider + if api_key: + args["api_key"] = api_key + if base_url: + args["base_url"] = str(base_url) + if endpoint and not base_url: + args["azure_endpoint"] = str(endpoint) + if deployment_name: + args["azure_deployment"] = deployment_name + if "websocket_base_url" in kwargs: + args["websocket_base_url"] = kwargs.pop("websocket_base_url") + + client = AsyncAzureOpenAI(**args) + + self.endpoint = str(endpoint) + self.base_url = str(base_url) + self.api_version = api_version + self.deployment_name = deployment_name + self.instruction_role = instruction_role + if default_headers: + from agent_framework._telemetry import USER_AGENT_KEY + + def_headers = {k: v for k, v in default_headers.items() if k != USER_AGENT_KEY} + else: + def_headers = None + self.default_headers = def_headers + + super().__init__(model_id=deployment_name, client=client, **kwargs) + + +# endregion + + +# region AzureOpenAIResponsesClient + + +AzureOpenAIResponsesOptionsT = TypeVar( + "AzureOpenAIResponsesOptionsT", + bound=TypedDict, # type: ignore[valid-type] + default="OpenAIChatOptions", + covariant=True, +) + +AzureOpenAIResponsesOptions = OpenAIChatOptions + + +@deprecated( + "AzureOpenAIResponsesClient is deprecated. " + "Use OpenAIChatClient with an AsyncAzureOpenAI client, or FoundryChatClient for Foundry projects." +) +class AzureOpenAIResponsesClient( # type: ignore[misc] + FunctionInvocationLayer[AzureOpenAIResponsesOptionsT], + ChatMiddlewareLayer[AzureOpenAIResponsesOptionsT], + ChatTelemetryLayer[AzureOpenAIResponsesOptionsT], + RawOpenAIChatClient[AzureOpenAIResponsesOptionsT], + Generic[AzureOpenAIResponsesOptionsT], +): + """Deprecated Azure Responses client. Use OpenAIChatClient with an AsyncAzureOpenAI client instead.""" + + OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.openai" + + def __init__( + self, + *, + api_key: str | None = None, + deployment_name: str | None = None, + endpoint: str | None = None, + base_url: str | None = None, + api_version: str | None = None, + token_endpoint: str | None = None, + credential: AzureCredentialTypes | AzureTokenProvider | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncOpenAI | None = None, + project_client: Any | None = None, + project_endpoint: str | None = None, + allow_preview: bool | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + instruction_role: str | None = None, + middleware: Sequence[MiddlewareTypes] | None = None, + function_invocation_configuration: FunctionInvocationConfiguration | None = None, + **kwargs: Any, + ) -> None: + """Initialize an Azure OpenAI Responses client. + + Keyword Args: + api_key: The API key. + deployment_name: The deployment name. + endpoint: The deployment endpoint. + base_url: The deployment base URL. + api_version: The deployment API version. + token_endpoint: The token endpoint to request an Azure token. + credential: Azure credential or token provider for authentication. + default_headers: Default headers for HTTP requests. + async_client: An existing client to use. + project_client: An existing AIProjectClient to use. + project_endpoint: The Azure AI Foundry project endpoint URL. + allow_preview: Enables preview opt-in on internally-created AIProjectClient. + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + instruction_role: The role to use for 'instruction' messages. + middleware: Optional sequence of middleware. + function_invocation_configuration: Optional function invocation configuration. + kwargs: Additional keyword arguments. + """ + if (model_id := kwargs.pop("model_id", None)) and not deployment_name: + deployment_name = str(model_id) + + if async_client is None and (project_client is not None or project_endpoint is not None): + async_client = self._create_client_from_project( + project_client=project_client, + project_endpoint=project_endpoint, + credential=credential, + allow_preview=allow_preview, + ) + + azure_openai_settings = load_settings( + AzureOpenAISettings, + env_prefix="AZURE_OPENAI_", + api_key=api_key, + base_url=base_url, + endpoint=endpoint, + responses_deployment_name=deployment_name, + api_version=api_version, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + token_endpoint=token_endpoint, + ) + _apply_azure_defaults(azure_openai_settings, default_api_version="preview") + endpoint_value = azure_openai_settings.get("endpoint") + if ( + not azure_openai_settings.get("base_url") + and endpoint_value + and (hostname := urlparse(str(endpoint_value)).hostname) + and hostname.endswith(".openai.azure.com") + ): + azure_openai_settings["base_url"] = urljoin(str(endpoint_value), "/openai/v1/") + + responses_deployment_name = azure_openai_settings.get("responses_deployment_name") + if not responses_deployment_name: + raise ValueError( + "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " + "or 'AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME' environment variable." + ) + + if not async_client: + # Create the Azure OpenAI client directly + merged_headers = dict(copy(default_headers)) if default_headers else {} + if APP_INFO: + merged_headers.update(APP_INFO) + merged_headers = prepend_agent_framework_to_user_agent(merged_headers) + + api_key_secret = azure_openai_settings.get("api_key") + ad_token_provider = None + if not api_key_secret and credential: + ad_token_provider = resolve_credential_to_token_provider( + credential, azure_openai_settings.get("token_endpoint") + ) + + if not api_key_secret and not ad_token_provider: + raise ValueError("Please provide either api_key, credential, or a client.") + + client_endpoint = azure_openai_settings.get("endpoint") + client_base_url = azure_openai_settings.get("base_url") + if not client_endpoint and not client_base_url: + raise ValueError("Please provide an endpoint or a base_url") + + client_args: dict[str, Any] = {"default_headers": merged_headers} + if resolved_api_version := azure_openai_settings.get("api_version"): + client_args["api_version"] = resolved_api_version + if ad_token_provider: + client_args["azure_ad_token_provider"] = ad_token_provider + if api_key_secret: + client_args["api_key"] = api_key_secret.get_secret_value() + if client_base_url: + client_args["base_url"] = str(client_base_url) + if client_endpoint and not client_base_url: + client_args["azure_endpoint"] = str(client_endpoint) + if responses_deployment_name: + client_args["azure_deployment"] = responses_deployment_name + if "websocket_base_url" in kwargs: + client_args["websocket_base_url"] = kwargs.pop("websocket_base_url") + + async_client = AsyncAzureOpenAI(**client_args) + + # Store Azure-specific attributes for serialization + self.endpoint = str(endpoint_value) if endpoint_value else None + self.api_version = azure_openai_settings.get("api_version") or "" + self.deployment_name = responses_deployment_name + + super().__init__( + async_client=async_client, + model=responses_deployment_name, + instruction_role=instruction_role, + default_headers=default_headers, + middleware=middleware, # type: ignore[arg-type] + function_invocation_configuration=function_invocation_configuration, + **kwargs, + ) + + @staticmethod + def _create_client_from_project( + *, + project_client: AIProjectClient | None, + project_endpoint: str | None, + credential: AzureCredentialTypes | AzureTokenProvider | None, + allow_preview: bool | None = None, + ) -> AsyncOpenAI: + """Create an AsyncOpenAI client from an Azure AI Foundry project.""" + if project_client is not None: + return project_client.get_openai_client() + + if not project_endpoint: + raise ValueError("Azure AI project endpoint is required when project_client is not provided.") + if not credential: + raise ValueError("Azure credential is required when using project_endpoint without a project_client.") + project_client_kwargs: dict[str, Any] = { + "endpoint": project_endpoint, + "credential": credential, # type: ignore[arg-type] + "user_agent": AGENT_FRAMEWORK_USER_AGENT, + } + if allow_preview is not None: + project_client_kwargs["allow_preview"] = allow_preview + project_client = AIProjectClient(**project_client_kwargs) + return project_client.get_openai_client() + + @override + def _check_model_presence(self, options: dict[str, Any]) -> None: + if not options.get("model"): + if not self.model: + raise ValueError("deployment_name must be a non-empty string") + options["model"] = self.model + + +# endregion + + +# region AzureOpenAIChatClient + + +ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None) + + +class AzureUserSecurityContext(TypedDict, total=False): + """User security context for Azure AI applications. + + These fields help security operations teams investigate and mitigate security + incidents by providing context about the application and end user. + """ + + application_name: str + """Name of the application making the request.""" + + end_user_id: str + """Unique identifier for the end user (recommend hashing username/email).""" + + end_user_tenant_id: str + """Microsoft 365 tenant ID the end user belongs to. Required for multi-tenant apps.""" + + source_ip: str + """The original client's IP address.""" + + +class AzureOpenAIChatOptions(OpenAIChatCompletionOptions[ResponseModelT], Generic[ResponseModelT], total=False): + """Azure OpenAI-specific chat options dict. + + Extends OpenAIChatCompletionOptions with Azure-specific options including + the "On Your Data" feature and enhanced security context. + """ + + data_sources: list[dict[str, Any]] + """Azure "On Your Data" data sources for retrieval-augmented generation.""" + + user_security_context: AzureUserSecurityContext + """Enhanced security context for Azure Defender integration.""" + + n: int + """Number of chat completion choices to generate for each input message.""" + + +AzureOpenAIChatOptionsT = TypeVar( + "AzureOpenAIChatOptionsT", + bound=TypedDict, # type: ignore[valid-type] + default="AzureOpenAIChatOptions", + covariant=True, +) + + +@deprecated("AzureOpenAIChatClient is deprecated. Use OpenAIChatCompletionClient with an AsyncAzureOpenAI client.") +class AzureOpenAIChatClient( # type: ignore[misc] + FunctionInvocationLayer[AzureOpenAIChatOptionsT], + ChatMiddlewareLayer[AzureOpenAIChatOptionsT], + ChatTelemetryLayer[AzureOpenAIChatOptionsT], + RawOpenAIChatCompletionClient[AzureOpenAIChatOptionsT], + Generic[AzureOpenAIChatOptionsT], +): + """Deprecated Azure OpenAI Chat client. Use OpenAIChatCompletionClient with AsyncAzureOpenAI instead.""" + + OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.openai" + + def __init__( + self, + *, + api_key: str | None = None, + deployment_name: str | None = None, + endpoint: str | None = None, + base_url: str | None = None, + api_version: str | None = None, + token_endpoint: str | None = None, + credential: AzureCredentialTypes | AzureTokenProvider | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncAzureOpenAI | None = None, + additional_properties: dict[str, Any] | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + instruction_role: str | None = None, + middleware: Sequence[MiddlewareTypes] | None = None, + function_invocation_configuration: FunctionInvocationConfiguration | None = None, + ) -> None: + """Initialize an Azure OpenAI Chat completion client. + + Keyword Args: + api_key: The API key. + deployment_name: The deployment name. + endpoint: The deployment endpoint. + base_url: The deployment base URL. + api_version: The deployment API version. + token_endpoint: The token endpoint to request an Azure token. + credential: Azure credential or token provider for authentication. + default_headers: Default headers for HTTP requests. + async_client: An existing client to use. + additional_properties: Additional properties stored on the client instance. + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + instruction_role: The role to use for 'instruction' messages. + middleware: Optional sequence of middleware. + function_invocation_configuration: Optional function invocation configuration. + """ + azure_openai_settings = load_settings( + AzureOpenAISettings, + env_prefix="AZURE_OPENAI_", + api_key=api_key, + base_url=base_url, + endpoint=endpoint, + chat_deployment_name=deployment_name, + api_version=api_version, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + token_endpoint=token_endpoint, + ) + _apply_azure_defaults(azure_openai_settings) + + chat_deployment_name = azure_openai_settings.get("chat_deployment_name") + if not chat_deployment_name: + raise ValueError( + "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " + "or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable." + ) + + if not async_client: + # Create the Azure OpenAI client directly + merged_headers = dict(copy(default_headers)) if default_headers else {} + if APP_INFO: + merged_headers.update(APP_INFO) + merged_headers = prepend_agent_framework_to_user_agent(merged_headers) + + api_key_secret = azure_openai_settings.get("api_key") + ad_token_provider = None + if not api_key_secret and credential: + ad_token_provider = resolve_credential_to_token_provider( + credential, azure_openai_settings.get("token_endpoint") + ) + + if not api_key_secret and not ad_token_provider: + raise ValueError("Please provide either api_key, credential, or a client.") + + endpoint_value = azure_openai_settings.get("endpoint") + base_url_value = azure_openai_settings.get("base_url") + if not endpoint_value and not base_url_value: + raise ValueError("Please provide an endpoint or a base_url") + + client_args: dict[str, Any] = {"default_headers": merged_headers} + if resolved_api_version := azure_openai_settings.get("api_version"): + client_args["api_version"] = resolved_api_version + if ad_token_provider: + client_args["azure_ad_token_provider"] = ad_token_provider + if api_key_secret: + client_args["api_key"] = api_key_secret.get_secret_value() + if base_url_value: + client_args["base_url"] = str(base_url_value) + if endpoint_value and not base_url_value: + client_args["azure_endpoint"] = str(endpoint_value) + if chat_deployment_name: + client_args["azure_deployment"] = chat_deployment_name + + async_client = AsyncAzureOpenAI(**client_args) + + # Store Azure-specific attributes for serialization + self.endpoint = str(azure_openai_settings.get("endpoint") or "") + self.api_version = azure_openai_settings.get("api_version") or "" + self.deployment_name = chat_deployment_name + + super().__init__( + async_client=async_client, + model=chat_deployment_name, + instruction_role=instruction_role, + default_headers=default_headers, + additional_properties=additional_properties, + middleware=middleware, # type: ignore[arg-type] + function_invocation_configuration=function_invocation_configuration, + ) + + @override + def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> Content | None: + """Parse the choice into a Content object with type='text'. + + Overwritten from RawOpenAIChatCompletionClient to deal with Azure On Your Data function. + """ + message = getattr(choice, "message", None) + if message is None: + message = getattr(choice, "delta", None) + if message is None: # type: ignore + return None + if hasattr(message, "refusal") and message.refusal: + return Content.from_text(text=message.refusal, raw_representation=choice) + if not message.content: + return None + text_content = Content.from_text(text=message.content, raw_representation=choice) + if not message.model_extra or "context" not in message.model_extra: + return text_content + + context_raw: object = cast(object, message.context) # type: ignore[union-attr] + if isinstance(context_raw, str): + try: + context_raw = json.loads(context_raw) + except json.JSONDecodeError: + logger.warning("Context is not a valid JSON string, ignoring context.") + return text_content + if not isinstance(context_raw, dict): + logger.warning("Context is not a valid dictionary, ignoring context.") + return text_content + context = cast(dict[str, Any], context_raw) + if intent := context.get("intent"): + text_content.additional_properties = {"intent": intent} + citations = context.get("citations") + if isinstance(citations, list) and citations: + annotations: list[Annotation] = [] + for citation_raw in cast(list[object], citations): + if not isinstance(citation_raw, dict): + continue + citation = cast(dict[str, Any], citation_raw) + annotations.append( + Annotation( + type="citation", + title=citation.get("title", ""), + url=citation.get("url", ""), + snippet=citation.get("content", ""), + file_id=citation.get("filepath", ""), + tool_name="Azure-on-your-Data", + additional_properties={"chunk_id": citation.get("chunk_id", "")}, + raw_representation=citation, + ) + ) + text_content.annotations = annotations + return text_content + + +# endregion + + +# region AzureOpenAIAssistantsClient + + +AzureOpenAIAssistantsOptionsT = TypeVar( + "AzureOpenAIAssistantsOptionsT", + bound=TypedDict, # type: ignore[valid-type] + default="OpenAIAssistantsOptions", + covariant=True, +) + +AzureOpenAIAssistantsOptions = OpenAIAssistantsOptions + + +@deprecated( + "AzureOpenAIAssistantsClient is deprecated. " + "Use OpenAIAssistantsClient (also deprecated) or migrate to OpenAIChatClient." +) +class AzureOpenAIAssistantsClient( + OpenAIAssistantsClient[AzureOpenAIAssistantsOptionsT], Generic[AzureOpenAIAssistantsOptionsT] +): + """Deprecated Azure OpenAI Assistants client. Use OpenAIAssistantsClient or migrate to OpenAIChatClient.""" + + DEFAULT_AZURE_API_VERSION: ClassVar[str] = "2024-05-01-preview" + + def __init__( + self, + *, + deployment_name: str | None = None, + assistant_id: str | None = None, + assistant_name: str | None = None, + assistant_description: str | None = None, + thread_id: str | None = None, + api_key: str | None = None, + endpoint: str | None = None, + base_url: str | None = None, + api_version: str | None = None, + token_endpoint: str | None = None, + credential: AzureCredentialTypes | AzureTokenProvider | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncAzureOpenAI | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize an Azure OpenAI Assistants client. + + Keyword Args: + deployment_name: The Azure OpenAI deployment name. + assistant_id: The ID of an Azure OpenAI assistant to use. + assistant_name: The name to use when creating new assistants. + assistant_description: The description to use when creating new assistants. + thread_id: Default thread ID to use for conversations. + api_key: The API key to use. + endpoint: The deployment endpoint. + base_url: The deployment base URL. + api_version: The deployment API version. + token_endpoint: The token endpoint to request an Azure token. + credential: Azure credential or token provider for authentication. + default_headers: Default headers for HTTP requests. + async_client: An existing client to use. + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + """ + azure_openai_settings = load_settings( + AzureOpenAISettings, + env_prefix="AZURE_OPENAI_", + api_key=api_key, + base_url=base_url, + endpoint=endpoint, + chat_deployment_name=deployment_name, + api_version=api_version, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + token_endpoint=token_endpoint, + ) + _apply_azure_defaults(azure_openai_settings, default_api_version=self.DEFAULT_AZURE_API_VERSION) + + chat_deployment_name = azure_openai_settings.get("chat_deployment_name") + if not chat_deployment_name: + raise ValueError( + "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " + "or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable." + ) + + api_key_secret = azure_openai_settings.get("api_key") + token_scope = azure_openai_settings.get("token_endpoint") + + ad_token_provider = None + if not async_client and not api_key_secret and credential: + ad_token_provider = resolve_credential_to_token_provider(credential, token_scope) + + if not async_client and not api_key_secret and not ad_token_provider: + raise ValueError("Please provide either api_key, credential, or a client.") + + if not async_client: + client_params: dict[str, Any] = { + "default_headers": default_headers, + } + if resolved_api_version := azure_openai_settings.get("api_version"): + client_params["api_version"] = resolved_api_version + + if api_key_secret: + client_params["api_key"] = api_key_secret.get_secret_value() + elif ad_token_provider: + client_params["azure_ad_token_provider"] = ad_token_provider + + if resolved_base_url := azure_openai_settings.get("base_url"): + client_params["base_url"] = str(resolved_base_url) + elif resolved_endpoint := azure_openai_settings.get("endpoint"): + client_params["azure_endpoint"] = str(resolved_endpoint) + + async_client = AsyncAzureOpenAI(**client_params) + + super().__init__( + model_id=chat_deployment_name, + assistant_id=assistant_id, + assistant_name=assistant_name, + assistant_description=assistant_description, + thread_id=thread_id, + async_client=async_client, # type: ignore[reportArgumentType] + default_headers=default_headers, + ) + + +# endregion + + +# region AzureOpenAIEmbeddingClient + + +AzureOpenAIEmbeddingOptionsT = TypeVar( + "AzureOpenAIEmbeddingOptionsT", + bound=TypedDict, # type: ignore[valid-type] + default="OpenAIEmbeddingOptions", + covariant=True, +) + + +@deprecated("AzureOpenAIEmbeddingClient is deprecated. Use OpenAIEmbeddingClient with an AsyncAzureOpenAI client.") +class AzureOpenAIEmbeddingClient( + EmbeddingTelemetryLayer[str, list[float], AzureOpenAIEmbeddingOptionsT], + RawOpenAIEmbeddingClient[AzureOpenAIEmbeddingOptionsT], + Generic[AzureOpenAIEmbeddingOptionsT], +): + """Deprecated Azure OpenAI embedding client. Use OpenAIEmbeddingClient with AsyncAzureOpenAI instead.""" + + OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.openai" + + def __init__( + self, + *, + api_key: str | None = None, + deployment_name: str | None = None, + endpoint: str | None = None, + base_url: str | None = None, + api_version: str | None = None, + token_endpoint: str | None = None, + credential: AzureCredentialTypes | AzureTokenProvider | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncAzureOpenAI | None = None, + otel_provider_name: str | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize an Azure OpenAI embedding client. + + Keyword Args: + api_key: The API key. + deployment_name: The deployment name. + endpoint: The deployment endpoint. + base_url: The deployment base URL. + api_version: The deployment API version. + token_endpoint: The token endpoint to request an Azure token. + credential: Azure credential or token provider for authentication. + default_headers: Default headers for HTTP requests. + async_client: An existing client to use. + otel_provider_name: Override the OpenTelemetry provider name. + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + """ + azure_openai_settings = load_settings( + AzureOpenAISettings, + env_prefix="AZURE_OPENAI_", + api_key=api_key, + base_url=base_url, + endpoint=endpoint, + embedding_deployment_name=deployment_name, + api_version=api_version, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + token_endpoint=token_endpoint, + ) + _apply_azure_defaults(azure_openai_settings) + + embedding_deployment_name = azure_openai_settings.get("embedding_deployment_name") + if not embedding_deployment_name: + raise ValueError( + "Azure OpenAI embedding deployment name is required. Set via 'deployment_name' parameter " + "or 'AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME' environment variable." + ) + + if not async_client: + # Create the Azure OpenAI client directly + merged_headers = dict(copy(default_headers)) if default_headers else {} + if APP_INFO: + merged_headers.update(APP_INFO) + merged_headers = prepend_agent_framework_to_user_agent(merged_headers) + + api_key_secret = azure_openai_settings.get("api_key") + ad_token_provider = None + if not api_key_secret and credential: + ad_token_provider = resolve_credential_to_token_provider( + credential, azure_openai_settings.get("token_endpoint") + ) + + if not api_key_secret and not ad_token_provider: + raise ValueError("Please provide either api_key, credential, or a client.") + + endpoint_value = azure_openai_settings.get("endpoint") + base_url_value = azure_openai_settings.get("base_url") + if not endpoint_value and not base_url_value: + raise ValueError("Please provide an endpoint or a base_url") + + client_args: dict[str, Any] = {"default_headers": merged_headers} + if resolved_api_version := azure_openai_settings.get("api_version"): + client_args["api_version"] = resolved_api_version + if ad_token_provider: + client_args["azure_ad_token_provider"] = ad_token_provider + if api_key_secret: + client_args["api_key"] = api_key_secret.get_secret_value() + if base_url_value: + client_args["base_url"] = str(base_url_value) + if endpoint_value and not base_url_value: + client_args["azure_endpoint"] = str(endpoint_value) + if embedding_deployment_name: + client_args["azure_deployment"] = embedding_deployment_name + + async_client = AsyncAzureOpenAI(**client_args) + + # Store Azure-specific attributes for serialization + self.endpoint = str(azure_openai_settings.get("endpoint") or "") + self.api_version = azure_openai_settings.get("api_version") or "" + self.deployment_name = embedding_deployment_name + + super().__init__( + async_client=async_client, + model=embedding_deployment_name, + default_headers=default_headers, + ) + if otel_provider_name is not None: + self.OTEL_PROVIDER_NAME = otel_provider_name # type: ignore[misc] + + +# endregion diff --git a/python/packages/core/agent_framework/azure/_entra_id_authentication.py b/python/packages/azure-ai/agent_framework_azure_ai/_entra_id_authentication.py similarity index 97% rename from python/packages/core/agent_framework/azure/_entra_id_authentication.py rename to python/packages/azure-ai/agent_framework_azure_ai/_entra_id_authentication.py index 8f68a40331..b1ae8a4739 100644 --- a/python/packages/core/agent_framework/azure/_entra_id_authentication.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_entra_id_authentication.py @@ -6,11 +6,10 @@ from collections.abc import Awaitable, Callable from typing import Union +from agent_framework.exceptions import ChatClientInvalidAuthException from azure.core.credentials import TokenCredential from azure.core.credentials_async import AsyncTokenCredential -from ..exceptions import ChatClientInvalidAuthException - logger: logging.Logger = logging.getLogger(__name__) AzureTokenProvider = Callable[[], Union[str, Awaitable[str]]] diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py new file mode 100644 index 0000000000..c141cc1115 --- /dev/null +++ b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py @@ -0,0 +1,226 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Microsoft Foundry Agent for connecting to pre-configured agents in Foundry. + +This module provides ``RawFoundryAgent`` and ``FoundryAgent`` — Agent subclasses +that connect to existing PromptAgents or HostedAgents in Foundry. Use +``FoundryAgent`` for the recommended experience with full middleware and telemetry. +""" + +from __future__ import annotations + +import sys +from collections.abc import Callable, Sequence +from typing import TYPE_CHECKING, Any + +from agent_framework import ( + AgentMiddlewareLayer, + BaseContextProvider, + RawAgent, +) +from agent_framework.observability import AgentTelemetryLayer +from azure.ai.projects.aio import AIProjectClient + +from ._entra_id_authentication import AzureCredentialTypes +from ._foundry_agent_client import ( + RawFoundryAgentChatClient, + _FoundryAgentChatClient, # pyright: ignore[reportPrivateUsage] +) + +if sys.version_info >= (3, 13): + from typing import TypeVar # type: ignore # pragma: no cover +else: + from typing_extensions import TypeVar # type: ignore # pragma: no cover +if sys.version_info >= (3, 11): + from typing import TypedDict # type: ignore # pragma: no cover +else: + from typing_extensions import TypedDict # type: ignore # pragma: no cover + +if TYPE_CHECKING: + from agent_framework._middleware import MiddlewareTypes + from agent_framework._tools import FunctionTool + +FoundryAgentOptionsT = TypeVar( + "FoundryAgentOptionsT", + bound=TypedDict, # type: ignore[valid-type] + default="OpenAIChatOptions", # noqa: F821 # pyright: ignore[reportUndefinedVariable] + covariant=True, +) + + +class RawFoundryAgent( # type: ignore[misc] + RawAgent[FoundryAgentOptionsT], +): + """Raw Microsoft Foundry Agent without agent-level middleware or telemetry. + + Connects to an existing PromptAgent or HostedAgent in Foundry. + For full middleware and telemetry support, use :class:`FoundryAgent`. + + Examples: + .. code-block:: python + + from agent_framework.azure import RawFoundryAgent + from azure.identity import AzureCliCredential + + agent = RawFoundryAgent( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-prompt-agent", + agent_version="1.0", + credential=AzureCliCredential(), + ) + result = await agent.run("Hello!") + """ + + def __init__( + self, + *, + project_endpoint: str | None = None, + agent_name: str | None = None, + agent_version: str | None = None, + credential: AzureCredentialTypes | None = None, + project_client: AIProjectClient | None = None, + allow_preview: bool | None = None, + tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None = None, + context_providers: Sequence[BaseContextProvider] | None = None, + client_type: type[RawFoundryAgentChatClient] | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + **kwargs: Any, + ) -> None: + """Initialize a Foundry Agent. + + Keyword Args: + project_endpoint: The Foundry project endpoint URL. + Can also be set via environment variable FOUNDRY_PROJECT_ENDPOINT. + agent_name: The name of the Foundry agent to connect to. + Can also be set via environment variable FOUNDRY_AGENT_NAME. + agent_version: The version of the agent (required for PromptAgents, optional for HostedAgents). + Can also be set via environment variable FOUNDRY_AGENT_VERSION. + credential: Azure credential for authentication. + project_client: An existing AIProjectClient to use. + allow_preview: Enables preview opt-in on internally-created AIProjectClient. + tools: Function tools to provide to the agent. Only ``FunctionTool`` objects are accepted. + context_providers: Optional context providers for injecting dynamic context. + client_type: Custom client class to use (must be a subclass of ``RawFoundryAgentChatClient``). + Defaults to ``_FoundryAgentChatClient`` (full client middleware). + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + kwargs: Additional keyword arguments passed to the Agent base class. + """ + # Create the client + actual_client_type = client_type or _FoundryAgentChatClient + if not issubclass(actual_client_type, RawFoundryAgentChatClient): + raise TypeError( + f"client_type must be a subclass of RawFoundryAgentChatClient, got {actual_client_type.__name__}" + ) + + client = actual_client_type( + project_endpoint=project_endpoint, + agent_name=agent_name, + agent_version=agent_version, + credential=credential, + project_client=project_client, + allow_preview=allow_preview, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + + super().__init__( + client=client, # type: ignore[arg-type] + tools=tools, # type: ignore[arg-type] + context_providers=context_providers, + **kwargs, + ) + + +class FoundryAgent( # type: ignore[misc] + AgentTelemetryLayer, + AgentMiddlewareLayer, + RawFoundryAgent[FoundryAgentOptionsT], +): + """Microsoft Foundry Agent with full middleware and telemetry support. + + Connects to an existing PromptAgent or HostedAgent in Foundry. + This is the recommended class for production use. + + Examples: + .. code-block:: python + + from agent_framework.azure import FoundryAgent + from azure.identity import AzureCliCredential + + # Connect to a PromptAgent + agent = FoundryAgent( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-prompt-agent", + agent_version="1.0", + credential=AzureCliCredential(), + tools=[my_function_tool], + ) + result = await agent.run("Hello!") + + # Connect to a HostedAgent (no version needed) + agent = FoundryAgent( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-hosted-agent", + credential=AzureCliCredential(), + ) + + # Custom client (e.g., raw client without client middleware) + agent = FoundryAgent( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-agent", + credential=AzureCliCredential(), + client_type=RawFoundryAgentChatClient, + ) + """ + + def __init__( + self, + *, + project_endpoint: str | None = None, + agent_name: str | None = None, + agent_version: str | None = None, + credential: AzureCredentialTypes | None = None, + project_client: AIProjectClient | None = None, + allow_preview: bool | None = None, + tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None = None, + context_providers: Sequence[BaseContextProvider] | None = None, + middleware: Sequence[MiddlewareTypes] | None = None, + client_type: type[RawFoundryAgentChatClient] | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + **kwargs: Any, + ) -> None: + """Initialize a Foundry Agent with full middleware and telemetry. + + Keyword Args: + project_endpoint: The Foundry project endpoint URL. + agent_name: The name of the Foundry agent to connect to. + agent_version: The version of the agent (for PromptAgents). + credential: Azure credential for authentication. + project_client: An existing AIProjectClient to use. + allow_preview: Enables preview opt-in on internally-created AIProjectClient. + tools: Function tools to provide to the agent. Only ``FunctionTool`` objects are accepted. + context_providers: Optional context providers. + middleware: Optional agent-level middleware. + client_type: Custom client class (must subclass ``RawFoundryAgentChatClient``). + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + kwargs: Additional keyword arguments. + """ + super().__init__( + project_endpoint=project_endpoint, + agent_name=agent_name, + agent_version=agent_version, + credential=credential, + project_client=project_client, + allow_preview=allow_preview, + tools=tools, + context_providers=context_providers, + middleware=middleware, + client_type=client_type, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + **kwargs, + ) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent_client.py new file mode 100644 index 0000000000..8a90d5bea7 --- /dev/null +++ b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent_client.py @@ -0,0 +1,351 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Microsoft Foundry Agent client for connecting to pre-configured agents in Foundry. + +This module provides ``RawFoundryAgentClient`` and ``FoundryAgentClient`` for +communicating with PromptAgents and HostedAgents via the Responses API. +""" + +from __future__ import annotations + +import logging +import sys +from collections.abc import Mapping, MutableMapping, Sequence +from typing import TYPE_CHECKING, Any, ClassVar, Generic, cast + +from agent_framework._middleware import ChatMiddlewareLayer +from agent_framework._settings import load_settings +from agent_framework._telemetry import AGENT_FRAMEWORK_USER_AGENT +from agent_framework._tools import FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool +from agent_framework._types import Message +from agent_framework.observability import ChatTelemetryLayer +from agent_framework_openai._chat_client import OpenAIChatOptions, RawOpenAIChatClient +from azure.ai.projects.aio import AIProjectClient + +from ._entra_id_authentication import AzureCredentialTypes + +logger: logging.Logger = logging.getLogger(__name__) + +if sys.version_info >= (3, 13): + from typing import TypeVar # type: ignore # pragma: no cover +else: + from typing_extensions import TypeVar # type: ignore # pragma: no cover +if sys.version_info >= (3, 12): + from typing import override # type: ignore # pragma: no cover +else: + from typing_extensions import override # type: ignore # pragma: no cover +if sys.version_info >= (3, 11): + from typing import TypedDict # type: ignore # pragma: no cover +else: + from typing_extensions import TypedDict # type: ignore # pragma: no cover + +if TYPE_CHECKING: + from agent_framework._middleware import ( + ChatMiddleware, + ChatMiddlewareCallable, + FunctionMiddleware, + FunctionMiddlewareCallable, + ) + + +class FoundryAgentSettings(TypedDict, total=False): + """Settings for Microsoft FoundryAgentClient resolved from args and environment. + + Keyword Args: + project_endpoint: The Foundry project endpoint URL. + Can be set via environment variable FOUNDRY_PROJECT_ENDPOINT. + agent_name: The name of the Foundry agent to connect to. + Can be set via environment variable FOUNDRY_AGENT_NAME. + agent_version: The version of the Foundry agent (for PromptAgents). + Can be set via environment variable FOUNDRY_AGENT_VERSION. + """ + + project_endpoint: str | None + agent_name: str | None + agent_version: str | None + + +FoundryAgentOptionsT = TypeVar( + "FoundryAgentOptionsT", + bound=TypedDict, # type: ignore[valid-type] + default="OpenAIChatOptions", + covariant=True, +) + + +class RawFoundryAgentChatClient( # type: ignore[misc] + RawOpenAIChatClient[FoundryAgentOptionsT], + Generic[FoundryAgentOptionsT], +): + """Raw Microsoft Foundry Agent chat client for connecting to pre-configured agents in Foundry. + + Connects to existing PromptAgents or HostedAgents via the Responses API. + Does not create or delete agents — the agent must already exist in Foundry. + + This is a raw client without function invocation, chat middleware, or telemetry layers. + Tools passed in options are validated (only ``FunctionTool`` allowed) but **not invoked** — + the function invocation loop is handled by ``_FoundryAgentChatClient`` or a custom subclass + that includes ``FunctionInvocationLayer``. + + Use this class as an extension point when building a custom client with specific middleware + layers via subclassing:: + + from agent_framework._tools import FunctionInvocationLayer + from agent_framework.azure import RawFoundryAgentChatClient + + + class MyClient(FunctionInvocationLayer, RawFoundryAgentChatClient): + pass + + + agent = FoundryAgent(..., client_type=MyClient) + """ + + OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.foundry" + + def __init__( + self, + *, + project_endpoint: str | None = None, + agent_name: str | None = None, + agent_version: str | None = None, + credential: AzureCredentialTypes | None = None, + project_client: AIProjectClient | None = None, + allow_preview: bool | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + **kwargs: Any, + ) -> None: + """Initialize a raw Foundry Agent client. + + Keyword Args: + project_endpoint: The Foundry project endpoint URL. + Can also be set via environment variable FOUNDRY_PROJECT_ENDPOINT. + agent_name: The name of the Foundry agent to connect to. + Can also be set via environment variable FOUNDRY_AGENT_NAME. + agent_version: The version of the agent (required for PromptAgents, optional for HostedAgents). + Can also be set via environment variable FOUNDRY_AGENT_VERSION. + credential: Azure credential for authentication. + project_client: An existing AIProjectClient to use. + allow_preview: Enables preview opt-in on internally-created AIProjectClient. + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + kwargs: Additional keyword arguments. + """ + settings = load_settings( + FoundryAgentSettings, + env_prefix="FOUNDRY_", + project_endpoint=project_endpoint, + agent_name=agent_name, + agent_version=agent_version, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + + resolved_endpoint = settings.get("project_endpoint") + self.agent_name = settings.get("agent_name") + self.agent_version = settings.get("agent_version") + + if not self.agent_name: + raise ValueError( + "Agent name is required. Set via 'agent_name' parameter or 'FOUNDRY_AGENT_NAME' environment variable." + ) + + # Create or use provided project client + self._should_close_client = False + if project_client is not None: + self.project_client = project_client + else: + if not resolved_endpoint: + raise ValueError( + "Either 'project_endpoint' or 'project_client' is required. " + "Set project_endpoint via parameter or 'FOUNDRY_PROJECT_ENDPOINT' environment variable." + ) + if not credential: + raise ValueError("Azure credential is required when using project_endpoint without a project_client.") + project_client_kwargs: dict[str, Any] = { + "endpoint": resolved_endpoint, + "credential": credential, + "user_agent": AGENT_FRAMEWORK_USER_AGENT, + } + if allow_preview is not None: + project_client_kwargs["allow_preview"] = allow_preview + self.project_client = AIProjectClient(**project_client_kwargs) + self._should_close_client = True + + # Get OpenAI client from project + async_client = self.project_client.get_openai_client() + + super().__init__(async_client=async_client, **kwargs) + + def _get_agent_reference(self) -> dict[str, str]: + """Build the agent reference dict for the Responses API.""" + ref: dict[str, str] = {"name": self.agent_name, "type": "agent_reference"} # type: ignore[dict-item] + if self.agent_version: + ref["version"] = self.agent_version + return ref + + @override + async def _prepare_options( + self, + messages: Sequence[Message], + options: Mapping[str, Any], + **kwargs: Any, + ) -> dict[str, Any]: + """Prepare options for the Responses API, injecting agent reference and validating tools.""" + # Validate tools — only FunctionTool allowed + tools = options.get("tools", []) + if tools: + for tool_item in tools: + if not isinstance(tool_item, FunctionTool): + raise TypeError( + f"Only FunctionTool objects are accepted for Foundry agents, " + f"got {type(tool_item).__name__}. Other tool types (MCPTool, dict schemas, " + f"hosted tools) must be defined on the Foundry agent definition in the service." + ) + + # Prepare messages: extract system/developer messages as instructions + prepared_messages, _instructions = self._prepare_messages_for_azure_ai(messages) + + # Call parent prepare_options (OpenAI Responses API format) + run_options = await super()._prepare_options(prepared_messages, options, **kwargs) + + # Apply Azure AI schema transforms + if "input" in run_options and isinstance(run_options["input"], list): + run_options["input"] = self._transform_input_for_azure_ai(cast(list[dict[str, Any]], run_options["input"])) + + # Inject agent reference + run_options["extra_body"] = {"agent_reference": self._get_agent_reference()} + + return run_options + + @override + def _check_model_presence(self, options: dict[str, Any]) -> None: + """Skip model check — model is configured on the Foundry agent.""" + pass + + def _prepare_messages_for_azure_ai(self, messages: Sequence[Message]) -> tuple[list[Message], str | None]: + """Extract system/developer messages as instructions for Azure AI. + + Foundry agents may not support system/developer messages directly. + Instead, extract them as instructions to prepend. + """ + prepared: list[Message] = [] + instructions_parts: list[str] = [] + for msg in messages: + if msg.role in ("system", "developer"): + if msg.text: + instructions_parts.append(msg.text) + else: + prepared.append(msg) + instructions = "\n".join(instructions_parts) if instructions_parts else None + return prepared, instructions + + def _transform_input_for_azure_ai(self, input_items: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Transform input items to match Azure AI Projects expected schema. + + Azure AI Projects 'create responses' API expects 'type' at item level + and 'annotations' for output_text content items. + """ + transformed: list[dict[str, Any]] = [] + for item in input_items: + new_item: dict[str, Any] = dict(item) + + if "role" in new_item and "type" not in new_item: + new_item["type"] = "message" + + if (content := new_item.get("content")) and isinstance(content, list): + new_content: list[Any] = [] + for content_item in content: # type: ignore[union-attr] + if isinstance(content_item, MutableMapping): + if content_item.get("type") == "output_text" and "annotations" not in content_item: # type: ignore[operator] + content_item["annotations"] = [] + new_content.append(content_item) + else: + new_content.append(content_item) + new_item["content"] = new_content + + transformed.append(new_item) + + return transformed + + async def close(self) -> None: + """Close the project client if we created it.""" + if self._should_close_client: + await self.project_client.close() + + +class _FoundryAgentChatClient( # type: ignore[misc] + FunctionInvocationLayer[FoundryAgentOptionsT], + ChatMiddlewareLayer[FoundryAgentOptionsT], + ChatTelemetryLayer[FoundryAgentOptionsT], + RawFoundryAgentChatClient[FoundryAgentOptionsT], + Generic[FoundryAgentOptionsT], +): + """Microsoft Foundry Agent client with middleware, telemetry, and function invocation support. + + Connects to existing PromptAgents or HostedAgents in Foundry. + + Examples: + .. code-block:: python + + from agent_framework import Agent + from agent_framework.azure import FoundryAgentClient + from azure.identity import AzureCliCredential + + client = FoundryAgentClient( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-prompt-agent", + agent_version="1.0", + credential=AzureCliCredential(), + ) + + agent = Agent(client=client, tools=[my_function_tool]) + result = await agent.run("Hello!") + """ + + def __init__( + self, + *, + project_endpoint: str | None = None, + agent_name: str | None = None, + agent_version: str | None = None, + credential: AzureCredentialTypes | None = None, + project_client: AIProjectClient | None = None, + allow_preview: bool | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + middleware: ( + Sequence[ChatMiddleware | ChatMiddlewareCallable | FunctionMiddleware | FunctionMiddlewareCallable] | None + ) = None, + function_invocation_configuration: FunctionInvocationConfiguration | None = None, + **kwargs: Any, + ) -> None: + """Initialize a Foundry Agent client with full middleware support. + + Keyword Args: + project_endpoint: The Foundry project endpoint URL. + agent_name: The name of the Foundry agent to connect to. + agent_version: The version of the agent (for PromptAgents). + credential: Azure credential for authentication. + project_client: An existing AIProjectClient to use. + allow_preview: Enables preview opt-in on internally-created AIProjectClient. + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + middleware: Optional sequence of middleware. + function_invocation_configuration: Optional function invocation configuration. + kwargs: Additional keyword arguments. + """ + super().__init__( + project_endpoint=project_endpoint, + agent_name=agent_name, + agent_version=agent_version, + credential=credential, + project_client=project_client, + allow_preview=allow_preview, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + middleware=middleware, + function_invocation_configuration=function_invocation_configuration, + **kwargs, + ) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_chat_client.py new file mode 100644 index 0000000000..bd3250cc64 --- /dev/null +++ b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_chat_client.py @@ -0,0 +1,481 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import sys +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal + +from agent_framework._middleware import ChatMiddlewareLayer +from agent_framework._settings import load_settings +from agent_framework._telemetry import AGENT_FRAMEWORK_USER_AGENT +from agent_framework._tools import FunctionInvocationConfiguration, FunctionInvocationLayer +from agent_framework._types import Content +from agent_framework.observability import ChatTelemetryLayer +from agent_framework_openai._chat_client import OpenAIChatOptions, RawOpenAIChatClient +from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import ( + AutoCodeInterpreterToolParam, + CodeInterpreterTool, + ImageGenTool, + WebSearchApproximateLocation, + WebSearchTool, + WebSearchToolFilters, +) +from azure.ai.projects.models import FileSearchTool as ProjectsFileSearchTool +from azure.ai.projects.models import MCPTool as FoundryMCPTool + +from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider +from ._shared import resolve_file_ids + +if sys.version_info >= (3, 13): + from typing import TypeVar # type: ignore # pragma: no cover +else: + from typing_extensions import TypeVar # type: ignore # pragma: no cover +if sys.version_info >= (3, 12): + from typing import override # type: ignore # pragma: no cover +else: + from typing_extensions import override # type: ignore # pragma: no cover +if sys.version_info >= (3, 11): + from typing import TypedDict # type: ignore # pragma: no cover +else: + from typing_extensions import TypedDict # type: ignore # pragma: no cover + +if TYPE_CHECKING: + from agent_framework._middleware import ( + ChatMiddleware, + ChatMiddlewareCallable, + FunctionMiddleware, + FunctionMiddlewareCallable, + ) + from openai import AsyncOpenAI + + +class FoundrySettings(TypedDict, total=False): + """Settings for Microsoft FoundryChatClient resolved from args and environment. + + Keyword Args: + model: The model deployment name. + Can be set via environment variable FOUNDRY_MODEL. + project_endpoint: The Microsoft Foundry project endpoint URL. + Can be set via environment variable FOUNDRY_PROJECT_ENDPOINT. + """ + + model: str | None + project_endpoint: str | None + + +FoundryChatOptionsT = TypeVar( + "FoundryChatOptionsT", + bound=TypedDict, # type: ignore[valid-type] + default="OpenAIChatOptions", + covariant=True, +) + +FoundryChatOptions = OpenAIChatOptions + + +class RawFoundryChatClient( # type: ignore[misc] + RawOpenAIChatClient[FoundryChatOptionsT], + Generic[FoundryChatOptionsT], +): + """Raw Microsoft Foundry chat client using the OpenAI Responses API via a Foundry project. + + This client creates an OpenAI-compatible client from a Foundry project + and delegates to ``RawOpenAIChatClient`` for request handling. + + Warning: + **This class should not normally be used directly.** Use ``FoundryChatClient`` + for a fully-featured client with middleware, telemetry, and function invocation. + """ + + OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.foundry" # type: ignore[reportIncompatibleVariableOverride, misc] + + def __init__( + self, + *, + project_endpoint: str | None = None, + project_client: AIProjectClient | None = None, + model: str | None = None, + credential: AzureCredentialTypes | AzureTokenProvider | None = None, + allow_preview: bool | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + instruction_role: str | None = None, + **kwargs: Any, + ) -> None: + """Initialize a raw Microsoft Foundry chat client. + + Keyword Args: + project_endpoint: The Foundry project endpoint URL. + Can also be set via environment variable FOUNDRY_PROJECT_ENDPOINT. + project_client: An existing AIProjectClient to use. If provided, + the OpenAI client will be obtained via ``project_client.get_openai_client()``. + model: The model deployment name. + Can also be set via environment variable FOUNDRY_MODEL. + credential: Azure credential or token provider for authentication. + Required when using ``project_endpoint`` without a ``project_client``. + allow_preview: Enables preview opt-in on internally-created AIProjectClient. + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + instruction_role: The role to use for 'instruction' messages. + kwargs: Additional keyword arguments. + """ + foundry_settings = load_settings( + FoundrySettings, + env_prefix="FOUNDRY_", + model=model, + project_endpoint=project_endpoint, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + + resolved_model = foundry_settings.get("model") + if not resolved_model: + raise ValueError("Model is required. Set via 'model' parameter or 'FOUNDRY_MODEL' environment variable.") + + resolved_endpoint = foundry_settings.get("project_endpoint") + + if resolved_endpoint is None and project_client is None: + raise ValueError( + "Either 'project_endpoint' or 'project_client' is required. " + "Set project_endpoint via parameter or 'FOUNDRY_PROJECT_ENDPOINT' environment variable." + ) + + async_client = self._create_client_from_project( + project_client=project_client, + project_endpoint=resolved_endpoint, + credential=credential, + allow_preview=allow_preview, + ) + + super().__init__( + model=resolved_model, + async_client=async_client, + instruction_role=instruction_role, + **kwargs, + ) + + @staticmethod + def _create_client_from_project( + *, + project_client: AIProjectClient | None, + project_endpoint: str | None, + credential: AzureCredentialTypes | AzureTokenProvider | None, + allow_preview: bool | None = None, + ) -> AsyncOpenAI: + """Create an AsyncOpenAI client from a Foundry project.""" + if project_client is not None: + return project_client.get_openai_client() + + if not project_endpoint: + raise ValueError( + "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " + "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." + ) + if not credential: + raise ValueError("Azure credential is required when using project_endpoint without a project_client.") + project_client_kwargs: dict[str, Any] = { + "endpoint": project_endpoint, + "credential": credential, # type: ignore[arg-type] + "user_agent": AGENT_FRAMEWORK_USER_AGENT, + } + if allow_preview is not None: + project_client_kwargs["allow_preview"] = allow_preview + project_client = AIProjectClient(**project_client_kwargs) + return project_client.get_openai_client() + + @override + def _check_model_presence(self, options: dict[str, Any]) -> None: + if not options.get("model"): + if not self.model: + raise ValueError("model must be a non-empty string") + options["model"] = self.model + + # region Tool factory methods (override OpenAI defaults with Foundry versions) + + @staticmethod + def get_code_interpreter_tool( # type: ignore[override] + *, + file_ids: list[str | Content] | None = None, + container: Literal["auto"] | dict[str, Any] = "auto", + **kwargs: Any, + ) -> CodeInterpreterTool: + """Create a code interpreter tool configuration for Foundry. + + Keyword Args: + file_ids: Optional list of file IDs or Content objects to make available. + container: Container configuration. Use "auto" for automatic management. + **kwargs: Additional arguments passed to the SDK CodeInterpreterTool constructor. + + Returns: + A CodeInterpreterTool ready to pass to an Agent. + """ + if file_ids is None and isinstance(container, dict): + file_ids = container.get("file_ids") + resolved = resolve_file_ids(file_ids) + tool_container = AutoCodeInterpreterToolParam(file_ids=resolved) + return CodeInterpreterTool(container=tool_container, **kwargs) + + @staticmethod + def get_file_search_tool( + *, + vector_store_ids: list[str], + max_num_results: int | None = None, + ranking_options: dict[str, Any] | None = None, + filters: dict[str, Any] | None = None, + **kwargs: Any, + ) -> ProjectsFileSearchTool: + """Create a file search tool configuration for Foundry. + + Keyword Args: + vector_store_ids: List of vector store IDs to search. + max_num_results: Maximum number of results to return (1-50). + ranking_options: Ranking options for search results. + filters: A filter to apply (ComparisonFilter or CompoundFilter). + **kwargs: Additional arguments passed to the SDK FileSearchTool constructor. + + Returns: + A FileSearchTool ready to pass to an Agent. + """ + if not vector_store_ids: + raise ValueError("File search tool requires 'vector_store_ids' to be specified.") + return ProjectsFileSearchTool( + vector_store_ids=vector_store_ids, + max_num_results=max_num_results, + ranking_options=ranking_options, # type: ignore[arg-type] + filters=filters, # type: ignore[arg-type] + **kwargs, + ) + + @staticmethod + def get_web_search_tool( # type: ignore[override] + *, + user_location: dict[str, str] | None = None, + search_context_size: Literal["low", "medium", "high"] | None = None, + allowed_domains: list[str] | None = None, + custom_search_configuration: dict[str, Any] | None = None, + **kwargs: Any, + ) -> WebSearchTool: + """Create a web search tool configuration for Microsoft Foundry. + + Keyword Args: + user_location: Location context with keys like "city", "country", "region", "timezone". + search_context_size: Amount of context from search results ("low", "medium", "high"). + allowed_domains: List of domains to restrict search results to. + custom_search_configuration: Custom Bing search configuration. + **kwargs: Additional arguments passed to the SDK WebSearchTool constructor. + + Returns: + A WebSearchTool ready to pass to an Agent. + """ + ws_kwargs: dict[str, Any] = {**kwargs} + if search_context_size: + ws_kwargs["search_context_size"] = search_context_size + if allowed_domains: + ws_kwargs["filters"] = WebSearchToolFilters(allowed_domains=allowed_domains) + if custom_search_configuration: + ws_kwargs["custom_search_configuration"] = custom_search_configuration + ws_tool = WebSearchTool(**ws_kwargs) + if user_location: + ws_tool.user_location = WebSearchApproximateLocation( + city=user_location.get("city"), + country=user_location.get("country"), + region=user_location.get("region"), + timezone=user_location.get("timezone"), + ) + return ws_tool + + @staticmethod + def get_image_generation_tool( # type: ignore[override] + *, + model: Literal["gpt-image-1"] | str | None = None, + size: Literal["1024x1024", "1024x1536", "1536x1024", "auto"] | None = None, + output_format: Literal["png", "webp", "jpeg"] | None = None, + quality: Literal["low", "medium", "high", "auto"] | None = None, + background: Literal["transparent", "opaque", "auto"] | None = None, + partial_images: int | None = None, + moderation: Literal["auto", "low"] | None = None, + output_compression: int | None = None, + **kwargs: Any, + ) -> ImageGenTool: + """Create an image generation tool configuration for Foundry. + + Keyword Args: + model: The model to use for image generation. + size: Output image size. + output_format: Output image format. + quality: Output image quality. + background: Background transparency setting. + partial_images: Number of partial images to return during generation. + moderation: Moderation level. + output_compression: Compression level. + **kwargs: Additional arguments passed to the SDK ImageGenTool constructor. + + Returns: + An ImageGenTool ready to pass to an Agent. + """ + return ImageGenTool( # type: ignore[misc] + model=model, # type: ignore[arg-type] + size=size, + output_format=output_format, + quality=quality, + background=background, + partial_images=partial_images, + moderation=moderation, + output_compression=output_compression, + **kwargs, + ) + + @staticmethod + def get_mcp_tool( + *, + name: str, + url: str | None = None, + description: str | None = None, + approval_mode: Literal["always_require", "never_require"] | dict[str, list[str]] | None = None, + allowed_tools: list[str] | None = None, + headers: dict[str, str] | None = None, + project_connection_id: str | None = None, + **kwargs: Any, + ) -> FoundryMCPTool: + """Create a hosted MCP tool configuration for Foundry. + + This configures an MCP server that runs remotely on Azure AI, not locally. + + Keyword Args: + name: A label/name for the MCP server. + url: The URL of the MCP server. Required if project_connection_id is not provided. + description: A description of what the MCP server provides. + approval_mode: Tool approval mode ("always_require", "never_require", or dict). + allowed_tools: List of allowed tool names from this MCP server. + headers: HTTP headers to include in requests to the MCP server. + project_connection_id: Foundry connection ID for managed MCP connections. + **kwargs: Additional arguments passed to the SDK MCPTool constructor. + + Returns: + An MCPTool configuration ready to pass to an Agent. + """ + mcp = FoundryMCPTool(server_label=name.replace(" ", "_"), server_url=url or "", **kwargs) + + if description: + mcp["server_description"] = description + if project_connection_id: + mcp["project_connection_id"] = project_connection_id + elif headers: + mcp["headers"] = headers + if allowed_tools: + mcp["allowed_tools"] = allowed_tools + if approval_mode: + if isinstance(approval_mode, str): + mcp["require_approval"] = "always" if approval_mode == "always_require" else "never" + else: + if always_require := approval_mode.get("always_require_approval"): + mcp["require_approval"] = {"always": {"tool_names": always_require}} + if never_require := approval_mode.get("never_require_approval"): + mcp["require_approval"] = {"never": {"tool_names": never_require}} + + return mcp + + # endregion + + +class FoundryChatClient( # type: ignore[misc] + FunctionInvocationLayer[FoundryChatOptionsT], + ChatMiddlewareLayer[FoundryChatOptionsT], + ChatTelemetryLayer[FoundryChatOptionsT], + RawFoundryChatClient[FoundryChatOptionsT], + Generic[FoundryChatOptionsT], +): + """Microsoft Foundry chat client using the OpenAI Responses API. + + Creates an OpenAI-compatible client from a Foundry project + with middleware, telemetry, and function invocation support. + + Keyword Args: + project_endpoint: The Foundry project endpoint URL. + Can also be set via environment variable AZURE_AI_PROJECT_ENDPOINT. + project_client: An existing AIProjectClient to use. + model: The model deployment name. + Can also be set via environment variable AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME. + model_id: Deprecated alias for ``model``. + credential: Azure credential or token provider for authentication. + allow_preview: Enables preview opt-in on internally-created AIProjectClient. + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + instruction_role: The role to use for 'instruction' messages. + middleware: Optional sequence of middleware. + function_invocation_configuration: Optional function invocation configuration. + + Examples: + .. code-block:: python + + from azure.identity import AzureCliCredential + from agent_framework_azure_ai import FoundryChatClient + + client = FoundryChatClient( + project_endpoint="https://your-project.services.ai.azure.com", + model="gpt-4o", + credential=AzureCliCredential(), + ) + + # Or using an existing AIProjectClient + from azure.ai.projects.aio import AIProjectClient + + project_client = AIProjectClient( + endpoint="https://your-project.services.ai.azure.com", + credential=AzureCliCredential(), + ) + client = FoundryChatClient( + project_client=project_client, + model="gpt-4o", + ) + """ + + OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.foundry" # type: ignore[reportIncompatibleVariableOverride, misc] + + def __init__( + self, + *, + project_endpoint: str | None = None, + project_client: AIProjectClient | None = None, + model: str | None = None, + credential: AzureCredentialTypes | AzureTokenProvider | None = None, + allow_preview: bool | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + instruction_role: str | None = None, + middleware: ( + Sequence[ChatMiddleware | ChatMiddlewareCallable | FunctionMiddleware | FunctionMiddlewareCallable] | None + ) = None, + function_invocation_configuration: FunctionInvocationConfiguration | None = None, + **kwargs: Any, + ) -> None: + """Initialize a Foundry chat client. + + Keyword Args: + project_endpoint: The Foundry project endpoint URL. + project_client: An existing AIProjectClient to use. + model: The model deployment name. + credential: Azure credential or token provider for authentication. + allow_preview: Enables preview opt-in on internally-created AIProjectClient. + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + instruction_role: The role to use for 'instruction' messages. + middleware: Optional sequence of middleware. + function_invocation_configuration: Optional function invocation configuration. + kwargs: Additional keyword arguments. + """ + super().__init__( + project_endpoint=project_endpoint, + project_client=project_client, + model=model, + credential=credential, + allow_preview=allow_preview, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + instruction_role=instruction_role, + middleware=middleware, + function_invocation_configuration=function_invocation_configuration, + **kwargs, + ) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_memory_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_memory_provider.py index fe5ab47ac5..1a20ce0f04 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_memory_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_memory_provider.py @@ -16,10 +16,10 @@ from agent_framework import AGENT_FRAMEWORK_USER_AGENT, Message from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext from agent_framework._settings import load_settings -from agent_framework.azure._entra_id_authentication import AzureCredentialTypes from azure.ai.projects.aio import AIProjectClient from openai.types.responses import ResponseInputItemParam +from ._entra_id_authentication import AzureCredentialTypes from ._shared import AzureAISettings if sys.version_info >= (3, 11): diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py index 82e6a1d5b7..2b83b69ae4 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py @@ -18,7 +18,6 @@ from agent_framework._mcp import MCPTool from agent_framework._settings import load_settings from agent_framework._tools import ToolTypes -from agent_framework.azure._entra_id_authentication import AzureCredentialTypes from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( AgentVersionDetails, @@ -30,6 +29,7 @@ ) from ._client import AzureAIClient, AzureAIProjectAgentOptions +from ._entra_id_authentication import AzureCredentialTypes from ._shared import AzureAISettings, create_text_format_config, from_azure_ai_tools, to_azure_ai_tools if sys.version_info >= (3, 13): diff --git a/python/packages/core/tests/azure/conftest.py b/python/packages/azure-ai/tests/azure_openai/conftest.py similarity index 99% rename from python/packages/core/tests/azure/conftest.py rename to python/packages/azure-ai/tests/azure_openai/conftest.py index 9d8ce0cebb..8e32c53608 100644 --- a/python/packages/core/tests/azure/conftest.py +++ b/python/packages/azure-ai/tests/azure_openai/conftest.py @@ -1,9 +1,8 @@ # Copyright (c) Microsoft. All rights reserved. from typing import Any -from pytest import fixture - from agent_framework import Message +from pytest import fixture # region: Connector Settings fixtures diff --git a/python/packages/core/tests/azure/test_azure_assistants_client.py b/python/packages/azure-ai/tests/azure_openai/test_azure_assistants_client.py similarity index 95% rename from python/packages/core/tests/azure/test_azure_assistants_client.py rename to python/packages/azure-ai/tests/azure_openai/test_azure_assistants_client.py index 3c51881279..67271c7f92 100644 --- a/python/packages/core/tests/azure/test_azure_assistants_client.py +++ b/python/packages/azure-ai/tests/azure_openai/test_azure_assistants_client.py @@ -5,9 +5,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from azure.identity import AzureCliCredential -from pydantic import Field - from agent_framework import ( Agent, AgentResponse, @@ -21,6 +18,8 @@ ) from agent_framework._settings import SecretString from agent_framework.azure import AzureOpenAIAssistantsClient +from azure.identity import AzureCliCredential +from pydantic import Field skip_if_azure_integration_tests_disabled = pytest.mark.skipif( os.getenv("AZURE_OPENAI_ENDPOINT", "") in ("", "https://test-endpoint.com"), @@ -87,7 +86,7 @@ def test_azure_assistants_client_init_with_client(mock_async_azure_openai: Magic ) assert client.client is mock_async_azure_openai - assert client.model_id == "test_chat_deployment" + assert client.model == "test_chat_deployment" assert client.assistant_id == "existing-assistant-id" assert client.thread_id == "test-thread-id" assert not client._should_delete_assistant # type: ignore @@ -108,7 +107,7 @@ def test_azure_assistants_client_init_auto_create_client( ) assert client.client is mock_async_azure_openai - assert client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] assert client.assistant_id is None assert client.assistant_name == "TestAssistant" assert not client._should_delete_assistant # type: ignore @@ -139,7 +138,7 @@ def test_azure_assistants_client_init_with_default_headers(azure_openai_unit_tes default_headers=default_headers, ) - assert client.model_id == "test_chat_deployment" + assert client.model == "test_chat_deployment" assert isinstance(client, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers @@ -235,7 +234,7 @@ def test_azure_assistants_client_serialize(azure_openai_unit_test_env: dict[str, dumped_settings = client.to_dict() - assert dumped_settings["model_id"] == "test_chat_deployment" + assert dumped_settings["model"] == "test_chat_deployment" assert dumped_settings["assistant_id"] == "test-assistant-id" assert dumped_settings["assistant_name"] == "TestAssistant" assert dumped_settings["thread_id"] == "test-thread-id" @@ -563,12 +562,12 @@ def test_azure_assistants_client_entra_id_authentication() -> None: mock_provider = MagicMock(return_value="token-string") with ( - patch("agent_framework.azure._assistants_client.load_settings") as mock_load_settings, + patch("agent_framework_azure_ai._deprecated_azure_openai.load_settings") as mock_load_settings, patch( - "agent_framework.azure._assistants_client.resolve_credential_to_token_provider", + "agent_framework_azure_ai._deprecated_azure_openai.resolve_credential_to_token_provider", return_value=mock_provider, ) as mock_resolve, - patch("agent_framework.azure._assistants_client.AsyncAzureOpenAI") as mock_azure_client, + patch("agent_framework_azure_ai._deprecated_azure_openai.AsyncAzureOpenAI") as mock_azure_client, patch("agent_framework.openai.OpenAIAssistantsClient.__init__", return_value=None), ): mock_load_settings.return_value = { @@ -602,7 +601,7 @@ def test_azure_assistants_client_entra_id_authentication() -> None: def test_azure_assistants_client_no_authentication_error() -> None: """Test authentication validation error when no auth provided.""" - with patch("agent_framework.azure._assistants_client.load_settings") as mock_load_settings: + with patch("agent_framework_azure_ai._deprecated_azure_openai.load_settings") as mock_load_settings: mock_load_settings.return_value = { "chat_deployment_name": "test-deployment", "responses_deployment_name": None, @@ -627,12 +626,12 @@ def test_azure_assistants_client_callable_credential() -> None: mock_provider = MagicMock(return_value="my-token") with ( - patch("agent_framework.azure._assistants_client.load_settings") as mock_load_settings, + patch("agent_framework_azure_ai._deprecated_azure_openai.load_settings") as mock_load_settings, patch( - "agent_framework.azure._assistants_client.resolve_credential_to_token_provider", + "agent_framework_azure_ai._deprecated_azure_openai.resolve_credential_to_token_provider", return_value=mock_provider, ), - patch("agent_framework.azure._assistants_client.AsyncAzureOpenAI") as mock_azure_client, + patch("agent_framework_azure_ai._deprecated_azure_openai.AsyncAzureOpenAI") as mock_azure_client, patch("agent_framework.openai.OpenAIAssistantsClient.__init__", return_value=None), ): mock_load_settings.return_value = { @@ -664,8 +663,8 @@ def test_azure_assistants_client_callable_credential() -> None: def test_azure_assistants_client_base_url_configuration() -> None: """Test base_url client parameter path.""" with ( - patch("agent_framework.azure._assistants_client.load_settings") as mock_load_settings, - patch("agent_framework.azure._assistants_client.AsyncAzureOpenAI") as mock_azure_client, + patch("agent_framework_azure_ai._deprecated_azure_openai.load_settings") as mock_load_settings, + patch("agent_framework_azure_ai._deprecated_azure_openai.AsyncAzureOpenAI") as mock_azure_client, patch("agent_framework.openai.OpenAIAssistantsClient.__init__", return_value=None), ): mock_load_settings.return_value = { @@ -695,8 +694,8 @@ def test_azure_assistants_client_base_url_configuration() -> None: def test_azure_assistants_client_azure_endpoint_configuration() -> None: """Test azure_endpoint client parameter path.""" with ( - patch("agent_framework.azure._assistants_client.load_settings") as mock_load_settings, - patch("agent_framework.azure._assistants_client.AsyncAzureOpenAI") as mock_azure_client, + patch("agent_framework_azure_ai._deprecated_azure_openai.load_settings") as mock_load_settings, + patch("agent_framework_azure_ai._deprecated_azure_openai.AsyncAzureOpenAI") as mock_azure_client, patch("agent_framework.openai.OpenAIAssistantsClient.__init__", return_value=None), ): mock_load_settings.return_value = { diff --git a/python/packages/core/tests/azure/test_azure_chat_client.py b/python/packages/azure-ai/tests/azure_openai/test_azure_chat_client.py similarity index 98% rename from python/packages/core/tests/azure/test_azure_chat_client.py rename to python/packages/azure-ai/tests/azure_openai/test_azure_chat_client.py index 80902de0af..22e0a20d96 100644 --- a/python/packages/core/tests/azure/test_azure_chat_client.py +++ b/python/packages/azure-ai/tests/azure_openai/test_azure_chat_client.py @@ -6,16 +6,6 @@ import openai import pytest -from azure.identity import AzureCliCredential -from httpx import Request, Response -from openai import AsyncAzureOpenAI, AsyncStream -from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions -from openai.types.chat import ChatCompletion, ChatCompletionChunk -from openai.types.chat.chat_completion import Choice -from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice -from openai.types.chat.chat_completion_chunk import ChoiceDelta as ChunkChoiceDelta -from openai.types.chat.chat_completion_message import ChatCompletionMessage - from agent_framework import ( Agent, AgentResponse, @@ -29,10 +19,19 @@ from agent_framework._telemetry import USER_AGENT_KEY from agent_framework.azure import AzureOpenAIChatClient from agent_framework.exceptions import ChatClientException -from agent_framework.openai import ( +from agent_framework_openai import ( ContentFilterResultSeverity, OpenAIContentFilterException, ) +from azure.identity import AzureCliCredential +from httpx import Request, Response +from openai import AsyncAzureOpenAI, AsyncStream +from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions +from openai.types.chat import ChatCompletion, ChatCompletionChunk +from openai.types.chat.chat_completion import Choice +from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice +from openai.types.chat.chat_completion_chunk import ChoiceDelta as ChunkChoiceDelta +from openai.types.chat.chat_completion_message import ChatCompletionMessage # region Service Setup @@ -48,7 +47,7 @@ def test_init(azure_openai_unit_test_env: dict[str, str]) -> None: assert azure_chat_client.client is not None assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) - assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + assert azure_chat_client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] assert isinstance(azure_chat_client, SupportsChatGetResponse) @@ -71,7 +70,7 @@ def test_init_base_url(azure_openai_unit_test_env: dict[str, str]) -> None: assert azure_chat_client.client is not None assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) - assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + assert azure_chat_client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] assert isinstance(azure_chat_client, SupportsChatGetResponse) for key, value in default_headers.items(): assert key in azure_chat_client.client.default_headers @@ -84,7 +83,7 @@ def test_init_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None: assert azure_chat_client.client is not None assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) - assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + assert azure_chat_client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] assert isinstance(azure_chat_client, SupportsChatGetResponse) @@ -131,7 +130,7 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: azure_chat_client = AzureOpenAIChatClient.from_dict(settings) dumped_settings = azure_chat_client.to_dict() - assert dumped_settings["model_id"] == settings["deployment_name"] + assert dumped_settings["model"] == settings["deployment_name"] assert str(settings["endpoint"]) in str(dumped_settings["endpoint"]) assert str(settings["deployment_name"]) == str(dumped_settings["deployment_name"]) assert settings["api_version"] == dumped_settings["api_version"] diff --git a/python/packages/core/tests/azure/test_azure_embedding_client.py b/python/packages/azure-ai/tests/azure_openai/test_azure_embedding_client.py similarity index 97% rename from python/packages/core/tests/azure/test_azure_embedding_client.py rename to python/packages/azure-ai/tests/azure_openai/test_azure_embedding_client.py index 97e477549c..de78178df1 100644 --- a/python/packages/core/tests/azure/test_azure_embedding_client.py +++ b/python/packages/azure-ai/tests/azure_openai/test_azure_embedding_client.py @@ -6,13 +6,12 @@ from unittest.mock import AsyncMock, MagicMock import pytest +from agent_framework.azure import AzureOpenAIEmbeddingClient +from agent_framework_openai import OpenAIEmbeddingOptions from openai.types import CreateEmbeddingResponse from openai.types import Embedding as OpenAIEmbedding from openai.types.create_embedding_response import Usage -from agent_framework.azure import AzureOpenAIEmbeddingClient -from agent_framework.openai import OpenAIEmbeddingOptions - def _make_openai_response( embeddings: list[list[float]], @@ -49,7 +48,7 @@ def test_azure_construction_with_deployment_name(azure_embedding_unit_test_env: api_key="test-key", endpoint="https://test.openai.azure.com/", ) - assert client.model_id == "text-embedding-3-small" + assert client.model == "text-embedding-3-small" def test_azure_construction_with_existing_client(azure_embedding_unit_test_env: None) -> None: @@ -58,7 +57,7 @@ def test_azure_construction_with_existing_client(azure_embedding_unit_test_env: deployment_name="my-deployment", async_client=mock_client, ) - assert client.model_id == "my-deployment" + assert client.model == "my-deployment" assert client.client is mock_client diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/azure-ai/tests/azure_openai/test_azure_responses_client.py similarity index 95% rename from python/packages/core/tests/azure/test_azure_responses_client.py rename to python/packages/azure-ai/tests/azure_openai/test_azure_responses_client.py index 35eaa2b407..65e9629b96 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/azure-ai/tests/azure_openai/test_azure_responses_client.py @@ -8,10 +8,6 @@ from unittest.mock import MagicMock import pytest -from azure.identity import AzureCliCredential -from pydantic import BaseModel -from pytest import param - from agent_framework import ( Agent, AgentResponse, @@ -22,6 +18,9 @@ tool, ) from agent_framework.azure import AzureOpenAIResponsesClient +from azure.identity import AzureCliCredential +from pydantic import BaseModel +from pytest import param skip_if_azure_integration_tests_disabled = pytest.mark.skipif( os.getenv("AZURE_OPENAI_ENDPOINT", "") in ("", "https://test-endpoint.com"), @@ -75,7 +74,7 @@ def test_init(azure_openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization azure_responses_client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) - assert azure_responses_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + assert azure_responses_client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] assert isinstance(azure_responses_client, SupportsChatGetResponse) @@ -90,7 +89,7 @@ def test_init_model_id_constructor(azure_openai_unit_test_env: dict[str, str]) - model_id = "test_model_id" azure_responses_client = AzureOpenAIResponsesClient(deployment_name=model_id) - assert azure_responses_client.model_id == model_id + assert azure_responses_client.model == model_id assert isinstance(azure_responses_client, SupportsChatGetResponse) @@ -98,7 +97,7 @@ def test_init_model_id_kwarg(azure_openai_unit_test_env: dict[str, str]) -> None """Test that model_id kwarg correctly sets the deployment name (issue #4299).""" azure_responses_client = AzureOpenAIResponsesClient(model_id="gpt-4o") - assert azure_responses_client.model_id == "gpt-4o" + assert azure_responses_client.model == "gpt-4o" assert isinstance(azure_responses_client, SupportsChatGetResponse) @@ -108,7 +107,7 @@ def test_init_model_id_kwarg_does_not_override_deployment_name( """Test that deployment_name takes precedence over model_id kwarg (issue #4299).""" azure_responses_client = AzureOpenAIResponsesClient(deployment_name="my-deployment", model_id="gpt-4o") - assert azure_responses_client.model_id == "my-deployment" + assert azure_responses_client.model == "my-deployment" assert isinstance(azure_responses_client, SupportsChatGetResponse) @@ -116,7 +115,7 @@ def test_init_model_id_kwarg_none(azure_openai_unit_test_env: dict[str, str]) -> """Test that model_id=None does not override the env-var deployment name.""" azure_responses_client = AzureOpenAIResponsesClient(model_id=None) - assert azure_responses_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + assert azure_responses_client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) -> None: @@ -127,7 +126,7 @@ def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) -> default_headers=default_headers, ) - assert azure_responses_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + assert azure_responses_client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] assert isinstance(azure_responses_client, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers @@ -156,7 +155,7 @@ def test_init_with_project_client(azure_openai_unit_test_env: dict[str, str]) -> mock_project_client.get_openai_client.return_value = mock_openai_client with patch( - "agent_framework.azure._responses_client.AzureOpenAIResponsesClient._create_client_from_project", + "agent_framework_azure_ai._deprecated_azure_openai.AzureOpenAIResponsesClient._create_client_from_project", return_value=mock_openai_client, ): azure_responses_client = AzureOpenAIResponsesClient( @@ -164,7 +163,7 @@ def test_init_with_project_client(azure_openai_unit_test_env: dict[str, str]) -> deployment_name="gpt-4o", ) - assert azure_responses_client.model_id == "gpt-4o" + assert azure_responses_client.model == "gpt-4o" assert azure_responses_client.client is mock_openai_client assert isinstance(azure_responses_client, SupportsChatGetResponse) @@ -179,7 +178,7 @@ def test_init_with_project_endpoint(azure_openai_unit_test_env: dict[str, str]) mock_openai_client.default_headers = {} with patch( - "agent_framework.azure._responses_client.AzureOpenAIResponsesClient._create_client_from_project", + "agent_framework_azure_ai._deprecated_azure_openai.AzureOpenAIResponsesClient._create_client_from_project", return_value=mock_openai_client, ): azure_responses_client = AzureOpenAIResponsesClient( @@ -188,7 +187,7 @@ def test_init_with_project_endpoint(azure_openai_unit_test_env: dict[str, str]) credential=AzureCliCredential(), ) - assert azure_responses_client.model_id == "gpt-4o" + assert azure_responses_client.model == "gpt-4o" assert azure_responses_client.client is mock_openai_client assert isinstance(azure_responses_client, SupportsChatGetResponse) @@ -220,7 +219,7 @@ def test_create_client_from_project_with_endpoint() -> None: mock_openai_client = MagicMock(spec=AsyncOpenAI) mock_credential = MagicMock() - with patch("agent_framework.azure._responses_client.AIProjectClient") as MockAIProjectClient: + with patch("agent_framework_azure_ai._deprecated_azure_openai.AIProjectClient") as MockAIProjectClient: mock_instance = MockAIProjectClient.return_value mock_instance.get_openai_client.return_value = mock_openai_client @@ -668,8 +667,8 @@ def get_test_image() -> Content: skip_if_azure_ai_integration_tests_disabled = pytest.mark.skipif( os.getenv("AZURE_AI_PROJECT_ENDPOINT", "") in ("", "https://test-project.cognitiveservices.azure.com/") - or os.getenv("AZURE_AI_MODEL_DEPLOYMENT_NAME", "") == "", - reason="No real AZURE_AI_PROJECT_ENDPOINT or AZURE_AI_MODEL_DEPLOYMENT_NAME provided; skipping integration tests.", + or os.getenv("AZURE_AI_MODEL", "") == "", + reason="No real AZURE_AI_PROJECT_ENDPOINT or AZURE_AI_MODEL provided; skipping integration tests.", ) @@ -695,7 +694,7 @@ async def get_weather_tool(location: str) -> str: client = AzureOpenAIResponsesClient( project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + deployment_name=os.environ["AZURE_AI_MODEL"], credential=AzureCliCredential(), ) diff --git a/python/packages/core/tests/azure/test_entra_id_authentication.py b/python/packages/azure-ai/tests/azure_openai/test_entra_id_authentication.py similarity index 97% rename from python/packages/core/tests/azure/test_entra_id_authentication.py rename to python/packages/azure-ai/tests/azure_openai/test_entra_id_authentication.py index cfdde2e8df..a741459fd6 100644 --- a/python/packages/core/tests/azure/test_entra_id_authentication.py +++ b/python/packages/azure-ai/tests/azure_openai/test_entra_id_authentication.py @@ -3,13 +3,13 @@ from unittest.mock import MagicMock, patch import pytest +from agent_framework.exceptions import ChatClientInvalidAuthException from azure.core.credentials import TokenCredential from azure.core.credentials_async import AsyncTokenCredential -from agent_framework.azure._entra_id_authentication import ( +from agent_framework_azure_ai._entra_id_authentication import ( resolve_credential_to_token_provider, ) -from agent_framework.exceptions import ChatClientInvalidAuthException TOKEN_ENDPOINT = "https://cognitiveservices.azure.com/.default" diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index f0246f40b2..4e14558dba 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -24,7 +24,7 @@ tool, ) from agent_framework._settings import load_settings -from agent_framework.openai._responses_client import RawOpenAIResponsesClient +from agent_framework_openai._chat_client import RawOpenAIChatClient from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( ApproximateLocation, @@ -415,7 +415,7 @@ async def test_prepare_options_basic(mock_project_client: MagicMock) -> None: with ( patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={"model": "test-model"}, ), patch.object( @@ -452,7 +452,7 @@ async def test_prepare_options_with_application_endpoint( with ( patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={"model": "test-model"}, ), patch.object( @@ -494,7 +494,7 @@ async def test_prepare_options_with_application_project_client( with ( patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={"model": "test-model"}, ), patch.object( @@ -512,19 +512,6 @@ async def test_prepare_options_with_application_project_client( assert "extra_body" not in run_options -async def test_initialize_client(mock_project_client: MagicMock) -> None: - """Test _initialize_client method.""" - client = create_test_azure_ai_client(mock_project_client) - - mock_openai_client = MagicMock() - mock_project_client.get_openai_client = MagicMock(return_value=mock_openai_client) - - await client._initialize_client() - - assert client.client is mock_openai_client - mock_project_client.get_openai_client.assert_called_once() - - def test_update_agent_name_and_description(mock_project_client: MagicMock) -> None: """Test _update_agent_name_and_description method.""" client = create_test_azure_ai_client(mock_project_client) @@ -827,14 +814,14 @@ async def test_runtime_tools_override_logs_warning( messages = [Message(role="user", contents=[Content.from_text(text="Hello")])] with patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={"model": "test-model", "tools": [{"type": "function", "name": "tool_one"}]}, ): await client._prepare_options(messages, {}) with ( patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={"model": "test-model", "tools": [{"type": "function", "name": "tool_two"}]}, ), patch("agent_framework_azure_ai._client.logger.warning") as mock_warning, @@ -853,7 +840,7 @@ async def test_prepare_options_logs_warning_for_tools_with_existing_agent_versio with ( patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={"model": "test-model", "tools": [{"type": "function", "name": "tool_one"}]}, ), patch("agent_framework_azure_ai._client.logger.warning") as mock_warning, @@ -875,7 +862,7 @@ async def test_prepare_options_logs_warning_for_tools_on_application_endpoint( with ( patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={"model": "test-model", "tools": [{"type": "function", "name": "tool_one"}]}, ), patch.object(client, "_get_agent_reference_or_create", new_callable=AsyncMock) as mock_get_agent_reference, @@ -1101,14 +1088,14 @@ async def test_runtime_structured_output_override_logs_warning( messages = [Message(role="user", contents=[Content.from_text(text="Hello")])] with patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={"model": "test-model"}, ): await client._prepare_options(messages, {"response_format": ResponseFormatModel}) with ( patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={"model": "test-model"}, ), patch("agent_framework_azure_ai._client.logger.warning") as mock_warning, @@ -1129,7 +1116,7 @@ async def test_prepare_options_excludes_response_format( with ( patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={ "model": "test-model", "response_format": ResponseFormatModel, @@ -1164,7 +1151,7 @@ async def test_prepare_options_keeps_values_for_unsupported_option_keys( with ( patch( - "agent_framework.openai._responses_client.RawOpenAIResponsesClient._prepare_options", + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", return_value={ "model": "test-model", "tools": [{"type": "function", "name": "weather"}], @@ -2031,7 +2018,7 @@ async def test_inner_get_response_enriches_non_streaming(mock_project_client: Ma async def _fake_awaitable() -> ChatResponse: return base_response - with patch.object(RawOpenAIResponsesClient, "_inner_get_response", return_value=_fake_awaitable()): + with patch.object(RawOpenAIChatClient, "_inner_get_response", return_value=_fake_awaitable()): result_awaitable = client._inner_get_response(messages=[], options={}, stream=False) result = await result_awaitable # type: ignore[misc] @@ -2054,7 +2041,7 @@ async def test_inner_get_response_no_search_output_non_streaming(mock_project_cl async def _fake_awaitable() -> ChatResponse: return base_response - with patch.object(RawOpenAIResponsesClient, "_inner_get_response", return_value=_fake_awaitable()): + with patch.object(RawOpenAIChatClient, "_inner_get_response", return_value=_fake_awaitable()): result_awaitable = client._inner_get_response(messages=[], options={}, stream=False) result = await result_awaitable # type: ignore[misc] @@ -2075,7 +2062,7 @@ def test_inner_get_response_streaming_registers_hook(mock_project_client: MagicM mock_stream = _create_mock_stream() - with patch.object(RawOpenAIResponsesClient, "_inner_get_response", return_value=mock_stream): + with patch.object(RawOpenAIChatClient, "_inner_get_response", return_value=mock_stream): result = client._inner_get_response(messages=[], options={}, stream=True) assert result is mock_stream @@ -2088,7 +2075,7 @@ def test_streaming_hook_captures_search_urls(mock_project_client: MagicMock) -> mock_stream = _create_mock_stream() - with patch.object(RawOpenAIResponsesClient, "_inner_get_response", return_value=mock_stream): + with patch.object(RawOpenAIChatClient, "_inner_get_response", return_value=mock_stream): client._inner_get_response(messages=[], options={}, stream=True) hook = mock_stream._transform_hooks[0] @@ -2116,7 +2103,7 @@ def test_streaming_hook_enriches_url_citation(mock_project_client: MagicMock) -> mock_stream = _create_mock_stream() - with patch.object(RawOpenAIResponsesClient, "_inner_get_response", return_value=mock_stream): + with patch.object(RawOpenAIChatClient, "_inner_get_response", return_value=mock_stream): client._inner_get_response(messages=[], options={}, stream=True) hook = mock_stream._transform_hooks[0] diff --git a/python/packages/azure-ai/tests/test_foundry_agent.py b/python/packages/azure-ai/tests/test_foundry_agent.py new file mode 100644 index 0000000000..3031eb4b98 --- /dev/null +++ b/python/packages/azure-ai/tests/test_foundry_agent.py @@ -0,0 +1,344 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for FoundryAgentClient and FoundryAgent classes.""" + +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from agent_framework._tools import tool + + +class TestRawFoundryAgentChatClient: + """Tests for RawFoundryAgentChatClient.""" + + def test_init_requires_agent_name(self) -> None: + """Test that agent_name is required.""" + from agent_framework_azure_ai._foundry_agent_client import RawFoundryAgentChatClient + + with pytest.raises(ValueError, match="Agent name is required"): + RawFoundryAgentChatClient( + project_client=MagicMock(), + ) + + def test_init_with_agent_name(self) -> None: + """Test construction with agent_name and project_client.""" + from agent_framework_azure_ai._foundry_agent_client import RawFoundryAgentChatClient + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + agent_version="1.0", + ) + + assert client.agent_name == "test-agent" + assert client.agent_version == "1.0" + + def test_get_agent_reference_with_version(self) -> None: + """Test agent reference includes version when provided.""" + from agent_framework_azure_ai._foundry_agent_client import RawFoundryAgentChatClient + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="my-agent", + agent_version="2.0", + ) + + ref = client._get_agent_reference() + assert ref == {"name": "my-agent", "version": "2.0", "type": "agent_reference"} + + def test_get_agent_reference_without_version(self) -> None: + """Test agent reference omits version for HostedAgents.""" + from agent_framework_azure_ai._foundry_agent_client import RawFoundryAgentChatClient + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="hosted-agent", + ) + + ref = client._get_agent_reference() + assert ref == {"name": "hosted-agent", "type": "agent_reference"} + assert "version" not in ref + + async def test_prepare_options_validates_tools(self) -> None: + """Test that _prepare_options rejects non-FunctionTool objects.""" + from agent_framework import Message + + from agent_framework_azure_ai._foundry_agent_client import RawFoundryAgentChatClient + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + # A dict tool should be rejected + with pytest.raises(TypeError, match="Only FunctionTool objects are accepted"): + await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"tools": [{"type": "function", "function": {"name": "bad"}}]}, + ) + + async def test_prepare_options_accepts_function_tools(self) -> None: + """Test that _prepare_options accepts FunctionTool objects.""" + from agent_framework import Message + + from agent_framework_azure_ai._foundry_agent_client import RawFoundryAgentChatClient + + mock_project = MagicMock() + mock_openai = MagicMock() + mock_project.get_openai_client.return_value = mock_openai + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + @tool(approval_mode="never_require") + def my_func() -> str: + """A test function.""" + return "ok" + + # Should not raise — patch the parent's _prepare_options + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={}, + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"tools": [my_func]}, + ) + assert "extra_body" in result + assert result["extra_body"]["agent_reference"]["name"] == "test-agent" + + def test_check_model_presence_is_noop(self) -> None: + """Test that _check_model_presence does nothing (model is on service).""" + from agent_framework_azure_ai._foundry_agent_client import RawFoundryAgentChatClient + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + options: dict[str, Any] = {} + client._check_model_presence(options) + assert "model" not in options + + +class TestFoundryAgentChatClient: + """Tests for _FoundryAgentChatClient (full middleware).""" + + def test_init(self) -> None: + """Test construction of the full-middleware client.""" + from agent_framework_azure_ai._foundry_agent_client import _FoundryAgentChatClient + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = _FoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + agent_version="1.0", + ) + + assert client.agent_name == "test-agent" + + +class TestRawFoundryAgent: + """Tests for RawFoundryAgent.""" + + def test_init_creates_client(self) -> None: + """Test that RawFoundryAgent creates a client internally.""" + from agent_framework_azure_ai._foundry_agent import RawFoundryAgent + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + agent = RawFoundryAgent( + project_client=mock_project, + agent_name="test-agent", + agent_version="1.0", + ) + + assert agent.client is not None + assert agent.client.agent_name == "test-agent" + + def test_init_with_custom_client_type(self) -> None: + """Test that client_type parameter is respected.""" + from agent_framework_azure_ai._foundry_agent import RawFoundryAgent + from agent_framework_azure_ai._foundry_agent_client import RawFoundryAgentChatClient + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + agent = RawFoundryAgent( + project_client=mock_project, + agent_name="test-agent", + client_type=RawFoundryAgentChatClient, + ) + + assert isinstance(agent.client, RawFoundryAgentChatClient) + + def test_init_rejects_invalid_client_type(self) -> None: + """Test that invalid client_type raises TypeError.""" + from agent_framework_azure_ai._foundry_agent import RawFoundryAgent + + with pytest.raises(TypeError, match="must be a subclass of RawFoundryAgentChatClient"): + RawFoundryAgent( + project_client=MagicMock(), + agent_name="test-agent", + client_type=object, # type: ignore[arg-type] + ) + + def test_init_with_function_tools(self) -> None: + """Test that FunctionTool and callables are accepted.""" + from agent_framework_azure_ai._foundry_agent import RawFoundryAgent + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + @tool(approval_mode="never_require") + def my_func() -> str: + """A test function.""" + return "ok" + + agent = RawFoundryAgent( + project_client=mock_project, + agent_name="test-agent", + tools=[my_func], + ) + + assert agent.default_options.get("tools") is not None + + +class TestFoundryAgent: + """Tests for FoundryAgent (full middleware).""" + + def test_init(self) -> None: + """Test construction of the full-middleware agent.""" + from agent_framework_azure_ai._foundry_agent import FoundryAgent + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + agent = FoundryAgent( + project_client=mock_project, + agent_name="test-agent", + agent_version="1.0", + ) + + assert agent.client is not None + assert agent.client.agent_name == "test-agent" + + def test_init_with_middleware(self) -> None: + """Test that agent-level middleware is accepted.""" + from agent_framework import ChatContext, ChatMiddleware + + from agent_framework_azure_ai._foundry_agent import FoundryAgent + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + class MyMiddleware(ChatMiddleware): + async def process(self, context: ChatContext) -> None: + pass + + agent = FoundryAgent( + project_client=mock_project, + agent_name="test-agent", + middleware=[MyMiddleware()], + ) + + assert agent.client is not None + + +class TestFoundryChatClientToolMethods: + """Tests for RawFoundryChatClient tool factory methods.""" + + def test_get_code_interpreter_tool(self) -> None: + """Test code interpreter tool creation.""" + from agent_framework_azure_ai._foundry_chat_client import RawFoundryChatClient + + tool_obj = RawFoundryChatClient.get_code_interpreter_tool() + assert tool_obj is not None + + def test_get_code_interpreter_tool_with_file_ids(self) -> None: + """Test code interpreter tool with file IDs.""" + from agent_framework_azure_ai._foundry_chat_client import RawFoundryChatClient + + tool_obj = RawFoundryChatClient.get_code_interpreter_tool(file_ids=["file-abc123"]) + assert tool_obj is not None + + def test_get_file_search_tool(self) -> None: + """Test file search tool creation.""" + from agent_framework_azure_ai._foundry_chat_client import RawFoundryChatClient + + tool_obj = RawFoundryChatClient.get_file_search_tool(vector_store_ids=["vs_abc123"]) + assert tool_obj is not None + + def test_get_file_search_tool_requires_vector_store_ids(self) -> None: + """Test that empty vector_store_ids raises ValueError.""" + from agent_framework_azure_ai._foundry_chat_client import RawFoundryChatClient + + with pytest.raises(ValueError, match="vector_store_ids"): + RawFoundryChatClient.get_file_search_tool(vector_store_ids=[]) + + def test_get_web_search_tool(self) -> None: + """Test web search tool creation.""" + from agent_framework_azure_ai._foundry_chat_client import RawFoundryChatClient + + tool_obj = RawFoundryChatClient.get_web_search_tool() + assert tool_obj is not None + + def test_get_web_search_tool_with_location(self) -> None: + """Test web search tool with user location.""" + from agent_framework_azure_ai._foundry_chat_client import RawFoundryChatClient + + tool_obj = RawFoundryChatClient.get_web_search_tool( + user_location={"city": "Seattle", "country": "US"}, + search_context_size="high", + ) + assert tool_obj is not None + + def test_get_image_generation_tool(self) -> None: + """Test image generation tool creation.""" + from agent_framework_azure_ai._foundry_chat_client import RawFoundryChatClient + + tool_obj = RawFoundryChatClient.get_image_generation_tool() + assert tool_obj is not None + + def test_get_mcp_tool(self) -> None: + """Test MCP tool creation.""" + from agent_framework_azure_ai._foundry_chat_client import RawFoundryChatClient + + tool_obj = RawFoundryChatClient.get_mcp_tool( + name="my_mcp", + url="https://mcp.example.com", + ) + assert tool_obj is not None + + def test_get_mcp_tool_with_connection_id(self) -> None: + """Test MCP tool with project connection ID.""" + from agent_framework_azure_ai._foundry_chat_client import RawFoundryChatClient + + tool_obj = RawFoundryChatClient.get_mcp_tool( + name="github_mcp", + project_connection_id="conn_abc123", + description="GitHub MCP via Foundry", + ) + assert tool_obj is not None diff --git a/python/packages/azure-cosmos/agent_framework_azure_cosmos/_history_provider.py b/python/packages/azure-cosmos/agent_framework_azure_cosmos/_history_provider.py index 6d205fa378..6a61350a9c 100644 --- a/python/packages/azure-cosmos/agent_framework_azure_cosmos/_history_provider.py +++ b/python/packages/azure-cosmos/agent_framework_azure_cosmos/_history_provider.py @@ -13,10 +13,13 @@ from agent_framework import AGENT_FRAMEWORK_USER_AGENT, Message from agent_framework._sessions import BaseHistoryProvider from agent_framework._settings import SecretString, load_settings -from agent_framework.azure._entra_id_authentication import AzureCredentialTypes +from azure.core.credentials import TokenCredential +from azure.core.credentials_async import AsyncTokenCredential from azure.cosmos import PartitionKey from azure.cosmos.aio import ContainerProxy, CosmosClient, DatabaseProxy +AzureCredentialTypes = TokenCredential | AsyncTokenCredential + logger = logging.getLogger(__name__) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index a4e3a57330..14c3050952 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -1995,6 +1995,7 @@ def __init__( messages: Message | Sequence[Message] | None = None, response_id: str | None = None, conversation_id: str | None = None, + model: str | None = None, model_id: str | None = None, created_at: CreatedAtT | None = None, finish_reason: FinishReasonLiteral | FinishReason | None = None, @@ -2011,8 +2012,9 @@ def __init__( messages: A single Message or sequence of Message objects to include in the response. response_id: Optional ID of the chat response. conversation_id: Optional identifier for the state of the conversation. - model_id: Optional model ID used in the creation of the chat response. - created_at: Optional timestamp for the chat response. + model: Optional model used in the creation of the chat response. + model_id: Deprecated alias for ``model``. + created_at: Optional timestamp for when the response was created. finish_reason: Optional reason for the chat response (e.g., "stop", "length", "tool_calls"). usage_details: Optional usage details for the chat response. value: Optional value of the structured output. @@ -2022,6 +2024,8 @@ def __init__( additional_properties: Optional additional properties associated with the chat response. raw_representation: Optional raw representation of the chat response from an underlying implementation. """ + if model_id is not None and model is None: + model = model_id if messages is None: self.messages: list[Message] = [] elif isinstance(messages, Message): @@ -2039,7 +2043,7 @@ def __init__( self.messages = processed_messages self.response_id = response_id self.conversation_id = conversation_id - self.model_id = model_id + self.model = model self.created_at = created_at self.finish_reason = finish_reason self.usage_details = usage_details @@ -2052,6 +2056,15 @@ def __init__( self.continuation_token = continuation_token self.raw_representation: Any | list[Any] | None = raw_representation + @property + def model_id(self) -> str | None: + """Deprecated alias for :attr:`model`.""" + return self.model + + @model_id.setter + def model_id(self, value: str | None) -> None: + self.model = value + @overload @classmethod def from_updates( @@ -2249,6 +2262,7 @@ def __init__( response_id: str | None = None, message_id: str | None = None, conversation_id: str | None = None, + model: str | None = None, model_id: str | None = None, created_at: CreatedAtT | None = None, finish_reason: FinishReasonLiteral | FinishReason | None = None, @@ -2265,7 +2279,8 @@ def __init__( response_id: Optional ID of the response of which this update is a part. message_id: Optional ID of the message of which this update is a part. conversation_id: Optional identifier for the state of the conversation of which this update is a part - model_id: Optional model ID associated with this response update. + model: Optional model associated with this response update. + model_id: Deprecated alias for ``model``. created_at: Optional timestamp for the chat response update. finish_reason: Optional finish reason for the operation. continuation_token: Optional token for resuming a long-running background operation. @@ -2275,6 +2290,8 @@ def __init__( from an underlying implementation. """ + if model_id is not None and model is None: + model = model_id # Handle contents - support dict conversion for from_dict if contents is None: self.contents: list[Content] = [] @@ -2294,7 +2311,7 @@ def __init__( self.response_id = response_id self.message_id = message_id self.conversation_id = conversation_id - self.model_id = model_id + self.model = model self.created_at = created_at self.finish_reason = finish_reason self.continuation_token = continuation_token @@ -2304,6 +2321,15 @@ def __init__( ) self.raw_representation = raw_representation + @property + def model_id(self) -> str | None: + """Deprecated alias for :attr:`model`.""" + return self.model + + @model_id.setter + def model_id(self, value: str | None) -> None: + self.model = value + @property def text(self) -> str: """Returns the concatenated text of all contents in the update.""" @@ -3418,7 +3444,7 @@ class Embedding(Generic[EmbeddingT]): Args: vector: The embedding vector data. - model_id: The model used to generate this embedding. + model: The model used to generate this embedding. dimensions: Explicit dimension count (computed from vector length if omitted). created_at: Timestamp of when the embedding was generated. additional_properties: Additional metadata. @@ -3430,7 +3456,7 @@ class Embedding(Generic[EmbeddingT]): embedding = Embedding( vector=[0.1, 0.2, 0.3], - model_id="text-embedding-3-small", + model="text-embedding-3-small", ) assert embedding.dimensions == 3 """ @@ -3439,19 +3465,31 @@ def __init__( self, vector: EmbeddingT, *, + model: str | None = None, model_id: str | None = None, dimensions: int | None = None, created_at: datetime | None = None, additional_properties: dict[str, Any] | None = None, ) -> None: + if model_id is not None and model is None: + model = model_id self.vector = vector self._dimensions = dimensions - self.model_id = model_id + self.model = model self.created_at = created_at self.additional_properties = ( _restore_compaction_annotation_in_additional_properties(additional_properties) or {} ) + @property + def model_id(self) -> str | None: + """Deprecated alias for :attr:`model`.""" + return self.model + + @model_id.setter + def model_id(self, value: str | None) -> None: + self.model = value + @property def dimensions(self) -> int | None: """Return the number of dimensions in the embedding vector. diff --git a/python/packages/core/agent_framework/azure/__init__.py b/python/packages/core/agent_framework/azure/__init__.py index dcf9fc321e..49e6691888 100644 --- a/python/packages/core/agent_framework/azure/__init__.py +++ b/python/packages/core/agent_framework/azure/__init__.py @@ -2,16 +2,7 @@ """Azure integration namespace for optional Agent Framework connectors. -This module lazily re-exports objects from optional Azure connector packages and -built-in core Azure OpenAI modules. - -Supported classes include: -- AzureAIClient -- AzureAIAgentClient -- AzureOpenAIChatClient -- AzureOpenAIResponsesClient -- AzureAISearchContextProvider -- DurableAIAgent +This module lazily re-exports objects from optional Azure connector packages. """ import importlib @@ -30,18 +21,23 @@ "AzureAISearchSettings": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"), "AzureAISettings": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAIAgentsProvider": ("agent_framework_azure_ai", "agent-framework-azure-ai"), - "AzureCredentialTypes": ("agent_framework.azure._entra_id_authentication", "agent-framework-core"), - "AzureTokenProvider": ("agent_framework.azure._entra_id_authentication", "agent-framework-core"), + "AzureCredentialTypes": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureTokenProvider": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "FoundryChatClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "FoundryMemoryProvider": ("agent_framework_azure_ai", "agent-framework-azure-ai"), - "AzureOpenAIAssistantsClient": ("agent_framework.azure._assistants_client", "agent-framework-core"), - "AzureOpenAIAssistantsOptions": ("agent_framework.azure._assistants_client", "agent-framework-core"), - "AzureOpenAIChatClient": ("agent_framework.azure._chat_client", "agent-framework-core"), - "AzureOpenAIChatOptions": ("agent_framework.azure._chat_client", "agent-framework-core"), - "AzureOpenAIEmbeddingClient": ("agent_framework.azure._embedding_client", "agent-framework-core"), - "AzureOpenAIResponsesClient": ("agent_framework.azure._responses_client", "agent-framework-core"), - "AzureOpenAIResponsesOptions": ("agent_framework.azure._responses_client", "agent-framework-core"), - "AzureOpenAISettings": ("agent_framework.azure._shared", "agent-framework-core"), - "AzureUserSecurityContext": ("agent_framework.azure._chat_client", "agent-framework-core"), + "RawFoundryChatClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureOpenAIAssistantsClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureOpenAIAssistantsOptions": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureOpenAIChatClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureOpenAIChatOptions": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureOpenAIEmbeddingClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureOpenAIResponsesClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureOpenAIResponsesOptions": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureOpenAISettings": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureUserSecurityContext": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "FoundryAgent": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "RawFoundryAgentChatClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "RawFoundryAgent": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "DurableAIAgent": ("agent_framework_durabletask", "agent-framework-durabletask"), "DurableAIAgentClient": ("agent_framework_durabletask", "agent-framework-durabletask"), "DurableAIAgentOrchestrationContext": ("agent_framework_durabletask", "agent-framework-durabletask"), diff --git a/python/packages/core/agent_framework/azure/__init__.pyi b/python/packages/core/agent_framework/azure/__init__.pyi index 9f435efe40..8a0c1d19a7 100644 --- a/python/packages/core/agent_framework/azure/__init__.pyi +++ b/python/packages/core/agent_framework/azure/__init__.pyi @@ -1,54 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework_azure_ai import ( - AzureAIAgentClient, - AzureAIAgentsProvider, - AzureAIClient, - AzureAIProjectAgentOptions, - AzureAIProjectAgentProvider, - AzureAISettings, - FoundryMemoryProvider, -) -from agent_framework_azure_ai_search import AzureAISearchContextProvider, AzureAISearchSettings -from agent_framework_azurefunctions import AgentFunctionApp -from agent_framework_durabletask import ( - AgentCallbackContext, - AgentResponseCallbackProtocol, - DurableAIAgent, - DurableAIAgentClient, - DurableAIAgentOrchestrationContext, - DurableAIAgentWorker, -) +# This is a dynamic namespace — all symbols are lazily loaded from optional packages. +from typing import Any -from agent_framework.azure._assistants_client import AzureOpenAIAssistantsClient -from agent_framework.azure._chat_client import AzureOpenAIChatClient -from agent_framework.azure._embedding_client import AzureOpenAIEmbeddingClient -from agent_framework.azure._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider -from agent_framework.azure._responses_client import AzureOpenAIResponsesClient -from agent_framework.azure._shared import AzureOpenAISettings - -__all__ = [ - "AgentCallbackContext", - "AgentFunctionApp", - "AgentResponseCallbackProtocol", - "AzureAIAgentClient", - "AzureAIAgentsProvider", - "AzureAIClient", - "AzureAIProjectAgentOptions", - "AzureAIProjectAgentProvider", - "AzureAISearchContextProvider", - "AzureAISearchSettings", - "AzureAISettings", - "AzureCredentialTypes", - "AzureOpenAIAssistantsClient", - "AzureOpenAIChatClient", - "AzureOpenAIEmbeddingClient", - "AzureOpenAIResponsesClient", - "AzureOpenAISettings", - "AzureTokenProvider", - "DurableAIAgent", - "DurableAIAgentClient", - "DurableAIAgentOrchestrationContext", - "DurableAIAgentWorker", - "FoundryMemoryProvider", -] +def __getattr__(name: str) -> Any: ... # pyright: ignore[reportIncompleteStub] +def __dir__() -> list[str]: ... diff --git a/python/packages/core/agent_framework/azure/_assistants_client.py b/python/packages/core/agent_framework/azure/_assistants_client.py deleted file mode 100644 index aae89d562d..0000000000 --- a/python/packages/core/agent_framework/azure/_assistants_client.py +++ /dev/null @@ -1,194 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from __future__ import annotations - -import sys -from collections.abc import Mapping -from typing import Any, ClassVar, Generic - -from openai.lib.azure import AsyncAzureOpenAI - -from .._settings import load_settings -from ..openai import OpenAIAssistantsClient -from ..openai._assistants_client import OpenAIAssistantsOptions -from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider, resolve_credential_to_token_provider -from ._shared import AzureOpenAISettings, _apply_azure_defaults # pyright: ignore[reportPrivateUsage] - -if sys.version_info >= (3, 13): - from typing import TypeVar # type: ignore # pragma: no cover -else: - from typing_extensions import TypeVar # type: ignore # pragma: no cover -if sys.version_info >= (3, 11): - from typing import TypedDict # type: ignore # pragma: no cover -else: - from typing_extensions import TypedDict # type: ignore # pragma: no cover - - -# region Azure OpenAI Assistants Options TypedDict - - -AzureOpenAIAssistantsOptionsT = TypeVar( - "AzureOpenAIAssistantsOptionsT", - bound=TypedDict, # type: ignore[valid-type] - default="OpenAIAssistantsOptions", - covariant=True, -) - - -# endregion - - -class AzureOpenAIAssistantsClient( - OpenAIAssistantsClient[AzureOpenAIAssistantsOptionsT], Generic[AzureOpenAIAssistantsOptionsT] -): - """Azure OpenAI Assistants client.""" - - DEFAULT_AZURE_API_VERSION: ClassVar[str] = "2024-05-01-preview" - - def __init__( - self, - *, - deployment_name: str | None = None, - assistant_id: str | None = None, - assistant_name: str | None = None, - assistant_description: str | None = None, - thread_id: str | None = None, - api_key: str | None = None, - endpoint: str | None = None, - base_url: str | None = None, - api_version: str | None = None, - token_endpoint: str | None = None, - credential: AzureCredentialTypes | AzureTokenProvider | None = None, - default_headers: Mapping[str, str] | None = None, - async_client: AsyncAzureOpenAI | None = None, - env_file_path: str | None = None, - env_file_encoding: str | None = None, - ) -> None: - """Initialize an Azure OpenAI Assistants client. - - Keyword Args: - deployment_name: The Azure OpenAI deployment name for the model to use. - Can also be set via environment variable AZURE_OPENAI_CHAT_DEPLOYMENT_NAME. - assistant_id: The ID of an Azure OpenAI assistant to use. - If not provided, a new assistant will be created (and deleted after the request). - assistant_name: The name to use when creating new assistants. - assistant_description: The description to use when creating new assistants. - thread_id: Default thread ID to use for conversations. Can be overridden by - conversation_id property when making a request. - If not provided, a new thread will be created (and deleted after the request). - api_key: The API key to use. If provided will override the env vars or .env file value. - Can also be set via environment variable AZURE_OPENAI_API_KEY. - endpoint: The deployment endpoint. If provided will override the value - in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_ENDPOINT. - base_url: The deployment base URL. If provided will override the value - in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_BASE_URL. - api_version: The deployment API version. If provided will override the value - in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_API_VERSION. - token_endpoint: The token endpoint to request an Azure token. - Can also be set via environment variable AZURE_OPENAI_TOKEN_ENDPOINT. - credential: Azure credential or token provider for authentication. Accepts a - ``TokenCredential``, ``AsyncTokenCredential``, or a callable that returns a - bearer token string (sync or async), for example from - ``azure.identity.get_bearer_token_provider()``. - default_headers: The default headers mapping of string keys to - string values for HTTP requests. - async_client: An existing client to use. - env_file_path: Use the environment settings file as a fallback - to environment variables. - env_file_encoding: The encoding of the environment settings file. - - Examples: - .. code-block:: python - - from agent_framework.azure import AzureOpenAIAssistantsClient - - # Using environment variables - # Set AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com - # Set AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=gpt-4 - # Set AZURE_OPENAI_API_KEY=your-key - client = AzureOpenAIAssistantsClient() - - # Or passing parameters directly - client = AzureOpenAIAssistantsClient( - endpoint="https://your-endpoint.openai.azure.com", deployment_name="gpt-4", api_key="your-key" - ) - - # Or loading from a .env file - client = AzureOpenAIAssistantsClient(env_file_path="path/to/.env") - - # Using custom ChatOptions with type safety: - from typing import TypedDict - from agent_framework.azure import AzureOpenAIAssistantsOptions - - - class MyOptions(AzureOpenAIAssistantsOptions, total=False): - my_custom_option: str - - - client: AzureOpenAIAssistantsClient[MyOptions] = AzureOpenAIAssistantsClient() - response = await client.get_response("Hello", options={"my_custom_option": "value"}) - """ - azure_openai_settings = load_settings( - AzureOpenAISettings, - env_prefix="AZURE_OPENAI_", - api_key=api_key, - base_url=base_url, - endpoint=endpoint, - chat_deployment_name=deployment_name, - api_version=api_version, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - token_endpoint=token_endpoint, - ) - _apply_azure_defaults(azure_openai_settings, default_api_version=self.DEFAULT_AZURE_API_VERSION) - - chat_deployment_name = azure_openai_settings.get("chat_deployment_name") - if not chat_deployment_name: - raise ValueError( - "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " - "or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable." - ) - - api_key_secret = azure_openai_settings.get("api_key") - token_scope = azure_openai_settings.get("token_endpoint") - - # Resolve credential to token provider - ad_token_provider = None - if not async_client and not api_key_secret and credential: - ad_token_provider = resolve_credential_to_token_provider(credential, token_scope) - - if not async_client and not api_key_secret and not ad_token_provider: - raise ValueError("Please provide either api_key, credential, or a client.") - - # Create Azure client if not provided - if not async_client: - client_params: dict[str, Any] = { - "default_headers": default_headers, - } - if resolved_api_version := azure_openai_settings.get("api_version"): - client_params["api_version"] = resolved_api_version - - if api_key_secret: - client_params["api_key"] = api_key_secret.get_secret_value() - elif ad_token_provider: - client_params["azure_ad_token_provider"] = ad_token_provider - - if resolved_base_url := azure_openai_settings.get("base_url"): - client_params["base_url"] = str(resolved_base_url) - elif resolved_endpoint := azure_openai_settings.get("endpoint"): - client_params["azure_endpoint"] = str(resolved_endpoint) - - async_client = AsyncAzureOpenAI(**client_params) - - super().__init__( - model_id=chat_deployment_name, - assistant_id=assistant_id, - assistant_name=assistant_name, - assistant_description=assistant_description, - thread_id=thread_id, - async_client=async_client, # type: ignore[reportArgumentType] - default_headers=default_headers, - ) diff --git a/python/packages/core/agent_framework/azure/_chat_client.py b/python/packages/core/agent_framework/azure/_chat_client.py deleted file mode 100644 index ef598ebe21..0000000000 --- a/python/packages/core/agent_framework/azure/_chat_client.py +++ /dev/null @@ -1,349 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from __future__ import annotations - -import json -import logging -import sys -from collections.abc import Mapping, Sequence -from typing import TYPE_CHECKING, Any, Generic, cast - -from pydantic import BaseModel - -from agent_framework import ( - Annotation, - ChatMiddlewareLayer, - ChatResponse, - ChatResponseUpdate, - Content, - FunctionInvocationConfiguration, - FunctionInvocationLayer, -) -from agent_framework.observability import ChatTelemetryLayer -from agent_framework.openai._chat_client import OpenAIChatOptions, RawOpenAIChatClient - -from .._settings import load_settings -from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider -from ._shared import ( - AzureOpenAIConfigMixin, - AzureOpenAISettings, - _apply_azure_defaults, # pyright: ignore[reportPrivateUsage] -) - -if sys.version_info >= (3, 13): - from typing import TypeVar # type: ignore # pragma: no cover -else: - from typing_extensions import TypeVar # type: ignore # pragma: no cover -if sys.version_info >= (3, 12): - from typing import override # type: ignore # pragma: no cover -else: - from typing_extensions import override # type: ignore # pragma: no cover -if sys.version_info >= (3, 11): - from typing import TypedDict # type: ignore # pragma: no cover -else: - from typing_extensions import TypedDict # type: ignore # pragma: no cover - -if TYPE_CHECKING: - from openai.lib.azure import AsyncAzureOpenAI - from openai.types.chat.chat_completion import Choice - from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice - - from agent_framework._middleware import MiddlewareTypes - -logger: logging.Logger = logging.getLogger(__name__) - - -ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None) - - -# region Azure OpenAI Chat Options TypedDict - - -class AzureUserSecurityContext(TypedDict, total=False): - """User security context for Azure AI applications. - - These fields help security operations teams investigate and mitigate security - incidents by providing context about the application and end user. - - Learn more: https://learn.microsoft.com/azure/well-architected/service-guides/cosmos-db - """ - - application_name: str - """Name of the application making the request.""" - - end_user_id: str - """Unique identifier for the end user (recommend hashing username/email).""" - - end_user_tenant_id: str - """Microsoft 365 tenant ID the end user belongs to. Required for multi-tenant apps.""" - - source_ip: str - """The original client's IP address.""" - - -class AzureOpenAIChatOptions(OpenAIChatOptions[ResponseModelT], Generic[ResponseModelT], total=False): - """Azure OpenAI-specific chat options dict. - - Extends OpenAIChatOptions with Azure-specific options including - the "On Your Data" feature and enhanced security context. - - See: https://learn.microsoft.com/azure/ai-foundry/openai/reference-preview-latest - - Keys: - # Inherited from OpenAIChatOptions/ChatOptions: - model_id: The model to use for the request, - translates to ``model`` in Azure OpenAI API. - temperature: Sampling temperature between 0 and 2. - top_p: Nucleus sampling parameter. - max_tokens: Maximum number of tokens to generate, - translates to ``max_completion_tokens`` in Azure OpenAI API. - stop: Stop sequences. - seed: Random seed for reproducibility. - frequency_penalty: Frequency penalty between -2.0 and 2.0. - presence_penalty: Presence penalty between -2.0 and 2.0. - tools: List of tools (functions) available to the model. - tool_choice: How the model should use tools. - allow_multiple_tool_calls: Whether to allow parallel tool calls, - translates to ``parallel_tool_calls`` in Azure OpenAI API. - response_format: Structured output schema. - metadata: Request metadata for tracking. - user: End-user identifier for abuse monitoring. - store: Whether to store the conversation. - instructions: System instructions for the model. - logit_bias: Token bias values (-100 to 100). - logprobs: Whether to return log probabilities. - top_logprobs: Number of top log probabilities to return (0-20). - - # Azure-specific options: - data_sources: Azure "On Your Data" data sources configuration. - user_security_context: Enhanced security context for Azure Defender. - n: Number of chat completions to generate (not recommended, incurs costs). - """ - - # Azure-specific options - data_sources: list[dict[str, Any]] - """Azure "On Your Data" data sources for retrieval-augmented generation. - - Supported types: azure_search, azure_cosmos_db, elasticsearch, pinecone, mongo_db. - See: https://learn.microsoft.com/azure/ai-foundry/openai/references/on-your-data - """ - - user_security_context: AzureUserSecurityContext - """Enhanced security context for Azure Defender integration.""" - - n: int - """Number of chat completion choices to generate for each input message. - Note: You will be charged based on tokens across all choices. Keep n=1 to minimize costs.""" - - -AzureOpenAIChatOptionsT = TypeVar( - "AzureOpenAIChatOptionsT", - bound=TypedDict, # type: ignore[valid-type] - default="AzureOpenAIChatOptions", - covariant=True, -) - - -# endregion - -ChatResponseT = TypeVar("ChatResponseT", ChatResponse, ChatResponseUpdate) -AzureOpenAIChatClientT = TypeVar("AzureOpenAIChatClientT", bound="AzureOpenAIChatClient") - - -class AzureOpenAIChatClient( # type: ignore[misc] - AzureOpenAIConfigMixin, - FunctionInvocationLayer[AzureOpenAIChatOptionsT], - ChatMiddlewareLayer[AzureOpenAIChatOptionsT], - ChatTelemetryLayer[AzureOpenAIChatOptionsT], - RawOpenAIChatClient[AzureOpenAIChatOptionsT], - Generic[AzureOpenAIChatOptionsT], -): - """Azure OpenAI Chat completion class with middleware, telemetry, and function invocation support.""" - - def __init__( - self, - *, - api_key: str | None = None, - deployment_name: str | None = None, - endpoint: str | None = None, - base_url: str | None = None, - api_version: str | None = None, - token_endpoint: str | None = None, - credential: AzureCredentialTypes | AzureTokenProvider | None = None, - default_headers: Mapping[str, str] | None = None, - async_client: AsyncAzureOpenAI | None = None, - additional_properties: dict[str, Any] | None = None, - env_file_path: str | None = None, - env_file_encoding: str | None = None, - instruction_role: str | None = None, - middleware: Sequence[MiddlewareTypes] | None = None, - function_invocation_configuration: FunctionInvocationConfiguration | None = None, - ) -> None: - """Initialize an Azure OpenAI Chat completion client. - - Keyword Args: - api_key: The API key. If provided, will override the value in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_API_KEY. - deployment_name: The deployment name. If provided, will override the value - (chat_deployment_name) in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_CHAT_DEPLOYMENT_NAME. - endpoint: The deployment endpoint. If provided will override the value - in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_ENDPOINT. - base_url: The deployment base URL. If provided will override the value - in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_BASE_URL. - api_version: The deployment API version. If provided will override the value - in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_API_VERSION. - token_endpoint: The token endpoint to request an Azure token. - Can also be set via environment variable AZURE_OPENAI_TOKEN_ENDPOINT. - credential: Azure credential or token provider for authentication. Accepts a - ``TokenCredential``, ``AsyncTokenCredential``, or a callable that returns a - bearer token string (sync or async), for example from - ``azure.identity.get_bearer_token_provider()``. - default_headers: The default headers mapping of string keys to - string values for HTTP requests. - async_client: An existing client to use. - additional_properties: Additional properties stored on the client instance. - env_file_path: Use the environment settings file as a fallback to using env vars. - env_file_encoding: The encoding of the environment settings file, defaults to 'utf-8'. - instruction_role: The role to use for 'instruction' messages, for example, summarization - prompts could use `developer` or `system`. - middleware: Optional sequence of middleware to apply to requests. - function_invocation_configuration: Optional configuration for function invocation behavior. - - Examples: - .. code-block:: python - - from agent_framework.azure import AzureOpenAIChatClient - - # Using environment variables - # Set AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com - # Set AZURE_OPENAI_CHAT_DEPLOYMENT_NAME= - # Set AZURE_OPENAI_API_KEY=your-key - client = AzureOpenAIChatClient() - - # Or passing parameters directly - client = AzureOpenAIChatClient( - endpoint="https://your-endpoint.openai.azure.com", - deployment_name="", - api_key="your-key", - ) - - # Or loading from a .env file - client = AzureOpenAIChatClient(env_file_path="path/to/.env") - - # Using custom ChatOptions with type safety: - from typing import TypedDict - from agent_framework.azure import AzureOpenAIChatOptions - - - class MyOptions(AzureOpenAIChatOptions, total=False): - my_custom_option: str - - - client: AzureOpenAIChatClient[MyOptions] = AzureOpenAIChatClient() - response = await client.get_response("Hello", options={"my_custom_option": "value"}) - """ - azure_openai_settings = load_settings( - AzureOpenAISettings, - env_prefix="AZURE_OPENAI_", - api_key=api_key, - base_url=base_url, - endpoint=endpoint, - chat_deployment_name=deployment_name, - api_version=api_version, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - token_endpoint=token_endpoint, - ) - _apply_azure_defaults(azure_openai_settings) - - chat_deployment_name = azure_openai_settings.get("chat_deployment_name") - if not chat_deployment_name: - raise ValueError( - "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " - "or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable." - ) - - endpoint_value = azure_openai_settings.get("endpoint") - base_url_value = azure_openai_settings.get("base_url") - api_version_value = cast(str, azure_openai_settings.get("api_version")) - api_key_value = azure_openai_settings.get("api_key") - token_endpoint_value = azure_openai_settings.get("token_endpoint") - - super().__init__( - deployment_name=chat_deployment_name, - endpoint=endpoint_value, - base_url=base_url_value, - api_version=api_version_value, - api_key=api_key_value.get_secret_value() if api_key_value else None, - token_endpoint=token_endpoint_value, - credential=credential, - default_headers=default_headers, - client=async_client, - additional_properties=additional_properties, - instruction_role=instruction_role, - middleware=middleware, - function_invocation_configuration=function_invocation_configuration, - ) - - @override - def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> Content | None: - """Parse the choice into a Content object with type='text'. - - Overwritten from RawOpenAIChatClient to deal with Azure On Your Data function. - For docs see: - https://learn.microsoft.com/en-us/azure/ai-foundry/openai/references/on-your-data?tabs=python#context - """ - message = getattr(choice, "message", None) - if message is None: - message = getattr(choice, "delta", None) - # When you enable asynchronous content filtering in Azure OpenAI, you may receive empty deltas - if message is None: # type: ignore - return None - if hasattr(message, "refusal") and message.refusal: - return Content.from_text(text=message.refusal, raw_representation=choice) - if not message.content: - return None - text_content = Content.from_text(text=message.content, raw_representation=choice) - if not message.model_extra or "context" not in message.model_extra: - return text_content - - context_raw: object = cast(object, message.context) # type: ignore[union-attr] - if isinstance(context_raw, str): - try: - context_raw = json.loads(context_raw) - except json.JSONDecodeError: - logger.warning("Context is not a valid JSON string, ignoring context.") - return text_content - if not isinstance(context_raw, dict): - logger.warning("Context is not a valid dictionary, ignoring context.") - return text_content - context = cast(dict[str, Any], context_raw) - # `all_retrieved_documents` is currently not used, but can be retrieved - # through the raw_representation in the text content. - if intent := context.get("intent"): - text_content.additional_properties = {"intent": intent} - citations = context.get("citations") - if isinstance(citations, list) and citations: - annotations: list[Annotation] = [] - for citation_raw in cast(list[object], citations): - if not isinstance(citation_raw, dict): - continue - citation = cast(dict[str, Any], citation_raw) - annotations.append( - Annotation( - type="citation", - title=citation.get("title", ""), - url=citation.get("url", ""), - snippet=citation.get("content", ""), - file_id=citation.get("filepath", ""), - tool_name="Azure-on-your-Data", - additional_properties={"chunk_id": citation.get("chunk_id", "")}, - raw_representation=citation, - ) - ) - text_content.annotations = annotations - return text_content diff --git a/python/packages/core/agent_framework/azure/_embedding_client.py b/python/packages/core/agent_framework/azure/_embedding_client.py deleted file mode 100644 index 7003a4611f..0000000000 --- a/python/packages/core/agent_framework/azure/_embedding_client.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from __future__ import annotations - -import sys -from collections.abc import Mapping -from typing import Generic - -from openai.lib.azure import AsyncAzureOpenAI - -from agent_framework.observability import EmbeddingTelemetryLayer -from agent_framework.openai import OpenAIEmbeddingOptions -from agent_framework.openai._embedding_client import RawOpenAIEmbeddingClient - -from .._settings import load_settings -from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider -from ._shared import ( - AzureOpenAIConfigMixin, - AzureOpenAISettings, - _apply_azure_defaults, # pyright: ignore[reportPrivateUsage] -) - -if sys.version_info >= (3, 13): - from typing import TypeVar # type: ignore # pragma: no cover -else: - from typing_extensions import TypeVar # type: ignore # pragma: no cover -if sys.version_info >= (3, 11): - from typing import TypedDict # type: ignore # pragma: no cover -else: - from typing_extensions import TypedDict # type: ignore # pragma: no cover - - -AzureOpenAIEmbeddingOptionsT = TypeVar( - "AzureOpenAIEmbeddingOptionsT", - bound=TypedDict, # type: ignore[valid-type] - default="OpenAIEmbeddingOptions", - covariant=True, -) - - -class AzureOpenAIEmbeddingClient( - AzureOpenAIConfigMixin, - EmbeddingTelemetryLayer[str, list[float], AzureOpenAIEmbeddingOptionsT], - RawOpenAIEmbeddingClient[AzureOpenAIEmbeddingOptionsT], - Generic[AzureOpenAIEmbeddingOptionsT], -): - """Azure OpenAI embedding client with telemetry support. - - Keyword Args: - api_key: The API key. If provided, will override the value in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_API_KEY. - deployment_name: The deployment name. If provided, will override the value - (embedding_deployment_name) in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME. - endpoint: The deployment endpoint. - Can also be set via environment variable AZURE_OPENAI_ENDPOINT. - base_url: The deployment base URL. - Can also be set via environment variable AZURE_OPENAI_BASE_URL. - api_version: The deployment API version. - Can also be set via environment variable AZURE_OPENAI_API_VERSION. - token_endpoint: The token endpoint to request an Azure token. - Can also be set via environment variable AZURE_OPENAI_TOKEN_ENDPOINT. - credential: Azure credential or token provider for authentication. - default_headers: Default headers for HTTP requests. - async_client: An existing client to use. - env_file_path: Path to .env file for settings. - env_file_encoding: Encoding for .env file. - - Examples: - .. code-block:: python - - from agent_framework.azure import AzureOpenAIEmbeddingClient - - # Using environment variables - # Set AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com - # Set AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME=text-embedding-3-small - # Set AZURE_OPENAI_API_KEY=your-key - client = AzureOpenAIEmbeddingClient() - - # Or passing parameters directly - client = AzureOpenAIEmbeddingClient( - endpoint="https://your-endpoint.openai.azure.com", - deployment_name="text-embedding-3-small", - api_key="your-key", - ) - - result = await client.get_embeddings(["Hello, world!"]) - """ - - def __init__( - self, - *, - api_key: str | None = None, - deployment_name: str | None = None, - endpoint: str | None = None, - base_url: str | None = None, - api_version: str | None = None, - token_endpoint: str | None = None, - credential: AzureCredentialTypes | AzureTokenProvider | None = None, - default_headers: Mapping[str, str] | None = None, - async_client: AsyncAzureOpenAI | None = None, - otel_provider_name: str | None = None, - env_file_path: str | None = None, - env_file_encoding: str | None = None, - ) -> None: - """Initialize an Azure OpenAI embedding client.""" - azure_openai_settings = load_settings( - AzureOpenAISettings, - env_prefix="AZURE_OPENAI_", - api_key=api_key, - base_url=base_url, - endpoint=endpoint, - embedding_deployment_name=deployment_name, - api_version=api_version, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - token_endpoint=token_endpoint, - ) - _apply_azure_defaults(azure_openai_settings) - - embedding_deployment_name = azure_openai_settings.get("embedding_deployment_name") - if not embedding_deployment_name: - raise ValueError( - "Azure OpenAI embedding deployment name is required. Set via 'deployment_name' parameter " - "or 'AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME' environment variable." - ) - - api_key_secret = azure_openai_settings.get("api_key") - - super().__init__( - deployment_name=embedding_deployment_name, - endpoint=azure_openai_settings.get("endpoint"), - base_url=azure_openai_settings.get("base_url"), - api_version=azure_openai_settings.get("api_version") or "", - api_key=api_key_secret.get_secret_value() if api_key_secret else None, - token_endpoint=azure_openai_settings.get("token_endpoint"), - credential=credential, - default_headers=default_headers, - client=async_client, - otel_provider_name=otel_provider_name, - ) diff --git a/python/packages/core/agent_framework/azure/_responses_client.py b/python/packages/core/agent_framework/azure/_responses_client.py deleted file mode 100644 index 8387e49591..0000000000 --- a/python/packages/core/agent_framework/azure/_responses_client.py +++ /dev/null @@ -1,277 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from __future__ import annotations - -import sys -from collections.abc import Mapping, Sequence -from typing import TYPE_CHECKING, Any, Generic -from urllib.parse import urljoin, urlparse - -from azure.ai.projects.aio import AIProjectClient -from openai import AsyncOpenAI - -from .._middleware import ChatMiddlewareLayer -from .._settings import load_settings -from .._telemetry import AGENT_FRAMEWORK_USER_AGENT -from .._tools import FunctionInvocationConfiguration, FunctionInvocationLayer -from ..observability import ChatTelemetryLayer -from ..openai._responses_client import RawOpenAIResponsesClient -from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider -from ._shared import ( - AzureOpenAIConfigMixin, - AzureOpenAISettings, - _apply_azure_defaults, # pyright: ignore[reportPrivateUsage] -) - -if sys.version_info >= (3, 13): - from typing import TypeVar # type: ignore # pragma: no cover -else: - from typing_extensions import TypeVar # type: ignore # pragma: no cover -if sys.version_info >= (3, 12): - from typing import override # type: ignore # pragma: no cover -else: - from typing_extensions import override # type: ignore # pragma: no cover -if sys.version_info >= (3, 11): - from typing import TypedDict # type: ignore # pragma: no cover -else: - from typing_extensions import TypedDict # type: ignore # pragma: no cover - -if TYPE_CHECKING: - from .._middleware import MiddlewareTypes - from ..openai._responses_client import OpenAIResponsesOptions - - -AzureOpenAIResponsesOptionsT = TypeVar( - "AzureOpenAIResponsesOptionsT", - bound=TypedDict, # type: ignore[valid-type] - default="OpenAIResponsesOptions", - covariant=True, -) - - -class AzureOpenAIResponsesClient( # type: ignore[misc] - AzureOpenAIConfigMixin, - FunctionInvocationLayer[AzureOpenAIResponsesOptionsT], - ChatMiddlewareLayer[AzureOpenAIResponsesOptionsT], - ChatTelemetryLayer[AzureOpenAIResponsesOptionsT], - RawOpenAIResponsesClient[AzureOpenAIResponsesOptionsT], - Generic[AzureOpenAIResponsesOptionsT], -): - """Azure Responses completion class with middleware, telemetry, and function invocation support.""" - - def __init__( - self, - *, - api_key: str | None = None, - deployment_name: str | None = None, - endpoint: str | None = None, - base_url: str | None = None, - api_version: str | None = None, - token_endpoint: str | None = None, - credential: AzureCredentialTypes | AzureTokenProvider | None = None, - default_headers: Mapping[str, str] | None = None, - async_client: AsyncOpenAI | None = None, - project_client: Any | None = None, - project_endpoint: str | None = None, - allow_preview: bool | None = None, - env_file_path: str | None = None, - env_file_encoding: str | None = None, - instruction_role: str | None = None, - middleware: Sequence[MiddlewareTypes] | None = None, - function_invocation_configuration: FunctionInvocationConfiguration | None = None, - **kwargs: Any, - ) -> None: - """Initialize an Azure OpenAI Responses client. - - The client can be created in two ways: - - 1. **Direct Azure OpenAI** (default): Provide endpoint, api_key, or credential - to connect directly to an Azure OpenAI deployment. - 2. **Foundry project endpoint**: Provide a ``project_client`` or ``project_endpoint`` - (with ``credential``) to create the client via an Azure AI Foundry project. - This requires the ``azure-ai-projects`` package to be installed. - - Keyword Args: - api_key: The API key. If provided, will override the value in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_API_KEY. - deployment_name: The deployment name. If provided, will override the value - (responses_deployment_name) in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME. - endpoint: The deployment endpoint. If provided will override the value - in the env vars or .env file. - Can also be set via environment variable AZURE_OPENAI_ENDPOINT. - base_url: The deployment base URL. If provided will override the value - in the env vars or .env file. Currently, the base_url must end with "/openai/v1/". - Can also be set via environment variable AZURE_OPENAI_BASE_URL. - api_version: The deployment API version. If provided will override the value - in the env vars or .env file. Currently, the api_version must be "preview". - Can also be set via environment variable AZURE_OPENAI_API_VERSION. - token_endpoint: The token endpoint to request an Azure token. - Can also be set via environment variable AZURE_OPENAI_TOKEN_ENDPOINT. - credential: Azure credential or token provider for authentication. Accepts a - ``TokenCredential``, ``AsyncTokenCredential``, or a callable that returns a - bearer token string (sync or async), for example from - ``azure.identity.get_bearer_token_provider()``. - default_headers: The default headers mapping of string keys to - string values for HTTP requests. - async_client: An existing client to use. - project_client: An existing ``AIProjectClient`` (from ``azure.ai.projects.aio``) to use. - The OpenAI client will be obtained via ``project_client.get_openai_client()``. - Requires the ``azure-ai-projects`` package. - project_endpoint: The Azure AI Foundry project endpoint URL. - When provided with ``credential``, an ``AIProjectClient`` will be created - and used to obtain the OpenAI client. Requires the ``azure-ai-projects`` package. - allow_preview: Enables preview opt-in on internally-created ``AIProjectClient``. - env_file_path: Use the environment settings file as a fallback to using env vars. - env_file_encoding: The encoding of the environment settings file, defaults to 'utf-8'. - instruction_role: The role to use for 'instruction' messages, for example, summarization - prompts could use `developer` or `system`. - middleware: Optional sequence of middleware to apply to requests. - function_invocation_configuration: Optional configuration for function invocation behavior. - kwargs: Additional keyword arguments. - - Examples: - .. code-block:: python - - from agent_framework.azure import AzureOpenAIResponsesClient - - # Using environment variables - # Set AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com - # Set AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=gpt-4o - # Set AZURE_OPENAI_API_KEY=your-key - client = AzureOpenAIResponsesClient() - - # Or passing parameters directly - client = AzureOpenAIResponsesClient( - endpoint="https://your-endpoint.openai.azure.com", deployment_name="gpt-4o", api_key="your-key" - ) - - # Or loading from a .env file - client = AzureOpenAIResponsesClient(env_file_path="path/to/.env") - - # Using a Foundry project endpoint - from azure.identity import DefaultAzureCredential - - client = AzureOpenAIResponsesClient( - project_endpoint="https://your-project.services.ai.azure.com", - deployment_name="gpt-4o", - credential=DefaultAzureCredential(), - ) - - # Or using an existing AIProjectClient - from azure.ai.projects.aio import AIProjectClient - - project_client = AIProjectClient( - endpoint="https://your-project.services.ai.azure.com", - credential=DefaultAzureCredential(), - ) - client = AzureOpenAIResponsesClient( - project_client=project_client, - deployment_name="gpt-4o", - ) - - # Using custom ChatOptions with type safety: - from typing import TypedDict - from agent_framework.azure import AzureOpenAIResponsesOptions - - - class MyOptions(AzureOpenAIResponsesOptions, total=False): - my_custom_option: str - - - client: AzureOpenAIResponsesClient[MyOptions] = AzureOpenAIResponsesClient() - response = await client.get_response("Hello", options={"my_custom_option": "value"}) - """ - if (model_id := kwargs.pop("model_id", None)) and not deployment_name: - deployment_name = str(model_id) - - # Project client path: create OpenAI client from an Azure AI Foundry project - if async_client is None and (project_client is not None or project_endpoint is not None): - async_client = self._create_client_from_project( - project_client=project_client, - project_endpoint=project_endpoint, - credential=credential, - allow_preview=allow_preview, - ) - - azure_openai_settings = load_settings( - AzureOpenAISettings, - env_prefix="AZURE_OPENAI_", - api_key=api_key, - base_url=base_url, - endpoint=endpoint, - responses_deployment_name=deployment_name, - api_version=api_version, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - token_endpoint=token_endpoint, - ) - _apply_azure_defaults(azure_openai_settings, default_api_version="preview") - # TODO(peterychang): This is a temporary hack to ensure that the base_url is set correctly - # while this feature is in preview. - # But we should only do this if we're on azure. Private deployments may not need this. - endpoint_value = azure_openai_settings.get("endpoint") - if ( - not azure_openai_settings.get("base_url") - and endpoint_value - and (hostname := urlparse(str(endpoint_value)).hostname) - and hostname.endswith(".openai.azure.com") - ): - azure_openai_settings["base_url"] = urljoin(str(endpoint_value), "/openai/v1/") - - responses_deployment_name = azure_openai_settings.get("responses_deployment_name") - if not responses_deployment_name: - raise ValueError( - "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " - "or 'AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME' environment variable." - ) - - api_key_secret = azure_openai_settings.get("api_key") - - super().__init__( - deployment_name=responses_deployment_name, - endpoint=azure_openai_settings.get("endpoint"), - base_url=azure_openai_settings.get("base_url"), - api_version=azure_openai_settings.get("api_version") or "", - api_key=api_key_secret.get_secret_value() if api_key_secret else None, - token_endpoint=azure_openai_settings.get("token_endpoint"), - credential=credential, - default_headers=default_headers, - client=async_client, - instruction_role=instruction_role, - middleware=middleware, - function_invocation_configuration=function_invocation_configuration, - ) - - @staticmethod - def _create_client_from_project( - *, - project_client: AIProjectClient | None, - project_endpoint: str | None, - credential: AzureCredentialTypes | AzureTokenProvider | None, - allow_preview: bool | None = None, - ) -> AsyncOpenAI: - """Create an AsyncOpenAI client from an Azure AI Foundry project.""" - if project_client is not None: - return project_client.get_openai_client() - - if not project_endpoint: - raise ValueError("Azure AI project endpoint is required when project_client is not provided.") - if not credential: - raise ValueError("Azure credential is required when using project_endpoint without a project_client.") - project_client_kwargs: dict[str, Any] = { - "endpoint": project_endpoint, - "credential": credential, # type: ignore[arg-type] - "user_agent": AGENT_FRAMEWORK_USER_AGENT, - } - if allow_preview is not None: - project_client_kwargs["allow_preview"] = allow_preview - project_client = AIProjectClient(**project_client_kwargs) - return project_client.get_openai_client() - - @override - def _check_model_presence(self, options: dict[str, Any]) -> None: - if not options.get("model"): - if not self.model_id: - raise ValueError("deployment_name must be a non-empty string") - options["model"] = self.model_id diff --git a/python/packages/core/agent_framework/azure/_shared.py b/python/packages/core/agent_framework/azure/_shared.py deleted file mode 100644 index 5e06fbbe74..0000000000 --- a/python/packages/core/agent_framework/azure/_shared.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from __future__ import annotations - -import logging -import sys -from collections.abc import Mapping -from copy import copy -from typing import Any, ClassVar, Final - -from openai import AsyncOpenAI -from openai.lib.azure import AsyncAzureOpenAI - -from .._settings import SecretString -from .._telemetry import APP_INFO, prepend_agent_framework_to_user_agent -from ..openai._shared import OpenAIBase -from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider, resolve_credential_to_token_provider - -logger: logging.Logger = logging.getLogger(__name__) - -if sys.version_info >= (3, 11): - from typing import TypedDict # type: ignore # pragma: no cover -else: - from typing_extensions import TypedDict # type: ignore # pragma: no cover - - -DEFAULT_AZURE_API_VERSION: Final[str] = "2024-10-21" -DEFAULT_AZURE_TOKEN_ENDPOINT: Final[str] = "https://cognitiveservices.azure.com/.default" # noqa: S105 - - -class AzureOpenAISettings(TypedDict, total=False): - """AzureOpenAI model settings. - - Settings are resolved in this order: explicit keyword arguments, values from an - explicitly provided .env file, then environment variables with the prefix - 'AZURE_OPENAI_'. If settings are missing after resolution, validation will fail. - - Keyword Args: - endpoint: The endpoint of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal, the endpoint should end in openai.azure.com. - If both base_url and endpoint are supplied, base_url will be used. - Can be set via environment variable AZURE_OPENAI_ENDPOINT. - chat_deployment_name: The name of the Azure Chat deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure AI Foundry. - Can be set via environment variable AZURE_OPENAI_CHAT_DEPLOYMENT_NAME. - responses_deployment_name: The name of the Azure Responses deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure AI Foundry. - Can be set via environment variable AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME. - embedding_deployment_name: The name of the Azure Embedding deployment. - Can be set via environment variable AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME. - api_key: The API key for the Azure deployment. This value can be - found in the Keys & Endpoint section when examining your resource in - the Azure portal. You can use either KEY1 or KEY2. - Can be set via environment variable AZURE_OPENAI_API_KEY. - api_version: The API version to use. The default value is `DEFAULT_AZURE_API_VERSION`. - Can be set via environment variable AZURE_OPENAI_API_VERSION. - base_url: The url of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal, the base_url consists of the endpoint, - followed by /openai/deployments/{deployment_name}/, - use endpoint if you only want to supply the endpoint. - Can be set via environment variable AZURE_OPENAI_BASE_URL. - token_endpoint: The token endpoint to use to retrieve the authentication token. - The default value is `DEFAULT_AZURE_TOKEN_ENDPOINT`. - Can be set via environment variable AZURE_OPENAI_TOKEN_ENDPOINT. - - Examples: - .. code-block:: python - - from agent_framework.azure import AzureOpenAISettings - - # Using environment variables - # Set AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com - # Set AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=gpt-4 - # Set AZURE_OPENAI_API_KEY=your-key - settings = load_settings(AzureOpenAISettings, env_prefix="AZURE_OPENAI_") - - # Or passing parameters directly - settings = load_settings( - AzureOpenAISettings, - env_prefix="AZURE_OPENAI_", - endpoint="https://your-endpoint.openai.azure.com", - chat_deployment_name="gpt-4", - api_key="your-key", - ) - - # Or loading from a .env file - settings = load_settings(AzureOpenAISettings, env_prefix="AZURE_OPENAI_", env_file_path="path/to/.env") - """ - - chat_deployment_name: str | None - responses_deployment_name: str | None - embedding_deployment_name: str | None - endpoint: str | None - base_url: str | None - api_key: SecretString | None - api_version: str | None - token_endpoint: str | None - - -def _apply_azure_defaults( - settings: AzureOpenAISettings, - default_api_version: str = DEFAULT_AZURE_API_VERSION, - default_token_endpoint: str = DEFAULT_AZURE_TOKEN_ENDPOINT, -) -> None: - """Apply default values for api_version and token_endpoint after loading settings. - - Args: - settings: The loaded Azure OpenAI settings dict. - default_api_version: The default API version to use if not set. - default_token_endpoint: The default token endpoint to use if not set. - """ - if not settings.get("api_version"): - settings["api_version"] = default_api_version - if not settings.get("token_endpoint"): - settings["token_endpoint"] = default_token_endpoint - - -_AZURE_DEFAULTS_APPLIER = _apply_azure_defaults - - -class AzureOpenAIConfigMixin(OpenAIBase): - """Internal class for configuring a connection to an Azure OpenAI service.""" - - OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.openai" - # Note: INJECTABLE = {"client"} is inherited from OpenAIBase - - def __init__( - self, - deployment_name: str, - endpoint: str | None = None, - base_url: str | None = None, - api_version: str = DEFAULT_AZURE_API_VERSION, - api_key: str | None = None, - token_endpoint: str | None = None, - credential: AzureCredentialTypes | AzureTokenProvider | None = None, - default_headers: Mapping[str, str] | None = None, - client: AsyncOpenAI | None = None, - instruction_role: str | None = None, - **kwargs: Any, - ) -> None: - """Internal class for configuring a connection to an Azure OpenAI service. - - The `validate_call` decorator is used with a configuration that allows arbitrary types. - This is necessary for types like `str` and `OpenAIModelTypes`. - - Args: - deployment_name: Name of the deployment. - endpoint: The specific endpoint URL for the deployment. - base_url: The base URL for Azure services. - api_version: Azure API version. Defaults to the defined DEFAULT_AZURE_API_VERSION. - api_key: API key for Azure services. - token_endpoint: Azure AD token scope used to obtain a bearer token from a credential. - credential: Azure credential or token provider for authentication. Accepts a - ``TokenCredential``, ``AsyncTokenCredential``, or a callable that returns a - bearer token string (sync or async). - default_headers: Default headers for HTTP requests. - client: An existing client to use. - instruction_role: The role to use for 'instruction' messages, for example, summarization - prompts could use `developer` or `system`. - kwargs: Additional keyword arguments. - - """ - # Merge APP_INFO into the headers if it exists - merged_headers = dict(copy(default_headers)) if default_headers else {} - if APP_INFO: - merged_headers.update(APP_INFO) - merged_headers = prepend_agent_framework_to_user_agent(merged_headers) - if not client: - # Resolve credential to a token provider if needed - ad_token_provider = None - if not api_key and credential: - ad_token_provider = resolve_credential_to_token_provider(credential, token_endpoint) - - if not api_key and not ad_token_provider: - raise ValueError("Please provide either api_key, credential, or a client.") - - if not endpoint and not base_url: - raise ValueError("Please provide an endpoint or a base_url") - - args: dict[str, Any] = { - "default_headers": merged_headers, - } - if api_version: - args["api_version"] = api_version - if ad_token_provider: - args["azure_ad_token_provider"] = ad_token_provider - if api_key: - args["api_key"] = api_key - if base_url: - args["base_url"] = str(base_url) - if endpoint and not base_url: - args["azure_endpoint"] = str(endpoint) - if deployment_name: - args["azure_deployment"] = deployment_name - if "websocket_base_url" in kwargs: - args["websocket_base_url"] = kwargs.pop("websocket_base_url") - - client = AsyncAzureOpenAI(**args) - - # Store configuration as instance attributes for serialization - self.endpoint = str(endpoint) - self.base_url = str(base_url) - self.api_version = api_version - self.deployment_name = deployment_name - self.instruction_role = instruction_role - # Store default_headers but filter out USER_AGENT_KEY for serialization - if default_headers: - from .._telemetry import USER_AGENT_KEY - - def_headers = {k: v for k, v in default_headers.items() if k != USER_AGENT_KEY} - else: - def_headers = None - self.default_headers = def_headers - - super().__init__(model_id=deployment_name, client=client, **kwargs) diff --git a/python/packages/core/agent_framework/openai/__init__.py b/python/packages/core/agent_framework/openai/__init__.py index a3fe1fe8f6..90bc732bae 100644 --- a/python/packages/core/agent_framework/openai/__init__.py +++ b/python/packages/core/agent_framework/openai/__init__.py @@ -1,48 +1,55 @@ # Copyright (c) Microsoft. All rights reserved. -"""OpenAI namespace for built-in Agent Framework clients. +"""OpenAI namespace for Agent Framework clients. -This module re-exports objects from the core OpenAI implementation modules in -``agent_framework.openai``. +This module lazily re-exports objects from the ``agent-framework-openai`` package. +Install it with: ``pip install agent-framework-openai`` Supported classes include: -- OpenAIChatClient -- OpenAIResponsesClient -- OpenAIAssistantsClient -- OpenAIAssistantProvider +- OpenAIChatClient (Responses API) +- OpenAIChatCompletionClient (Chat Completions API) +- OpenAIEmbeddingClient +- OpenAIAssistantsClient (deprecated) """ -from ._assistant_provider import OpenAIAssistantProvider -from ._assistants_client import ( - AssistantToolResources, - OpenAIAssistantsClient, - OpenAIAssistantsOptions, -) -from ._chat_client import OpenAIChatClient, OpenAIChatOptions -from ._embedding_client import OpenAIEmbeddingClient, OpenAIEmbeddingOptions -from ._exceptions import ContentFilterResultSeverity, OpenAIContentFilterException -from ._responses_client import ( - OpenAIContinuationToken, - OpenAIResponsesClient, - OpenAIResponsesOptions, - RawOpenAIResponsesClient, -) -from ._shared import OpenAISettings - -__all__ = [ - "AssistantToolResources", - "ContentFilterResultSeverity", - "OpenAIAssistantProvider", - "OpenAIAssistantsClient", - "OpenAIAssistantsOptions", - "OpenAIChatClient", - "OpenAIChatOptions", - "OpenAIContentFilterException", - "OpenAIContinuationToken", - "OpenAIEmbeddingClient", - "OpenAIEmbeddingOptions", - "OpenAIResponsesClient", - "OpenAIResponsesOptions", - "OpenAISettings", - "RawOpenAIResponsesClient", -] +import importlib +from typing import Any + +_IMPORTS: dict[str, tuple[str, str]] = { + "OpenAIChatClient": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIChatOptions": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIContinuationToken": ("agent_framework_openai", "agent-framework-openai"), + "RawOpenAIChatClient": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIChatCompletionClient": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIChatCompletionOptions": ("agent_framework_openai", "agent-framework-openai"), + "RawOpenAIChatCompletionClient": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIEmbeddingClient": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIEmbeddingOptions": ("agent_framework_openai", "agent-framework-openai"), + "OpenAISettings": ("agent_framework_openai", "agent-framework-openai"), + "ContentFilterResultSeverity": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIContentFilterException": ("agent_framework_openai", "agent-framework-openai"), + "AssistantToolResources": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIAssistantProvider": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIAssistantsClient": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIAssistantsOptions": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIResponsesClient": ("agent_framework_openai", "agent-framework-openai"), + "OpenAIResponsesOptions": ("agent_framework_openai", "agent-framework-openai"), + "RawOpenAIResponsesClient": ("agent_framework_openai", "agent-framework-openai"), +} + + +def __getattr__(name: str) -> Any: + if name in _IMPORTS: + import_path, package_name = _IMPORTS[name] + try: + return getattr(importlib.import_module(import_path), name) + except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + f"The package {package_name} is required to use `{name}`. " + f"Please use `pip install {package_name}`, or update your requirements.txt or pyproject.toml file." + ) from exc + raise AttributeError(f"Module `openai` has no attribute {name}.") + + +def __dir__() -> list[str]: + return list(_IMPORTS.keys()) diff --git a/python/packages/core/agent_framework/openai/__init__.pyi b/python/packages/core/agent_framework/openai/__init__.pyi new file mode 100644 index 0000000000..d654b4d09e --- /dev/null +++ b/python/packages/core/agent_framework/openai/__init__.pyi @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft. All rights reserved. + +# This is a dynamic namespace — all symbols are lazily loaded from agent-framework-openai. +from typing import Any + +def __getattr__(name: str) -> Any: ... # pyright: ignore[reportIncompleteStub] +def __dir__() -> list[str]: ... diff --git a/python/packages/core/tests/core/test_agents.py b/python/packages/core/tests/core/test_agents.py index cab55196f8..c751265fbe 100644 --- a/python/packages/core/tests/core/test_agents.py +++ b/python/packages/core/tests/core/test_agents.py @@ -1950,13 +1950,13 @@ async def test_shared_local_storage_cross_provider_responses_history_does_not_le from openai.types.chat.chat_completion_message import ChatCompletionMessage from agent_framework._sessions import InMemoryHistoryProvider - from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient + from agent_framework.openai import OpenAIChatClient, OpenAIChatCompletionClient @tool(approval_mode="never_require") def search_hotels(city: str) -> str: return f"Found 3 hotels in {city}" - responses_client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + responses_client = OpenAIChatClient(model="test-model", api_key="test-key") responses_agent = Agent( client=responses_client, tools=[search_hotels], @@ -2024,7 +2024,7 @@ def search_hotels(city: str) -> str: responses_replay_call = next(item for item in responses_replay_input if item.get("type") == "function_call") assert responses_replay_call["id"] == "fc_provider123" - chat_client = OpenAIChatClient(model_id="test-model", api_key="test-key") + chat_client = OpenAIChatCompletionClient(model="test-model", api_key="test-key") chat_agent = Agent(client=chat_client) chat_response = ChatCompletion( diff --git a/python/packages/core/tests/core/test_clients.py b/python/packages/core/tests/core/test_clients.py index 258a31d73b..73526298df 100644 --- a/python/packages/core/tests/core/test_clients.py +++ b/python/packages/core/tests/core/test_clients.py @@ -66,10 +66,10 @@ def test_base_client_as_agent_uses_explicit_additional_properties(chat_client_ba assert agent.additional_properties == {"team": "core"} -def test_openai_chat_client_get_response_docstring_surfaces_layered_runtime_docs() -> None: - from agent_framework.openai import OpenAIChatClient +def test_openai_chat_completion_client_get_response_docstring_surfaces_layered_runtime_docs() -> None: + from agent_framework.openai import OpenAIChatCompletionClient - docstring = inspect.getdoc(OpenAIChatClient.get_response) + docstring = inspect.getdoc(OpenAIChatCompletionClient.get_response) assert docstring is not None assert "Get a response from a chat client." in docstring @@ -78,12 +78,12 @@ def test_openai_chat_client_get_response_docstring_surfaces_layered_runtime_docs assert "function_middleware: Optional per-call function middleware." not in docstring -def test_openai_chat_client_get_response_is_defined_on_openai_class() -> None: - from agent_framework.openai import OpenAIChatClient +def test_openai_chat_completion_client_get_response_is_defined_on_openai_class() -> None: + from agent_framework.openai import OpenAIChatCompletionClient - signature = inspect.signature(OpenAIChatClient.get_response) + signature = inspect.signature(OpenAIChatCompletionClient.get_response) - assert OpenAIChatClient.get_response.__qualname__ == "OpenAIChatClient.get_response" + assert OpenAIChatCompletionClient.get_response.__qualname__ == "OpenAIChatCompletionClient.get_response" assert "middleware" in signature.parameters @@ -349,15 +349,15 @@ def test_openai_responses_client_supports_all_tool_protocols(): assert isinstance(OpenAIResponsesClient, SupportsFileSearchTool) -def test_openai_chat_client_supports_web_search_only(): +def test_openai_chat_completion_client_supports_web_search_only(): """Test that OpenAIChatClient only supports web search tool.""" - from agent_framework.openai import OpenAIChatClient + from agent_framework.openai import OpenAIChatCompletionClient - assert not isinstance(OpenAIChatClient, SupportsCodeInterpreterTool) - assert isinstance(OpenAIChatClient, SupportsWebSearchTool) - assert not isinstance(OpenAIChatClient, SupportsImageGenerationTool) - assert not isinstance(OpenAIChatClient, SupportsMCPTool) - assert not isinstance(OpenAIChatClient, SupportsFileSearchTool) + assert not isinstance(OpenAIChatCompletionClient, SupportsCodeInterpreterTool) + assert isinstance(OpenAIChatCompletionClient, SupportsWebSearchTool) + assert not isinstance(OpenAIChatCompletionClient, SupportsImageGenerationTool) + assert not isinstance(OpenAIChatCompletionClient, SupportsMCPTool) + assert not isinstance(OpenAIChatCompletionClient, SupportsFileSearchTool) def test_openai_assistants_client_supports_code_interpreter_and_file_search(): diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 1cde898787..8e5a0dc7d8 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -1031,11 +1031,11 @@ def test_chat_tool_mode_from_dict(): def test_chat_options_init() -> None: """Test that ChatOptions can be created as a TypedDict.""" options: ChatOptions = {} - assert options.get("model_id") is None + assert options.get("model") is None # With values - options_with_model: ChatOptions = {"model_id": "gpt-4o", "temperature": 0.7} - assert options_with_model.get("model_id") == "gpt-4o" + options_with_model: ChatOptions = {"model": "gpt-4o", "temperature": 0.7} + assert options_with_model.get("model") == "gpt-4o" assert options_with_model.get("temperature") == 0.7 @@ -1069,18 +1069,18 @@ def test_chat_options_tool_choice_validation(): def test_chat_options_merge(tool_tool, ai_tool) -> None: """Test merge_chat_options utility function.""" options1: ChatOptions = { - "model_id": "gpt-4o", + "model": "gpt-4o", "tools": [tool_tool], "logit_bias": {"x": 1}, "metadata": {"a": "b"}, } - options2: ChatOptions = {"model_id": "gpt-4.1", "tools": [ai_tool]} + options2: ChatOptions = {"model": "gpt-4.1", "tools": [ai_tool]} assert options1 != options2 # Merge options - override takes precedence for non-collection fields options3 = merge_chat_options(options1, options2) - assert options3.get("model_id") == "gpt-4.1" + assert options3.get("model") == "gpt-4.1" assert options3.get("tools") == [tool_tool, ai_tool] # tools are combined assert options3.get("logit_bias") == {"x": 1} # base value preserved assert options3.get("metadata") == {"a": "b"} # base value preserved @@ -1089,7 +1089,7 @@ def test_chat_options_merge(tool_tool, ai_tool) -> None: def test_chat_options_and_tool_choice_override() -> None: """Test that tool_choice from other takes precedence in ChatOptions merge.""" # Agent-level defaults to "auto" - agent_options: ChatOptions = {"model_id": "gpt-4o", "tool_choice": "auto"} + agent_options: ChatOptions = {"model": "gpt-4o", "tool_choice": "auto"} # Run-level specifies "required" run_options: ChatOptions = {"tool_choice": "required"} @@ -1097,19 +1097,19 @@ def test_chat_options_and_tool_choice_override() -> None: # Run-level should override agent-level assert merged.get("tool_choice") == "required" - assert merged.get("model_id") == "gpt-4o" # Other fields preserved + assert merged.get("model") == "gpt-4o" # Other fields preserved def test_chat_options_and_tool_choice_none_in_other_uses_self() -> None: """Test that when other.tool_choice is None, self.tool_choice is used.""" agent_options: ChatOptions = {"tool_choice": "auto"} - run_options: ChatOptions = {"model_id": "gpt-4.1"} # tool_choice is None + run_options: ChatOptions = {"model": "gpt-4.1"} # tool_choice is None merged = merge_chat_options(agent_options, run_options) # Should keep agent-level tool_choice since run-level is None assert merged.get("tool_choice") == "auto" - assert merged.get("model_id") == "gpt-4.1" + assert merged.get("model") == "gpt-4.1" def test_chat_options_and_tool_choice_with_tool_mode() -> None: @@ -1845,7 +1845,7 @@ def test_chat_response_complex_serialization(): "output_token_count": 8, "total_token_count": 13, }, - "model_id": "gpt-4", # Test alias handling + "model": "gpt-4", # Test alias handling } response = ChatResponse.from_dict(response_data) @@ -1861,7 +1861,7 @@ def test_chat_response_complex_serialization(): assert isinstance(response_dict["messages"][0], dict) assert isinstance(response_dict["finish_reason"], str) # FinishReason serializes to string assert isinstance(response_dict["usage_details"], dict) - assert response_dict["model_id"] == "gpt-4" # Should serialize as model_id + assert response_dict["model"] == "gpt-4" # Should serialize as model_id def test_chat_response_update_all_content_types(): @@ -2309,7 +2309,7 @@ def test_chat_response_deepcopy_deep_copies_additional_properties(): "total_token_count": 30, }, "response_id": "resp-123", - "model_id": "gpt-4", + "model": "gpt-4", }, id="chat_response", ), diff --git a/python/packages/core/tests/openai/conftest.py b/python/packages/core/tests/openai/conftest.py deleted file mode 100644 index beb20ead82..0000000000 --- a/python/packages/core/tests/openai/conftest.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -from typing import Any - -from pytest import fixture - - -# region Connector Settings fixtures -@fixture -def exclude_list(request: Any) -> list[str]: - """Fixture that returns a list of environment variables to exclude.""" - return request.param if hasattr(request, "param") else [] - - -@fixture -def override_env_param_dict(request: Any) -> dict[str, str]: - """Fixture that returns a dict of environment variables to override.""" - return request.param if hasattr(request, "param") else {} - - -@fixture() -def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore - """Fixture to set environment variables for OpenAISettings.""" - - if exclude_list is None: - exclude_list = [] - - if override_env_param_dict is None: - override_env_param_dict = {} - - env_vars = { - "OPENAI_API_KEY": "test-dummy-key", - "OPENAI_ORG_ID": "test_org_id", - "OPENAI_RESPONSES_MODEL_ID": "test_responses_model_id", - "OPENAI_CHAT_MODEL_ID": "test_chat_model_id", - "OPENAI_TEXT_MODEL_ID": "test_text_model_id", - "OPENAI_EMBEDDING_MODEL_ID": "test_embedding_model_id", - "OPENAI_TEXT_TO_IMAGE_MODEL_ID": "test_text_to_image_model_id", - "OPENAI_AUDIO_TO_TEXT_MODEL_ID": "test_audio_to_text_model_id", - "OPENAI_TEXT_TO_AUDIO_MODEL_ID": "test_text_to_audio_model_id", - "OPENAI_REALTIME_MODEL_ID": "test_realtime_model_id", - } - - env_vars.update(override_env_param_dict) # type: ignore - - for key, value in env_vars.items(): - if key in exclude_list: - monkeypatch.delenv(key, raising=False) # type: ignore - continue - monkeypatch.setenv(key, value) # type: ignore - - return env_vars diff --git a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py index 2566d031aa..5f4941cd57 100644 --- a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py +++ b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py @@ -15,7 +15,7 @@ ) from agent_framework._settings import load_settings from agent_framework.observability import ChatTelemetryLayer -from agent_framework.openai._chat_client import RawOpenAIChatClient +from agent_framework_openai._chat_completion_client import RawOpenAIChatCompletionClient from foundry_local import FoundryLocalManager from foundry_local.models import DeviceType from openai import AsyncOpenAI @@ -126,21 +126,21 @@ class FoundryLocalSettings(TypedDict, total=False): (Env var FOUNDRY_LOCAL_MODEL_ID) """ - model_id: str | None + model: str | None class FoundryLocalClient( FunctionInvocationLayer[FoundryLocalChatOptionsT], ChatMiddlewareLayer[FoundryLocalChatOptionsT], ChatTelemetryLayer[FoundryLocalChatOptionsT], - RawOpenAIChatClient[FoundryLocalChatOptionsT], + RawOpenAIChatCompletionClient[FoundryLocalChatOptionsT], Generic[FoundryLocalChatOptionsT], ): """Foundry Local Chat completion class with middleware, telemetry, and function invocation support.""" def __init__( self, - model_id: str | None = None, + model: str | None = None, *, bootstrap: bool = True, timeout: float | None = None, @@ -155,7 +155,7 @@ def __init__( """Initialize a FoundryLocalClient. Keyword Args: - model_id: The Foundry Local model ID or alias to use. If not provided, + model: The Foundry Local model ID or alias to use. If not provided, it will be loaded from the FoundryLocalSettings. bootstrap: Whether to start the Foundry Local service if not already running. Default is True. @@ -242,25 +242,23 @@ class MyOptions(FoundryLocalChatOptions, total=False): settings = load_settings( FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_", - required_fields=["model_id"], - model_id=model_id, + required_fields=["model"], + model=model, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) - model_id_setting: str = settings["model_id"] # type: ignore[assignment] # pyright: ignore[reportTypedDictNotRequiredAccess] + model_setting: str = settings["model"] # type: ignore[assignment] # pyright: ignore[reportTypedDictNotRequiredAccess] manager = FoundryLocalManager(bootstrap=bootstrap, timeout=timeout) model_info = manager.get_model_info( - alias_or_model_id=model_id_setting, + alias_or_model_id=model_setting, device=device, ) if model_info is None: message = ( - f"Model with ID or alias '{model_id_setting}:{device.value}' not found in Foundry Local." + f"Model with ID or alias '{model_setting}:{device.value}' not found in Foundry Local." if device - else ( - f"Model with ID or alias '{model_id_setting}' for your current device not found in Foundry Local." - ) + else (f"Model with ID or alias '{model_setting}' for your current device not found in Foundry Local.") ) raise ValueError(message) if prepare_model: @@ -269,7 +267,7 @@ class MyOptions(FoundryLocalChatOptions, total=False): super().__init__( model_id=model_info.id, - client=AsyncOpenAI(base_url=manager.endpoint, api_key=manager.api_key), + async_client=AsyncOpenAI(base_url=manager.endpoint, api_key=manager.api_key), additional_properties=additional_properties, middleware=middleware, function_invocation_configuration=function_invocation_configuration, diff --git a/python/packages/foundry_local/tests/conftest.py b/python/packages/foundry_local/tests/conftest.py index 0afc223356..cd4356d0b5 100644 --- a/python/packages/foundry_local/tests/conftest.py +++ b/python/packages/foundry_local/tests/conftest.py @@ -27,7 +27,7 @@ def foundry_local_unit_test_env(monkeypatch: Any, exclude_list: list[str], overr override_env_param_dict = {} env_vars = { - "FOUNDRY_LOCAL_MODEL_ID": "test-model-id", + "FOUNDRY_LOCAL_MODEL": "test-model-id", } env_vars.update(override_env_param_dict) diff --git a/python/packages/foundry_local/tests/test_foundry_local_client.py b/python/packages/foundry_local/tests/test_foundry_local_client.py index ad2bf89c81..7da1120e4c 100644 --- a/python/packages/foundry_local/tests/test_foundry_local_client.py +++ b/python/packages/foundry_local/tests/test_foundry_local_client.py @@ -17,7 +17,7 @@ def test_foundry_local_settings_init_from_env(foundry_local_unit_test_env: dict[ """Test FoundryLocalSettings initialization from environment variables.""" settings = load_settings(FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_") - assert settings["model_id"] == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL_ID"] + assert settings["model"] == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL"] def test_foundry_local_settings_init_with_explicit_values() -> None: @@ -25,29 +25,29 @@ def test_foundry_local_settings_init_with_explicit_values() -> None: settings = load_settings( FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_", - model_id="custom-model-id", + model="custom-model-id", ) - assert settings["model_id"] == "custom-model-id" + assert settings["model"] == "custom-model-id" -@pytest.mark.parametrize("exclude_list", [["FOUNDRY_LOCAL_MODEL_ID"]], indirect=True) -def test_foundry_local_settings_missing_model_id(foundry_local_unit_test_env: dict[str, str]) -> None: +@pytest.mark.parametrize("exclude_list", [["FOUNDRY_LOCAL_MODEL"]], indirect=True) +def test_foundry_local_settings_missing_model(foundry_local_unit_test_env: dict[str, str]) -> None: """Test FoundryLocalSettings when model_id is missing raises error.""" - with pytest.raises(SettingNotFoundError, match="Required setting 'model_id'"): + with pytest.raises(SettingNotFoundError, match="Required setting 'model'"): load_settings( FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_", - required_fields=["model_id"], + required_fields=["model"], ) def test_foundry_local_settings_explicit_overrides_env(foundry_local_unit_test_env: dict[str, str]) -> None: """Test that explicit values override environment variables.""" - settings = load_settings(FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_", model_id="override-model-id") + settings = load_settings(FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_", model="override-model-id") - assert settings["model_id"] == "override-model-id" - assert settings["model_id"] != foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL_ID"] + assert settings["model"] == "override-model-id" + assert settings["model"] != foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL"] # Client Initialization Tests @@ -59,9 +59,9 @@ def test_foundry_local_client_init(mock_foundry_local_manager: MagicMock) -> Non "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ): - client = FoundryLocalClient(model_id="test-model-id") + client = FoundryLocalClient(model="test-model-id") - assert client.model_id == "test-model-id" + assert client.model == "test-model-id" assert client.manager is mock_foundry_local_manager assert isinstance(client, SupportsChatGetResponse) @@ -72,7 +72,7 @@ def test_foundry_local_client_init_with_bootstrap_false(mock_foundry_local_manag "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ) as mock_manager_class: - FoundryLocalClient(model_id="test-model-id", bootstrap=False) + FoundryLocalClient(model="test-model-id", bootstrap=False) mock_manager_class.assert_called_once_with( bootstrap=False, @@ -86,7 +86,7 @@ def test_foundry_local_client_init_with_timeout(mock_foundry_local_manager: Magi "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ) as mock_manager_class: - FoundryLocalClient(model_id="test-model-id", timeout=60.0) + FoundryLocalClient(model="test-model-id", timeout=60.0) mock_manager_class.assert_called_once_with( bootstrap=True, @@ -105,7 +105,7 @@ def test_foundry_local_client_init_model_not_found(mock_foundry_local_manager: M ), pytest.raises(ValueError, match="not found in Foundry Local"), ): - FoundryLocalClient(model_id="unknown-model") + FoundryLocalClient(model="unknown-model") def test_foundry_local_client_uses_model_info_id(mock_foundry_local_manager: MagicMock) -> None: @@ -118,9 +118,9 @@ def test_foundry_local_client_uses_model_info_id(mock_foundry_local_manager: Mag "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ): - client = FoundryLocalClient(model_id="model-alias") + client = FoundryLocalClient(model="model-alias") - assert client.model_id == "resolved-model-id" + assert client.model == "resolved-model-id" def test_foundry_local_client_init_from_env( @@ -133,7 +133,7 @@ def test_foundry_local_client_init_from_env( ): client = FoundryLocalClient() - assert client.model_id == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL_ID"] + assert client.model == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL"] def test_foundry_local_client_init_with_device(mock_foundry_local_manager: MagicMock) -> None: @@ -144,7 +144,7 @@ def test_foundry_local_client_init_with_device(mock_foundry_local_manager: Magic "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ): - FoundryLocalClient(model_id="test-model-id", device=DeviceType.CPU) + FoundryLocalClient(model="test-model-id", device=DeviceType.CPU) mock_foundry_local_manager.get_model_info.assert_called_once_with( alias_or_model_id="test-model-id", @@ -173,7 +173,7 @@ def test_foundry_local_client_init_model_not_found_with_device(mock_foundry_loca ), pytest.raises(ValueError, match="unknown-model:GPU.*not found"), ): - FoundryLocalClient(model_id="unknown-model", device=DeviceType.GPU) + FoundryLocalClient(model="unknown-model", device=DeviceType.GPU) def test_foundry_local_client_init_with_prepare_model_false(mock_foundry_local_manager: MagicMock) -> None: @@ -182,7 +182,7 @@ def test_foundry_local_client_init_with_prepare_model_false(mock_foundry_local_m "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ): - FoundryLocalClient(model_id="test-model-id", prepare_model=False) + FoundryLocalClient(model="test-model-id", prepare_model=False) mock_foundry_local_manager.download_model.assert_not_called() mock_foundry_local_manager.load_model.assert_not_called() @@ -194,7 +194,7 @@ def test_foundry_local_client_init_calls_download_and_load(mock_foundry_local_ma "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ): - FoundryLocalClient(model_id="test-model-id") + FoundryLocalClient(model="test-model-id") mock_foundry_local_manager.download_model.assert_called_once_with( alias_or_model_id="test-model-id", diff --git a/python/packages/lab/lightning/tests/test_lightning.py b/python/packages/lab/lightning/tests/test_lightning.py index d4f6d20adf..8f1602b566 100644 --- a/python/packages/lab/lightning/tests/test_lightning.py +++ b/python/packages/lab/lightning/tests/test_lightning.py @@ -8,7 +8,7 @@ import pytest from agent_framework import AgentExecutor, AgentResponse, Agent, WorkflowBuilder, Workflow -from agent_framework.openai import OpenAIChatClient +from agent_framework.openai import OpenAIChatCompletionClient from openai.types.chat import ChatCompletion, ChatCompletionMessage from openai.types.chat.chat_completion import Choice @@ -54,11 +54,11 @@ def workflow_two_agents(): "os.environ", { "OPENAI_API_KEY": "test-key", - "OPENAI_CHAT_MODEL_ID": "gpt-4o", + "OPENAI_MODEL": "gpt-4o", }, ): - first_chat_client = OpenAIChatClient() - second_chat_client = OpenAIChatClient() + first_chat_client = OpenAIChatCompletionClient() + second_chat_client = OpenAIChatCompletionClient() # Mock the OpenAI API calls with ( diff --git a/python/packages/openai/AGENTS.md b/python/packages/openai/AGENTS.md new file mode 100644 index 0000000000..d31506cf5d --- /dev/null +++ b/python/packages/openai/AGENTS.md @@ -0,0 +1,34 @@ +# AGENTS.md — agent-framework-openai + +OpenAI integration package for Agent Framework. Contains OpenAI Responses API and Chat Completions API clients. + +## Package Structure + +``` +agent_framework_openai/ +├── __init__.py # Public API exports +├── _chat_client.py # OpenAIChatClient (Responses API) + RawOpenAIChatClient +├── _chat_completion_client.py # OpenAIChatCompletionClient (Chat Completions API) + RawOpenAIChatCompletionClient +├── _embedding_client.py # OpenAIEmbeddingClient +├── _exceptions.py # OpenAI-specific exceptions +├── _shared.py # OpenAIBase, OpenAIConfigMixin, OpenAISettings +├── _assistants_client.py # OpenAIAssistantsClient (DEPRECATED) +└── _assistant_provider.py # OpenAIAssistantProvider (DEPRECATED) +``` + +## Key Classes + +| Class | API | Status | +|---|---|---| +| `OpenAIChatClient` | Responses API | Primary | +| `OpenAIChatCompletionClient` | Chat Completions API | Primary | +| `OpenAIEmbeddingClient` | Embeddings API | Primary | +| `OpenAIAssistantsClient` | Assistants API | Deprecated | + +All clients follow the Raw + Full-Featured pattern (e.g., `RawOpenAIChatClient` + `OpenAIChatClient`). + +## Dependencies + +- `agent-framework-core` — core abstractions +- `openai` — OpenAI Python SDK +- `packaging` — version checking diff --git a/python/packages/openai/LICENSE b/python/packages/openai/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/openai/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/packages/openai/README.md b/python/packages/openai/README.md new file mode 100644 index 0000000000..6ed4d20c03 --- /dev/null +++ b/python/packages/openai/README.md @@ -0,0 +1,17 @@ +# agent-framework-openai + +OpenAI integration for Microsoft Agent Framework. Provides chat clients for the OpenAI Responses API and Chat Completions API. + +## Installation + +```bash +pip install agent-framework-openai +``` + +## Usage + +```python +from agent_framework.openai import OpenAIChatClient + +client = OpenAIChatClient(model_id="gpt-4o") +``` diff --git a/python/packages/openai/agent_framework_openai/__init__.py b/python/packages/openai/agent_framework_openai/__init__.py new file mode 100644 index 0000000000..855dfb5f7a --- /dev/null +++ b/python/packages/openai/agent_framework_openai/__init__.py @@ -0,0 +1,87 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""OpenAI integration for Microsoft Agent Framework. + +This package provides OpenAI client implementations for the Agent Framework, +including clients for the Responses API and Chat Completions API. +""" + +import importlib.metadata +import sys + +if sys.version_info >= (3, 13): + from warnings import deprecated # type: ignore # pragma: no cover +else: + from typing_extensions import deprecated # type: ignore # pragma: no cover + +from ._assistant_provider import OpenAIAssistantProvider +from ._assistants_client import ( + AssistantToolResources, + OpenAIAssistantsClient, + OpenAIAssistantsOptions, +) +from ._chat_client import ( + OpenAIChatClient, + OpenAIChatOptions, + OpenAIContinuationToken, + RawOpenAIChatClient, +) +from ._chat_completion_client import ( + OpenAIChatCompletionClient, + OpenAIChatCompletionOptions, + RawOpenAIChatCompletionClient, +) +from ._embedding_client import OpenAIEmbeddingClient, OpenAIEmbeddingOptions +from ._exceptions import ContentFilterResultSeverity, OpenAIContentFilterException +from ._shared import OpenAISettings + +try: + __version__ = importlib.metadata.version("agent-framework-openai") +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" # Fallback for development mode + +# Deprecated aliases for old names — use subclasses so the warning only fires for the alias + + +@deprecated( + "OpenAIResponsesClient is deprecated, use OpenAIChatClient instead.", + category=DeprecationWarning, +) +class OpenAIResponsesClient(OpenAIChatClient): # type: ignore[misc] + """Deprecated alias for :class:`OpenAIChatClient`.""" + + +@deprecated( + "RawOpenAIResponsesClient is deprecated, use RawOpenAIChatClient instead.", + category=DeprecationWarning, +) +class RawOpenAIResponsesClient(RawOpenAIChatClient): # type: ignore[misc] + """Deprecated alias for :class:`RawOpenAIChatClient`.""" + + +OpenAIResponsesOptions = OpenAIChatOptions +"""Deprecated alias for :class:`OpenAIChatOptions`.""" + + +__all__ = [ + "AssistantToolResources", + "ContentFilterResultSeverity", + "OpenAIAssistantProvider", + "OpenAIAssistantsClient", + "OpenAIAssistantsOptions", + "OpenAIChatClient", + "OpenAIChatCompletionClient", + "OpenAIChatCompletionOptions", + "OpenAIChatOptions", + "OpenAIContentFilterException", + "OpenAIContinuationToken", + "OpenAIEmbeddingClient", + "OpenAIEmbeddingOptions", + "OpenAIResponsesClient", + "OpenAIResponsesOptions", + "OpenAISettings", + "RawOpenAIChatClient", + "RawOpenAIChatCompletionClient", + "RawOpenAIResponsesClient", + "__version__", +] diff --git a/python/packages/core/agent_framework/openai/_assistant_provider.py b/python/packages/openai/agent_framework_openai/_assistant_provider.py similarity index 98% rename from python/packages/core/agent_framework/openai/_assistant_provider.py rename to python/packages/openai/agent_framework_openai/_assistant_provider.py index 9746725128..f0b88e1761 100644 --- a/python/packages/core/agent_framework/openai/_assistant_provider.py +++ b/python/packages/openai/agent_framework_openai/_assistant_provider.py @@ -6,16 +6,15 @@ from collections.abc import Awaitable, Callable, Mapping, MutableMapping, Sequence from typing import TYPE_CHECKING, Any, Generic, cast +from agent_framework._agents import Agent +from agent_framework._middleware import MiddlewareTypes +from agent_framework._sessions import BaseContextProvider +from agent_framework._settings import SecretString, load_settings +from agent_framework._tools import FunctionTool, ToolTypes, normalize_tools from openai import AsyncOpenAI from openai.types.beta.assistant import Assistant from pydantic import BaseModel -from agent_framework._settings import SecretString, load_settings - -from .._agents import Agent -from .._middleware import MiddlewareTypes -from .._sessions import BaseContextProvider -from .._tools import FunctionTool, ToolTypes, normalize_tools from ._assistants_client import OpenAIAssistantsClient from ._shared import OpenAISettings, from_assistant_tools, to_assistant_tools @@ -540,7 +539,7 @@ def _create_chat_agent_from_assistant( """ # Create the chat client with the assistant client = OpenAIAssistantsClient( - model_id=assistant.model, + model=assistant.model, assistant_id=assistant.id, assistant_name=assistant.name, assistant_description=assistant.description, diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/openai/agent_framework_openai/_assistants_client.py similarity index 96% rename from python/packages/core/agent_framework/openai/_assistants_client.py rename to python/packages/openai/agent_framework_openai/_assistants_client.py index 9179fb4a8c..f5755d8640 100644 --- a/python/packages/core/agent_framework/openai/_assistants_client.py +++ b/python/packages/openai/agent_framework_openai/_assistants_client.py @@ -15,6 +15,27 @@ ) from typing import TYPE_CHECKING, Any, Generic, Literal, TypedDict, cast +from agent_framework._clients import BaseChatClient +from agent_framework._middleware import ChatMiddlewareLayer +from agent_framework._settings import load_settings +from agent_framework._tools import ( + FunctionInvocationConfiguration, + FunctionInvocationLayer, + FunctionTool, + normalize_tools, +) +from agent_framework._types import ( + Annotation, + ChatOptions, + ChatResponse, + ChatResponseUpdate, + Content, + Message, + ResponseStream, + TextSpanRegion, + UsageDetails, +) +from agent_framework.observability import ChatTelemetryLayer from openai import AsyncOpenAI from openai.types.beta.threads import ( FileCitationAnnotation, @@ -37,27 +58,6 @@ from openai.types.beta.threads.runs import RunStep from pydantic import BaseModel -from .._clients import BaseChatClient -from .._middleware import ChatMiddlewareLayer -from .._settings import load_settings -from .._tools import ( - FunctionInvocationConfiguration, - FunctionInvocationLayer, - FunctionTool, - normalize_tools, -) -from .._types import ( - Annotation, - ChatOptions, - ChatResponse, - ChatResponseUpdate, - Content, - Message, - ResponseStream, - TextSpanRegion, - UsageDetails, -) -from ..observability import ChatTelemetryLayer from ._shared import OpenAIConfigMixin, OpenAISettings if sys.version_info >= (3, 13): @@ -76,7 +76,7 @@ from typing_extensions import Self, TypedDict # type: ignore # pragma: no cover if TYPE_CHECKING: - from .._middleware import MiddlewareTypes + from agent_framework._middleware import MiddlewareTypes logger = logging.getLogger("agent_framework.openai") @@ -123,7 +123,7 @@ class OpenAIAssistantsOptions(ChatOptions[ResponseModelT], Generic[ResponseModel Keys: # Inherited from ChatOptions: - model_id: The model to use for the assistant, + model_id: Deprecated. The model to use for the assistant, translates to ``model`` in OpenAI API. temperature: Sampling temperature between 0 and 2. top_p: Nucleus sampling parameter. @@ -191,7 +191,7 @@ class OpenAIAssistantsOptions(ChatOptions[ResponseModelT], Generic[ResponseModel ASSISTANTS_OPTION_TRANSLATIONS: dict[str, str] = { - "model_id": "model", + "model_id": "model", # backward compat: accept model_id in options "max_tokens": "max_completion_tokens", "allow_multiple_tool_calls": "parallel_tool_calls", } @@ -277,6 +277,7 @@ def get_file_search_tool( def __init__( self, *, + model: str | None = None, model_id: str | None = None, assistant_id: str | None = None, assistant_name: str | None = None, @@ -296,8 +297,9 @@ def __init__( """Initialize an OpenAI Assistants client. Keyword Args: - model_id: OpenAI model name, see https://platform.openai.com/docs/models. - Can also be set via environment variable OPENAI_CHAT_MODEL_ID. + model: OpenAI model name, see https://platform.openai.com/docs/models. + Can also be set via environment variable OPENAI_MODEL. + model_id: Deprecated alias for ``model``. assistant_id: The ID of an OpenAI assistant to use. If not provided, a new assistant will be created (and deleted after the request). assistant_name: The name to use when creating new assistants. @@ -328,11 +330,11 @@ def __init__( # Using environment variables # Set OPENAI_API_KEY=sk-... - # Set OPENAI_CHAT_MODEL_ID=gpt-4 + # Set OPENAI_MODEL=gpt-4 client = OpenAIAssistantsClient() # Or passing parameters directly - client = OpenAIAssistantsClient(model_id="gpt-4", api_key="sk-...") + client = OpenAIAssistantsClient(model="gpt-4", api_key="sk-...") # Or loading from a .env file client = OpenAIAssistantsClient(env_file_path="path/to/.env") @@ -346,16 +348,21 @@ class MyOptions(OpenAIAssistantsOptions, total=False): my_custom_option: str - client: OpenAIAssistantsClient[MyOptions] = OpenAIAssistantsClient(model_id="gpt-4") + client: OpenAIAssistantsClient[MyOptions] = OpenAIAssistantsClient(model="gpt-4") response = await client.get_response("Hello", options={"my_custom_option": "value"}) """ + if model_id is not None and model is None: + import warnings + + warnings.warn("model_id is deprecated, use model instead", DeprecationWarning, stacklevel=2) + model = model_id openai_settings = load_settings( OpenAISettings, env_prefix="OPENAI_", api_key=api_key, base_url=base_url, org_id=org_id, - chat_model_id=model_id, + model=model, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) @@ -366,15 +373,14 @@ class MyOptions(OpenAIAssistantsOptions, total=False): "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) - chat_model_id = openai_settings.get("chat_model_id") - if not chat_model_id: + resolved_model = openai_settings.get("model") + if not resolved_model: raise ValueError( - "OpenAI model ID is required. " - "Set via 'model_id' parameter or 'OPENAI_CHAT_MODEL_ID' environment variable." + "OpenAI model is required. Set via 'model' parameter or 'OPENAI_MODEL' environment variable." ) super().__init__( - model_id=chat_model_id, + model=resolved_model, api_key=self._get_api_key(api_key_value), org_id=openai_settings.get("org_id"), default_headers=default_headers, @@ -465,12 +471,12 @@ async def _get_assistant_id_or_create(self) -> str: """ # If no assistant is provided, create a temporary assistant if self.assistant_id is None: - if not self.model_id: - raise ValueError("Parameter 'model_id' is required for assistant creation.") + if not self.model: + raise ValueError("Parameter 'model' is required for assistant creation.") client = await self._ensure_client() created_assistant = await client.beta.assistants.create( # type: ignore[reportDeprecated] - model=self.model_id, + model=self.model, description=self.assistant_description, name=self.assistant_name, ) @@ -781,13 +787,13 @@ def _prepare_options( options: Mapping[str, Any], **kwargs: Any, ) -> tuple[dict[str, Any], list[Content] | None]: - from .._types import validate_tool_mode + from agent_framework._types import validate_tool_mode run_options: dict[str, Any] = {**kwargs} # Extract options from the dict max_tokens = options.get("max_tokens") - model_id = options.get("model_id") + model = options.get("model") or options.get("model_id") # backward compat top_p = options.get("top_p") temperature = options.get("temperature") allow_multiple_tool_calls = options.get("allow_multiple_tool_calls") @@ -798,8 +804,8 @@ def _prepare_options( if max_tokens is not None: run_options["max_completion_tokens"] = max_tokens - if model_id is not None: - run_options["model"] = model_id + if model is not None: + run_options["model"] = model if top_p is not None: run_options["top_p"] = top_p if temperature is not None: diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py similarity index 92% rename from python/packages/core/agent_framework/openai/_responses_client.py rename to python/packages/openai/agent_framework_openai/_chat_client.py index 0c57dffb39..d25234f305 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -14,6 +14,7 @@ MutableMapping, Sequence, ) +from copy import copy from datetime import datetime, timezone from itertools import chain from typing import ( @@ -27,31 +28,11 @@ cast, ) -from openai import AsyncOpenAI, BadRequestError -from openai.types.responses import FunctionShellTool -from openai.types.responses.file_search_tool_param import FileSearchToolParam -from openai.types.responses.function_tool_param import FunctionToolParam -from openai.types.responses.parsed_response import ( - ParsedResponse, -) -from openai.types.responses.response import Response as OpenAIResponse -from openai.types.responses.response_stream_event import ( - ResponseStreamEvent as OpenAIResponseStreamEvent, -) -from openai.types.responses.response_usage import ResponseUsage -from openai.types.responses.tool_param import ( - CodeInterpreter, - CodeInterpreterContainerCodeInterpreterToolAuto, - ImageGeneration, - Mcp, -) -from openai.types.responses.web_search_tool_param import WebSearchToolParam -from pydantic import BaseModel - -from .._clients import BaseChatClient -from .._middleware import ChatMiddlewareLayer -from .._settings import load_settings -from .._tools import ( +from agent_framework._clients import BaseChatClient +from agent_framework._middleware import ChatMiddlewareLayer +from agent_framework._settings import SecretString, load_settings +from agent_framework._telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent +from agent_framework._tools import ( SHELL_TOOL_KIND_VALUE, FunctionInvocationConfiguration, FunctionInvocationLayer, @@ -60,7 +41,7 @@ normalize_tools, tool, ) -from .._types import ( +from agent_framework._types import ( Annotation, ChatOptions, ChatResponse, @@ -76,13 +57,34 @@ prepend_instructions_to_messages, validate_tool_mode, ) -from ..exceptions import ( +from agent_framework.exceptions import ( ChatClientException, ChatClientInvalidRequestException, ) -from ..observability import ChatTelemetryLayer +from agent_framework.observability import ChatTelemetryLayer +from openai import AsyncOpenAI, BadRequestError +from openai.types.responses import FunctionShellTool +from openai.types.responses.file_search_tool_param import FileSearchToolParam +from openai.types.responses.function_tool_param import FunctionToolParam +from openai.types.responses.parsed_response import ( + ParsedResponse, +) +from openai.types.responses.response import Response as OpenAIResponse +from openai.types.responses.response_stream_event import ( + ResponseStreamEvent as OpenAIResponseStreamEvent, +) +from openai.types.responses.response_usage import ResponseUsage +from openai.types.responses.tool_param import ( + CodeInterpreter, + CodeInterpreterContainerCodeInterpreterToolAuto, + ImageGeneration, + Mcp, +) +from openai.types.responses.web_search_tool_param import WebSearchToolParam +from pydantic import BaseModel + from ._exceptions import OpenAIContentFilterException -from ._shared import OpenAIBase, OpenAIConfigMixin, OpenAISettings +from ._shared import OpenAISettings, get_api_key if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover @@ -98,7 +100,7 @@ from typing_extensions import TypedDict # type: ignore # pragma: no cover if TYPE_CHECKING: - from .._middleware import ( + from agent_framework._middleware import ( ChatMiddleware, ChatMiddlewareCallable, FunctionMiddleware, @@ -147,7 +149,7 @@ class StreamOptions(TypedDict, total=False): ResponseFormatT = TypeVar("ResponseFormatT", bound=BaseModel | None, default=None) -class OpenAIResponsesOptions(ChatOptions[ResponseFormatT], Generic[ResponseFormatT], total=False): +class OpenAIChatOptions(ChatOptions[ResponseFormatT], Generic[ResponseFormatT], total=False): """OpenAI Responses API-specific chat options. Extends ChatOptions with options specific to OpenAI's Responses API. @@ -222,10 +224,10 @@ class OpenAIResponsesOptions(ChatOptions[ResponseFormatT], Generic[ResponseForma completion or resume a streaming response.""" -OpenAIResponsesOptionsT = TypeVar( - "OpenAIResponsesOptionsT", +OpenAIChatOptionsT = TypeVar( + "OpenAIChatOptionsT", bound=TypedDict, # type: ignore[valid-type] - default="OpenAIResponsesOptions", + default="OpenAIChatOptions", covariant=True, ) @@ -236,10 +238,9 @@ class OpenAIResponsesOptions(ChatOptions[ResponseFormatT], Generic[ResponseForma # region ResponsesClient -class RawOpenAIResponsesClient( # type: ignore[misc] - OpenAIBase, - BaseChatClient[OpenAIResponsesOptionsT], - Generic[OpenAIResponsesOptionsT], +class RawOpenAIChatClient( # type: ignore[misc] + BaseChatClient[OpenAIChatOptionsT], + Generic[OpenAIChatOptionsT], ): """Raw OpenAI Responses client without middleware, telemetry, or function invocation. @@ -253,13 +254,108 @@ class RawOpenAIResponsesClient( # type: ignore[misc] 2. **ChatMiddlewareLayer** - Applies chat middleware per model call and stays outside telemetry 3. **ChatTelemetryLayer** - Must stay inside chat middleware for correct per-call telemetry - Use ``OpenAIResponsesClient`` instead for a fully-featured client with all layers applied. + Use ``OpenAIChatClient`` instead for a fully-featured client with all layers applied. """ + INJECTABLE: ClassVar[set[str]] = {"client"} STORES_BY_DEFAULT: ClassVar[bool] = True # type: ignore[reportIncompatibleVariableOverride, misc] FILE_SEARCH_MAX_RESULTS: int = 50 + def __init__( + self, + *, + model: str | None = None, + model_id: str | None = None, + api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None = None, + org_id: str | None = None, + base_url: str | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncOpenAI | None = None, + instruction_role: str | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + **kwargs: Any, + ) -> None: + """Initialize a raw OpenAI Responses client. + + Keyword Args: + model: OpenAI model name. + model_id: Deprecated alias for ``model``. + api_key: OpenAI API key, SecretString, or callable returning a key. + org_id: OpenAI organization ID. + base_url: Custom API base URL. + default_headers: Additional HTTP headers. + async_client: Pre-configured AsyncOpenAI client (skips client creation). + instruction_role: Role for instruction messages (e.g. ``"system"``). + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + kwargs: Additional keyword arguments forwarded to ``BaseChatClient``. + """ + if model_id is not None and model is None: + import warnings + + warnings.warn("model_id is deprecated, use model instead", DeprecationWarning, stacklevel=2) + model = model_id + + if not async_client: + openai_settings = load_settings( + OpenAISettings, + env_prefix="OPENAI_", + api_key=api_key, + org_id=org_id, + base_url=base_url, + model=model, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + + api_key_value = openai_settings.get("api_key") + if not api_key_value: + raise ValueError( + "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." + ) + resolved_model = openai_settings.get("model") or model + if not resolved_model: + raise ValueError( + "OpenAI model is required. Set via 'model' parameter or 'OPENAI_MODEL' environment variable." + ) + model = resolved_model + + resolved_api_key = get_api_key(api_key_value) + + # Merge APP_INFO into the headers + merged_headers = dict(copy(default_headers)) if default_headers else {} + if APP_INFO: + merged_headers.update(APP_INFO) + merged_headers = prepend_agent_framework_to_user_agent(merged_headers) + + client_args: dict[str, Any] = {"api_key": resolved_api_key, "default_headers": merged_headers} + if resolved_org_id := openai_settings.get("org_id"): + client_args["organization"] = resolved_org_id + if resolved_base_url := openai_settings.get("base_url"): + client_args["base_url"] = resolved_base_url + + async_client = AsyncOpenAI(**client_args) + + self.client = async_client + self.model: str | None = model.strip() if model else None + + # Store configuration for serialization + self.org_id = org_id + self.base_url = str(base_url) if base_url else None + if default_headers: + self.default_headers: dict[str, Any] | None = { + k: v for k, v in default_headers.items() if k != USER_AGENT_KEY + } + else: + self.default_headers = None + + if instruction_role is not None: + self.instruction_role = instruction_role + + super().__init__(**kwargs) + # region Inner Methods async def _prepare_request( @@ -273,7 +369,7 @@ async def _prepare_request( Returns: Tuple of (client, run_options, validated_options). """ - client = await self._ensure_client() + client = self.client validated_options = await self._validate_options(options) run_options = await self._prepare_options(messages, validated_options, **kwargs) return client, run_options, validated_options @@ -309,7 +405,7 @@ async def _stream() -> AsyncIterable[ChatResponseUpdate]: nonlocal validated_options if continuation_token is not None: # Resume a background streaming response by retrieving with stream=True - client = await self._ensure_client() + client = self.client validated_options = await self._validate_options(options) try: stream_response = await client.responses.retrieve( @@ -356,7 +452,7 @@ async def _stream() -> AsyncIterable[ChatResponseUpdate]: async def _get_response() -> ChatResponse: if continuation_token is not None: # Poll a background response by retrieving without stream - client = await self._ensure_client() + client = self.client validated_options = await self._validate_options(options) try: response = await client.responses.retrieve(continuation_token["response_id"]) @@ -540,13 +636,13 @@ def get_code_interpreter_tool( Examples: .. code-block:: python - from agent_framework.openai import OpenAIResponsesClient + from agent_framework.openai import OpenAIChatClient # Basic code interpreter - tool = OpenAIResponsesClient.get_code_interpreter_tool() + tool = OpenAIChatClient.get_code_interpreter_tool() # With file access - tool = OpenAIResponsesClient.get_code_interpreter_tool(file_ids=["file-abc123"]) + tool = OpenAIChatClient.get_code_interpreter_tool(file_ids=["file-abc123"]) # Use with agent agent = ChatAgent(client, tools=[tool]) @@ -582,13 +678,13 @@ def get_web_search_tool( Examples: .. code-block:: python - from agent_framework.openai import OpenAIResponsesClient + from agent_framework.openai import OpenAIChatClient # Basic web search - tool = OpenAIResponsesClient.get_web_search_tool() + tool = OpenAIChatClient.get_web_search_tool() # With location context - tool = OpenAIResponsesClient.get_web_search_tool( + tool = OpenAIChatClient.get_web_search_tool( user_location={"city": "Seattle", "country": "US"}, search_context_size="medium", ) @@ -644,13 +740,13 @@ def get_image_generation_tool( Examples: .. code-block:: python - from agent_framework.openai import OpenAIResponsesClient + from agent_framework.openai import OpenAIChatClient # Basic image generation - tool = OpenAIResponsesClient.get_image_generation_tool() + tool = OpenAIChatClient.get_image_generation_tool() # High quality large image - tool = OpenAIResponsesClient.get_image_generation_tool( + tool = OpenAIChatClient.get_image_generation_tool( size="1536x1024", quality="high", output_format="png", @@ -712,18 +808,16 @@ def get_shell_tool( Examples: .. code-block:: python - from agent_framework.openai import OpenAIResponsesClient + from agent_framework.openai import OpenAIChatClient # Hosted shell (OpenAI container) - tool = OpenAIResponsesClient.get_shell_tool() + tool = OpenAIChatClient.get_shell_tool() # Hosted shell with custom environment - tool = OpenAIResponsesClient.get_shell_tool( - environment={"type": "container_auto", "file_ids": ["file-abc"]} - ) + tool = OpenAIChatClient.get_shell_tool(environment={"type": "container_auto", "file_ids": ["file-abc"]}) # Local shell execution - tool = OpenAIResponsesClient.get_shell_tool( + tool = OpenAIChatClient.get_shell_tool( func=my_shell_func, ) """ @@ -801,16 +895,16 @@ def get_mcp_tool( Examples: .. code-block:: python - from agent_framework.openai import OpenAIResponsesClient + from agent_framework.openai import OpenAIChatClient # Basic MCP tool - tool = OpenAIResponsesClient.get_mcp_tool( + tool = OpenAIChatClient.get_mcp_tool( name="my_mcp", url="https://mcp.example.com", ) # With approval settings - tool = OpenAIResponsesClient.get_mcp_tool( + tool = OpenAIChatClient.get_mcp_tool( name="github_mcp", url="https://mcp.github.com", description="GitHub MCP server", @@ -819,7 +913,7 @@ def get_mcp_tool( ) # With specific tool approvals - tool = OpenAIResponsesClient.get_mcp_tool( + tool = OpenAIChatClient.get_mcp_tool( name="tools_mcp", url="https://tools.example.com", approval_mode={ @@ -874,15 +968,15 @@ def get_file_search_tool( Examples: .. code-block:: python - from agent_framework.openai import OpenAIResponsesClient + from agent_framework.openai import OpenAIChatClient # Basic file search - tool = OpenAIResponsesClient.get_file_search_tool( + tool = OpenAIChatClient.get_file_search_tool( vector_store_ids=["vs_abc123"], ) # With result limit - tool = OpenAIResponsesClient.get_file_search_tool( + tool = OpenAIChatClient.get_file_search_tool( vector_store_ids=["vs_abc123", "vs_def456"], max_num_results=10, ) @@ -943,7 +1037,7 @@ async def _prepare_options( # translations between options and Responses API translations = { - "model_id": "model", + "model_id": "model", # backward compat: accept model_id in options "allow_multiple_tool_calls": "parallel_tool_calls", "conversation_id": "previous_response_id", "max_tokens": "max_output_tokens", @@ -1003,9 +1097,9 @@ def _check_model_presence(self, options: dict[str, Any]) -> None: Since AzureAIClients use a different param for this, this method is overridden in those clients. """ if not options.get("model"): - if not self.model_id: - raise ValueError("model_id must be a non-empty string") - options["model"] = self.model_id + if not self.model: + raise ValueError("model must be a non-empty string") + options["model"] = self.model def _get_current_conversation_id(self, options: Mapping[str, Any], **kwargs: Any) -> str | None: """Get the current conversation ID, preferring kwargs over options. @@ -1684,7 +1778,7 @@ def _parse_response_from_openai( "%Y-%m-%dT%H:%M:%S.%fZ" ), "messages": response_message, - "model_id": response.model, + "model": response.model, "additional_properties": metadata, "raw_representation": response, } @@ -1717,7 +1811,7 @@ def _parse_chunk_from_openai( conversation_id: str | None = None response_id: str | None = None continuation_token: OpenAIContinuationToken | None = None - model = self.model_id + model = self.model match event.type: # types: # ResponseAudioDeltaEvent, @@ -2230,7 +2324,7 @@ def _get_ann_value(key: str) -> Any: conversation_id=conversation_id, response_id=response_id, role="assistant", - model_id=model, + model=model, continuation_token=continuation_token, additional_properties=metadata, raw_representation=event, @@ -2257,20 +2351,21 @@ def _get_metadata_from_response(self, output: Any) -> dict[str, Any]: return {} -class OpenAIResponsesClient( # type: ignore[misc] - OpenAIConfigMixin, - FunctionInvocationLayer[OpenAIResponsesOptionsT], - ChatMiddlewareLayer[OpenAIResponsesOptionsT], - ChatTelemetryLayer[OpenAIResponsesOptionsT], - RawOpenAIResponsesClient[OpenAIResponsesOptionsT], - Generic[OpenAIResponsesOptionsT], +class OpenAIChatClient( # type: ignore[misc] + FunctionInvocationLayer[OpenAIChatOptionsT], + ChatMiddlewareLayer[OpenAIChatOptionsT], + ChatTelemetryLayer[OpenAIChatOptionsT], + RawOpenAIChatClient[OpenAIChatOptionsT], + Generic[OpenAIChatOptionsT], ): """OpenAI Responses client class with middleware, telemetry, and function invocation support.""" + OTEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc] + def __init__( self, *, - model_id: str | None = None, + model: str | None = None, api_key: str | Callable[[], str | Awaitable[str]] | None = None, org_id: str | None = None, base_url: str | None = None, @@ -2288,8 +2383,8 @@ def __init__( """Initialize an OpenAI Responses client. Keyword Args: - model_id: OpenAI model name, see https://platform.openai.com/docs/models. - Can also be set via environment variable OPENAI_RESPONSES_MODEL_ID. + model: OpenAI model name, see https://platform.openai.com/docs/models. + Can also be set via environment variable OPENAI_MODEL. api_key: The API key to use. If provided will override the env vars or .env file value. Can also be set via environment variable OPENAI_API_KEY. org_id: The org ID to use. If provided will override the env vars or .env file value. @@ -2311,63 +2406,63 @@ def __init__( Examples: .. code-block:: python - from agent_framework.openai import OpenAIResponsesClient + from agent_framework.openai import OpenAIChatClient # Using environment variables # Set OPENAI_API_KEY=sk-... - # Set OPENAI_RESPONSES_MODEL_ID=gpt-4o - client = OpenAIResponsesClient() + # Set OPENAI_MODEL=gpt-4o + client = OpenAIChatClient() # Or passing parameters directly - client = OpenAIResponsesClient(model_id="gpt-4o", api_key="sk-...") + client = OpenAIChatClient(model="gpt-4o", api_key="sk-...") # Or loading from a .env file - client = OpenAIResponsesClient(env_file_path="path/to/.env") + client = OpenAIChatClient(env_file_path="path/to/.env") # Using custom ChatOptions with type safety: from typing import TypedDict - from agent_framework.openai import OpenAIResponsesOptions + from agent_framework.openai import OpenAIChatOptions - class MyOptions(OpenAIResponsesOptions, total=False): + class MyOptions(OpenAIChatOptions, total=False): my_custom_option: str - client: OpenAIResponsesClient[MyOptions] = OpenAIResponsesClient(model_id="gpt-4o") + client: OpenAIChatClient[MyOptions] = OpenAIChatClient(model="gpt-4o") response = await client.get_response("Hello", options={"my_custom_option": "value"}) """ - openai_settings = load_settings( - OpenAISettings, - env_prefix="OPENAI_", + super().__init__( + model=model, api_key=api_key, org_id=org_id, base_url=base_url, - responses_model_id=model_id, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - ) - - api_key_setting = openai_settings.get("api_key") - if not async_client and not api_key_setting: - raise ValueError( - "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." - ) - responses_model_id = openai_settings.get("responses_model_id") - if not responses_model_id: - raise ValueError( - "OpenAI model ID is required. " - "Set via 'model_id' parameter or 'OPENAI_RESPONSES_MODEL_ID' environment variable." - ) - - super().__init__( - model_id=responses_model_id, - api_key=self._get_api_key(api_key_setting), - org_id=openai_settings.get("org_id"), default_headers=default_headers, - client=async_client, + async_client=async_client, instruction_role=instruction_role, - base_url=openai_settings.get("base_url"), + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, middleware=middleware, function_invocation_configuration=function_invocation_configuration, **kwargs, ) + + +def _apply_openai_chat_client_docstrings() -> None: + """Align OpenAI Responses client docstrings with the raw implementation.""" + from agent_framework._clients import BaseChatClient + from agent_framework._docstrings import apply_layered_docstring + + apply_layered_docstring(RawOpenAIChatClient.get_response, BaseChatClient.get_response) + apply_layered_docstring( + OpenAIChatClient.get_response, + RawOpenAIChatClient.get_response, + extra_keyword_args={ + "middleware": """ + Optional per-call chat and function middleware. + This is merged with any middleware configured on the client for the current request. + """, + }, + ) + + +_apply_openai_chat_client_docstrings() diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_completion_client.py similarity index 82% rename from python/packages/core/agent_framework/openai/_chat_client.py rename to python/packages/openai/agent_framework_openai/_chat_completion_client.py index a77d44d933..0c17818ce9 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_completion_client.py @@ -13,34 +13,24 @@ MutableMapping, Sequence, ) +from copy import copy from datetime import datetime, timezone from itertools import chain -from typing import Any, Generic, Literal, cast, overload - -from openai import AsyncOpenAI, BadRequestError -from openai.lib._parsing._completions import type_to_response_format_param -from openai.types import CompletionUsage -from openai.types.chat.chat_completion import ChatCompletion, Choice -from openai.types.chat.chat_completion_chunk import ChatCompletionChunk -from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice -from openai.types.chat.chat_completion_message_custom_tool_call import ( - ChatCompletionMessageCustomToolCall, -) -from openai.types.chat.completion_create_params import WebSearchOptions -from pydantic import BaseModel - -from .._clients import BaseChatClient -from .._docstrings import apply_layered_docstring -from .._middleware import ChatAndFunctionMiddlewareTypes, ChatMiddlewareLayer -from .._settings import load_settings -from .._tools import ( +from typing import Any, ClassVar, Generic, Literal, cast, overload + +from agent_framework._clients import BaseChatClient +from agent_framework._docstrings import apply_layered_docstring +from agent_framework._middleware import ChatAndFunctionMiddlewareTypes, ChatMiddlewareLayer +from agent_framework._settings import SecretString, load_settings +from agent_framework._telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent +from agent_framework._tools import ( FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, ToolTypes, normalize_tools, ) -from .._types import ( +from agent_framework._types import ( ChatOptions, ChatResponse, ChatResponseUpdate, @@ -50,13 +40,25 @@ ResponseStream, UsageDetails, ) -from ..exceptions import ( +from agent_framework.exceptions import ( ChatClientException, ChatClientInvalidRequestException, ) -from ..observability import ChatTelemetryLayer +from agent_framework.observability import ChatTelemetryLayer +from openai import AsyncOpenAI, BadRequestError +from openai.lib._parsing._completions import type_to_response_format_param +from openai.types import CompletionUsage +from openai.types.chat.chat_completion import ChatCompletion, Choice +from openai.types.chat.chat_completion_chunk import ChatCompletionChunk +from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice +from openai.types.chat.chat_completion_message_custom_tool_call import ( + ChatCompletionMessageCustomToolCall, +) +from openai.types.chat.completion_create_params import WebSearchOptions +from pydantic import BaseModel + from ._exceptions import OpenAIContentFilterException -from ._shared import OpenAIBase, OpenAIConfigMixin, OpenAISettings +from ._shared import OpenAISettings, get_api_key if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover @@ -94,7 +96,7 @@ class Prediction(TypedDict, total=False): content: str | list[PredictionTextContent] -class OpenAIChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], total=False): +class OpenAIChatCompletionOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], total=False): """OpenAI-specific chat options dict. Extends ChatOptions with options specific to OpenAI's Chat Completions API. @@ -133,20 +135,24 @@ class OpenAIChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], to prediction: Prediction -OpenAIChatOptionsT = TypeVar("OpenAIChatOptionsT", bound=TypedDict, default="OpenAIChatOptions", covariant=True) # type: ignore[valid-type] +OpenAIChatCompletionOptionsT = TypeVar( + "OpenAIChatCompletionOptionsT", + bound=TypedDict, # type: ignore[valid-type] + default="OpenAIChatCompletionOptions", + covariant=True, +) OPTION_TRANSLATIONS: dict[str, str] = { - "model_id": "model", + "model_id": "model", # backward compat: accept model_id in options "allow_multiple_tool_calls": "parallel_tool_calls", "max_tokens": "max_completion_tokens", } # region Base Client -class RawOpenAIChatClient( # type: ignore[misc] - OpenAIBase, - BaseChatClient[OpenAIChatOptionsT], - Generic[OpenAIChatOptionsT], +class RawOpenAIChatCompletionClient( # type: ignore[misc] + BaseChatClient[OpenAIChatCompletionOptionsT], + Generic[OpenAIChatCompletionOptionsT], ): """Raw OpenAI Chat completion class without middleware, telemetry, or function invocation. @@ -160,9 +166,105 @@ class RawOpenAIChatClient( # type: ignore[misc] 2. **ChatMiddlewareLayer** - Applies chat middleware per model call and stays outside telemetry 3. **ChatTelemetryLayer** - Must stay inside chat middleware for correct per-call telemetry - Use ``OpenAIChatClient`` instead for a fully-featured client with all layers applied. + Use ``OpenAIChatCompletionClient`` instead for a fully-featured client with all layers applied. """ + INJECTABLE: ClassVar[set[str]] = {"client"} + + def __init__( + self, + *, + model: str | None = None, + model_id: str | None = None, + api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None = None, + org_id: str | None = None, + base_url: str | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncOpenAI | None = None, + instruction_role: str | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + **kwargs: Any, + ) -> None: + """Initialize a raw OpenAI Chat completion client. + + Keyword Args: + model: OpenAI model name. + model_id: Deprecated alias for ``model``. + api_key: OpenAI API key, SecretString, or callable returning a key. + org_id: OpenAI organization ID. + base_url: Custom API base URL. + default_headers: Additional HTTP headers. + async_client: Pre-configured AsyncOpenAI client (skips client creation). + instruction_role: Role for instruction messages (e.g. ``"system"``). + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + kwargs: Additional keyword arguments forwarded to ``BaseChatClient``. + """ + if model_id is not None and model is None: + import warnings + + warnings.warn("model_id is deprecated, use model instead", DeprecationWarning, stacklevel=2) + model = model_id + + if not async_client: + openai_settings = load_settings( + OpenAISettings, + env_prefix="OPENAI_", + api_key=api_key, + org_id=org_id, + base_url=base_url, + model=model, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + + api_key_value = openai_settings.get("api_key") + if not api_key_value: + raise ValueError( + "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." + ) + resolved_model = openai_settings.get("model") or model + if not resolved_model: + raise ValueError( + "OpenAI model is required. Set via 'model' parameter or 'OPENAI_MODEL' environment variable." + ) + model = resolved_model + + resolved_api_key = get_api_key(api_key_value) + + # Merge APP_INFO into the headers + merged_headers = dict(copy(default_headers)) if default_headers else {} + if APP_INFO: + merged_headers.update(APP_INFO) + merged_headers = prepend_agent_framework_to_user_agent(merged_headers) + + client_args: dict[str, Any] = {"api_key": resolved_api_key, "default_headers": merged_headers} + if resolved_org_id := openai_settings.get("org_id"): + client_args["organization"] = resolved_org_id + if resolved_base_url := openai_settings.get("base_url"): + client_args["base_url"] = resolved_base_url + + async_client = AsyncOpenAI(**client_args) + + self.client = async_client + self.model: str | None = model.strip() if model else None + + # Store configuration for serialization + self.org_id = org_id + self.base_url = str(base_url) if base_url else None + if default_headers: + self.default_headers: dict[str, Any] | None = { + k: v for k, v in default_headers.items() if k != USER_AGENT_KEY + } + else: + self.default_headers = None + + if instruction_role is not None: + self.instruction_role = instruction_role + + super().__init__(**kwargs) + # region Hosted Tool Factory Methods @staticmethod @@ -188,13 +290,13 @@ def get_web_search_tool( Examples: .. code-block:: python - from agent_framework.openai import OpenAIChatClient + from agent_framework.openai import OpenAIChatCompletionClient # Basic web search - tool = OpenAIChatClient.get_web_search_tool() + tool = OpenAIChatCompletionClient.get_web_search_tool() # With location context - tool = OpenAIChatClient.get_web_search_tool( + tool = OpenAIChatCompletionClient.get_web_search_tool( web_search_options={ "user_location": { "type": "approximate", @@ -231,7 +333,7 @@ def get_response( messages: Sequence[Message], *, stream: Literal[False] = ..., - options: OpenAIChatOptionsT | ChatOptions[None] | None = None, + options: OpenAIChatCompletionOptionsT | ChatOptions[None] | None = None, **kwargs: Any, ) -> Awaitable[ChatResponse[Any]]: ... @@ -241,7 +343,7 @@ def get_response( messages: Sequence[Message], *, stream: Literal[True], - options: OpenAIChatOptionsT | ChatOptions[Any] | None = None, + options: OpenAIChatCompletionOptionsT | ChatOptions[Any] | None = None, **kwargs: Any, ) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ... @@ -251,7 +353,7 @@ def get_response( messages: Sequence[Message], *, stream: bool = False, - options: OpenAIChatOptionsT | ChatOptions[Any] | None = None, + options: OpenAIChatCompletionOptionsT | ChatOptions[Any] | None = None, **kwargs: Any, ) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: """Get a response from the raw OpenAI chat client.""" @@ -283,7 +385,7 @@ def _inner_get_response( options_dict["stream_options"] = {"include_usage": True} async def _stream() -> AsyncIterable[ChatResponseUpdate]: - client = await self._ensure_client() + client = self.client try: async for chunk in await client.chat.completions.create(stream=True, **options_dict): if len(chunk.choices) == 0 and chunk.usage is None: @@ -309,7 +411,7 @@ async def _stream() -> AsyncIterable[ChatResponseUpdate]: # Non-streaming mode async def _get_response() -> ChatResponse: - client = await self._ensure_client() + client = self.client try: return self._parse_response_from_openai( await client.chat.completions.create(stream=False, **options_dict), options @@ -374,7 +476,7 @@ def _prepare_tools_for_openai( def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, Any]) -> dict[str, Any]: # Prepend instructions from options if they exist - from .._types import prepend_instructions_to_messages, validate_tool_mode + from agent_framework._types import prepend_instructions_to_messages, validate_tool_mode if instructions := options.get("instructions"): messages = prepend_instructions_to_messages(list(messages), instructions, role="system") @@ -397,9 +499,9 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An # model id if not run_options.get("model"): - if not self.model_id: - raise ValueError("model_id must be a non-empty string") - run_options["model"] = self.model_id + if not self.model: + raise ValueError("model must be a non-empty string") + run_options["model"] = self.model # tools tools = options.get("tools") @@ -452,7 +554,7 @@ def _parse_response_from_openai(self, response: ChatCompletion, options: Mapping created_at=datetime.fromtimestamp(response.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), usage_details=self._parse_usage_from_openai(response.usage) if response.usage else None, messages=messages, - model_id=response.model, + model=response.model, additional_properties=response_metadata, finish_reason=finish_reason, response_format=options.get("response_format"), @@ -488,7 +590,7 @@ def _parse_response_update_from_openai( created_at=datetime.fromtimestamp(chunk.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), contents=contents, role="assistant", - model_id=chunk.model, + model=chunk.model, additional_properties=chunk_metadata, finish_reason=finish_reason, raw_representation=chunk, @@ -774,16 +876,17 @@ def service_url(self) -> str: # region Public client -class OpenAIChatClient( # type: ignore[misc] - OpenAIConfigMixin, - FunctionInvocationLayer[OpenAIChatOptionsT], - ChatMiddlewareLayer[OpenAIChatOptionsT], - ChatTelemetryLayer[OpenAIChatOptionsT], - RawOpenAIChatClient[OpenAIChatOptionsT], - Generic[OpenAIChatOptionsT], +class OpenAIChatCompletionClient( # type: ignore[misc] + FunctionInvocationLayer[OpenAIChatCompletionOptionsT], + ChatMiddlewareLayer[OpenAIChatCompletionOptionsT], + ChatTelemetryLayer[OpenAIChatCompletionOptionsT], + RawOpenAIChatCompletionClient[OpenAIChatCompletionOptionsT], + Generic[OpenAIChatCompletionOptionsT], ): """OpenAI Chat completion class with middleware, telemetry, and function invocation support.""" + OTEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc] + @overload def get_response( self, @@ -803,7 +906,7 @@ def get_response( messages: Sequence[Message], *, stream: Literal[False] = ..., - options: OpenAIChatOptionsT | ChatOptions[None] | None = None, + options: OpenAIChatCompletionOptionsT | ChatOptions[None] | None = None, function_invocation_kwargs: Mapping[str, Any] | None = None, client_kwargs: Mapping[str, Any] | None = None, middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None, @@ -816,7 +919,7 @@ def get_response( messages: Sequence[Message], *, stream: Literal[True], - options: OpenAIChatOptionsT | ChatOptions[Any] | None = None, + options: OpenAIChatCompletionOptionsT | ChatOptions[Any] | None = None, function_invocation_kwargs: Mapping[str, Any] | None = None, client_kwargs: Mapping[str, Any] | None = None, middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None, @@ -829,7 +932,7 @@ def get_response( messages: Sequence[Message], *, stream: bool = False, - options: OpenAIChatOptionsT | ChatOptions[Any] | None = None, + options: OpenAIChatCompletionOptionsT | ChatOptions[Any] | None = None, function_invocation_kwargs: Mapping[str, Any] | None = None, client_kwargs: Mapping[str, Any] | None = None, middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None, @@ -855,7 +958,7 @@ def get_response( def __init__( self, *, - model_id: str | None = None, + model: str | None = None, api_key: str | Callable[[], str | Awaitable[str]] | None = None, org_id: str | None = None, default_headers: Mapping[str, str] | None = None, @@ -870,8 +973,8 @@ def __init__( """Initialize an OpenAI Chat completion client. Keyword Args: - model_id: OpenAI model name, see https://platform.openai.com/docs/models. - Can also be set via environment variable OPENAI_CHAT_MODEL_ID. + model: OpenAI model name, see https://platform.openai.com/docs/models. + Can also be set via environment variable OPENAI_MODEL. api_key: The API key to use. If provided will override the env vars or .env file value. Can also be set via environment variable OPENAI_API_KEY. org_id: The org ID to use. If provided will override the env vars or .env file value. @@ -893,76 +996,52 @@ def __init__( Examples: .. code-block:: python - from agent_framework.openai import OpenAIChatClient + from agent_framework.openai import OpenAIChatCompletionClient # Using environment variables # Set OPENAI_API_KEY=sk-... - # Set OPENAI_CHAT_MODEL_ID= - client = OpenAIChatClient() + # Set OPENAI_MODEL= + client = OpenAIChatCompletionClient() # Or passing parameters directly - client = OpenAIChatClient(model_id="", api_key="sk-...") + client = OpenAIChatCompletionClient(model="", api_key="sk-...") # Or loading from a .env file - client = OpenAIChatClient(env_file_path="path/to/.env") + client = OpenAIChatCompletionClient(env_file_path="path/to/.env") # Using custom ChatOptions with type safety: from typing import TypedDict - from agent_framework.openai import OpenAIChatOptions + from agent_framework.openai import OpenAIChatCompletionOptions - class MyOptions(OpenAIChatOptions, total=False): + class MyOptions(OpenAIChatCompletionOptions, total=False): my_custom_option: str - client: OpenAIChatClient[MyOptions] = OpenAIChatClient(model_id="") + client: OpenAIChatCompletionClient[MyOptions] = OpenAIChatCompletionClient(model="") response = await client.get_response("Hello", options={"my_custom_option": "value"}) """ - openai_settings = load_settings( - OpenAISettings, - env_prefix="OPENAI_", + super().__init__( + model=model, api_key=api_key, - base_url=base_url, org_id=org_id, - chat_model_id=model_id, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - ) - - api_key_value = openai_settings.get("api_key") - if not async_client and not api_key_value: - raise ValueError( - "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." - ) - - chat_model_id = openai_settings.get("chat_model_id") - if not chat_model_id: - raise ValueError( - "OpenAI model ID is required. " - "Set via 'model_id' parameter or 'OPENAI_CHAT_MODEL_ID' environment variable." - ) - - base_url_value = openai_settings.get("base_url") - - super().__init__( - model_id=chat_model_id, - api_key=self._get_api_key(api_key_value), - base_url=base_url_value if base_url_value else None, - org_id=openai_settings.get("org_id"), + base_url=base_url, default_headers=default_headers, - client=async_client, + async_client=async_client, instruction_role=instruction_role, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, middleware=middleware, function_invocation_configuration=function_invocation_configuration, ) -def _apply_openai_chat_client_docstrings() -> None: - """Align OpenAI chat-client docstrings with the raw implementation.""" - apply_layered_docstring(RawOpenAIChatClient.get_response, BaseChatClient.get_response) +def _apply_openai_chat_completion_client_docstrings() -> None: + """Align OpenAI chat completion client docstrings with the raw implementation.""" + apply_layered_docstring(RawOpenAIChatCompletionClient.get_response, BaseChatClient.get_response) apply_layered_docstring( - OpenAIChatClient.get_response, - RawOpenAIChatClient.get_response, + OpenAIChatCompletionClient.get_response, + RawOpenAIChatCompletionClient.get_response, extra_keyword_args={ "middleware": """ Optional per-call chat and function middleware. @@ -972,4 +1051,4 @@ def _apply_openai_chat_client_docstrings() -> None: ) -_apply_openai_chat_client_docstrings() +_apply_openai_chat_completion_client_docstrings() diff --git a/python/packages/core/agent_framework/openai/_embedding_client.py b/python/packages/openai/agent_framework_openai/_embedding_client.py similarity index 51% rename from python/packages/core/agent_framework/openai/_embedding_client.py rename to python/packages/openai/agent_framework_openai/_embedding_client.py index b940e47c7c..ad959d5b39 100644 --- a/python/packages/core/agent_framework/openai/_embedding_client.py +++ b/python/packages/openai/agent_framework_openai/_embedding_client.py @@ -6,15 +6,17 @@ import struct import sys from collections.abc import Awaitable, Callable, Mapping, Sequence -from typing import Any, Generic, Literal, TypedDict - +from copy import copy +from typing import Any, ClassVar, Generic, Literal, TypedDict + +from agent_framework._clients import BaseEmbeddingClient +from agent_framework._settings import SecretString, load_settings +from agent_framework._telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent +from agent_framework._types import Embedding, EmbeddingGenerationOptions, GeneratedEmbeddings, UsageDetails +from agent_framework.observability import EmbeddingTelemetryLayer from openai import AsyncOpenAI -from .._clients import BaseEmbeddingClient -from .._settings import load_settings -from .._types import Embedding, EmbeddingGenerationOptions, GeneratedEmbeddings, UsageDetails -from ..observability import EmbeddingTelemetryLayer -from ._shared import OpenAIBase, OpenAIConfigMixin, OpenAISettings +from ._shared import OpenAISettings, get_api_key if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover @@ -33,7 +35,7 @@ class OpenAIEmbeddingOptions(EmbeddingGenerationOptions, total=False): from agent_framework.openai import OpenAIEmbeddingOptions options: OpenAIEmbeddingOptions = { - "model_id": "text-embedding-3-small", + "model": "text-embedding-3-small", "dimensions": 1536, "encoding_format": "float", } @@ -52,12 +54,103 @@ class OpenAIEmbeddingOptions(EmbeddingGenerationOptions, total=False): class RawOpenAIEmbeddingClient( - OpenAIBase, BaseEmbeddingClient[str, list[float], OpenAIEmbeddingOptionsT], Generic[OpenAIEmbeddingOptionsT], ): """Raw OpenAI embedding client without telemetry.""" + INJECTABLE: ClassVar[set[str]] = {"client"} + + def __init__( + self, + *, + model: str | None = None, + model_id: str | None = None, + api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None = None, + org_id: str | None = None, + base_url: str | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncOpenAI | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + **kwargs: Any, + ) -> None: + """Initialize a raw OpenAI embedding client. + + Keyword Args: + model: OpenAI embedding model name. + model_id: Deprecated alias for ``model``. + api_key: OpenAI API key, SecretString, or callable returning a key. + org_id: OpenAI organization ID. + base_url: Custom API base URL. + default_headers: Additional HTTP headers. + async_client: Pre-configured AsyncOpenAI client (skips client creation). + env_file_path: Path to .env file for settings. + env_file_encoding: Encoding for .env file. + kwargs: Additional keyword arguments forwarded to ``BaseEmbeddingClient``. + """ + if model_id is not None and model is None: + import warnings + + warnings.warn("model_id is deprecated, use model instead", DeprecationWarning, stacklevel=2) + model = model_id + + if not async_client: + openai_settings = load_settings( + OpenAISettings, + env_prefix="OPENAI_", + api_key=api_key, + org_id=org_id, + base_url=base_url, + embedding_model=model, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + + api_key_value = openai_settings.get("api_key") + resolved_model = openai_settings.get("embedding_model") or model + + # Only create a client when we have enough configuration. + # Subclasses that manage their own client pass no args here + if api_key_value: + if not resolved_model: + raise ValueError( + "OpenAI embedding model is required. " + "Set via 'model' parameter or 'OPENAI_EMBEDDING_MODEL' environment variable." + ) + model = resolved_model + + resolved_api_key = get_api_key(api_key_value) + + # Merge APP_INFO into the headers + merged_headers = dict(copy(default_headers)) if default_headers else {} + if APP_INFO: + merged_headers.update(APP_INFO) + merged_headers = prepend_agent_framework_to_user_agent(merged_headers) + + client_args: dict[str, Any] = {"api_key": resolved_api_key, "default_headers": merged_headers} + if resolved_org_id := openai_settings.get("org_id"): + client_args["organization"] = resolved_org_id + if resolved_base_url := openai_settings.get("base_url"): + client_args["base_url"] = resolved_base_url + + async_client = AsyncOpenAI(**client_args) + + self.client = async_client + self.model: str | None = model.strip() if model else None + + # Store configuration for serialization + self.org_id = org_id + self.base_url = str(base_url) if base_url else None + if default_headers: + self.default_headers: dict[str, Any] | None = { + k: v for k, v in default_headers.items() if k != USER_AGENT_KEY + } + else: + self.default_headers = None + + super().__init__(**kwargs) + def service_url(self) -> str: """Get the URL of the service.""" return str(self.client.base_url) if self.client else "Unknown" @@ -78,15 +171,16 @@ async def get_embeddings( Generated embeddings with usage metadata. Raises: - ValueError: If model_id is not provided or values is empty. + ValueError: If model is not provided or values is empty. """ if not values: return GeneratedEmbeddings([], options=options) # type: ignore opts: dict[str, Any] = options or {} # type: ignore - model = opts.get("model_id") or self.model_id + # backward compat: accept model_id in options + model = opts.get("model") or opts.get("model_id") or self.model if not model: - raise ValueError("model_id is required") + raise ValueError("model is required") kwargs: dict[str, Any] = {"input": list(values), "model": model} if dimensions := opts.get("dimensions"): @@ -96,7 +190,7 @@ async def get_embeddings( if user := opts.get("user"): kwargs["user"] = user - response = await (await self._ensure_client()).embeddings.create(**kwargs) + response = await self.client.embeddings.create(**kwargs) # type: ignore[union-attr] encoding = kwargs.get("encoding_format", "float") embeddings: list[Embedding[list[float]]] = [] @@ -112,7 +206,7 @@ async def get_embeddings( Embedding( vector=vector, dimensions=len(vector), - model_id=response.model, + model=response.model, ) ) @@ -127,7 +221,6 @@ async def get_embeddings( class OpenAIEmbeddingClient( - OpenAIConfigMixin, EmbeddingTelemetryLayer[str, list[float], OpenAIEmbeddingOptionsT], RawOpenAIEmbeddingClient[OpenAIEmbeddingOptionsT], Generic[OpenAIEmbeddingOptionsT], @@ -135,8 +228,9 @@ class OpenAIEmbeddingClient( """OpenAI embedding client with telemetry support. Keyword Args: - model_id: The embedding model ID (e.g. "text-embedding-3-small"). - Can also be set via environment variable OPENAI_EMBEDDING_MODEL_ID. + model: The embedding model (e.g. "text-embedding-3-small"). + Can also be set via environment variable OPENAI_EMBEDDING_MODEL. + model_id: Deprecated alias for ``model``. api_key: OpenAI API key. Can also be set via environment variable OPENAI_API_KEY. org_id: OpenAI organization ID. @@ -154,12 +248,12 @@ class OpenAIEmbeddingClient( # Using environment variables # Set OPENAI_API_KEY=sk-... - # Set OPENAI_EMBEDDING_MODEL_ID=text-embedding-3-small + # Set OPENAI_EMBEDDING_MODEL=text-embedding-3-small client = OpenAIEmbeddingClient() # Or passing parameters directly client = OpenAIEmbeddingClient( - model_id="text-embedding-3-small", + model="text-embedding-3-small", api_key="sk-...", ) @@ -168,10 +262,12 @@ class OpenAIEmbeddingClient( print(result[0].vector) """ + OTEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc] + def __init__( self, *, - model_id: str | None = None, + model: str | None = None, api_key: str | Callable[[], str | Awaitable[str]] | None = None, org_id: str | None = None, default_headers: Mapping[str, str] | None = None, @@ -182,38 +278,26 @@ def __init__( env_file_encoding: str | None = None, ) -> None: """Initialize an OpenAI embedding client.""" - openai_settings = load_settings( - OpenAISettings, - env_prefix="OPENAI_", + super().__init__( + model=model, api_key=api_key, - base_url=base_url, org_id=org_id, - embedding_model_id=model_id, + base_url=base_url, + default_headers=default_headers, + async_client=async_client, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) + if otel_provider_name is not None: + self.OTEL_PROVIDER_NAME = otel_provider_name # type: ignore[misc] - api_key_value = openai_settings.get("api_key") - if not async_client and not api_key_value: + # Validate that the client was created successfully (from explicit args or env vars) + if self.client is None: raise ValueError( "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) - - embedding_model_id = openai_settings.get("embedding_model_id") - if not embedding_model_id: + if not self.model: raise ValueError( - "OpenAI embedding model ID is required. " - "Set via 'model_id' parameter or 'OPENAI_EMBEDDING_MODEL_ID' environment variable." + "OpenAI embedding model is required. " + "Set via 'model' parameter or 'OPENAI_EMBEDDING_MODEL' environment variable." ) - - base_url_value = openai_settings.get("base_url") - - super().__init__( - model_id=embedding_model_id, - api_key=self._get_api_key(api_key_value), - base_url=base_url_value if base_url_value else None, - org_id=openai_settings.get("org_id"), - default_headers=default_headers, - client=async_client, - otel_provider_name=otel_provider_name, - ) diff --git a/python/packages/core/agent_framework/openai/_exceptions.py b/python/packages/openai/agent_framework_openai/_exceptions.py similarity index 97% rename from python/packages/core/agent_framework/openai/_exceptions.py rename to python/packages/openai/agent_framework_openai/_exceptions.py index 9aa597da45..bbea701acb 100644 --- a/python/packages/core/agent_framework/openai/_exceptions.py +++ b/python/packages/openai/agent_framework_openai/_exceptions.py @@ -6,10 +6,9 @@ from enum import Enum from typing import Any +from agent_framework.exceptions import ChatClientContentFilterException from openai import BadRequestError -from ..exceptions import ChatClientContentFilterException - class ContentFilterResultSeverity(Enum): """The severity of the content filter result.""" diff --git a/python/packages/core/agent_framework/openai/_shared.py b/python/packages/openai/agent_framework_openai/_shared.py similarity index 81% rename from python/packages/core/agent_framework/openai/_shared.py rename to python/packages/openai/agent_framework_openai/_shared.py index 9817b7fb11..96990b79fa 100644 --- a/python/packages/core/agent_framework/openai/_shared.py +++ b/python/packages/openai/agent_framework_openai/_shared.py @@ -9,6 +9,10 @@ from typing import Any, ClassVar, Union, cast import openai +from agent_framework._serialization import SerializationMixin +from agent_framework._settings import SecretString +from agent_framework._telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent +from agent_framework._tools import FunctionTool from openai import ( AsyncOpenAI, AsyncStream, @@ -22,11 +26,6 @@ from openai.types.responses.response_stream_event import ResponseStreamEvent from packaging.version import parse -from .._serialization import SerializationMixin -from .._settings import SecretString -from .._telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent -from .._tools import FunctionTool - logger: logging.Logger = logging.getLogger("agent_framework.openai") @@ -88,12 +87,10 @@ class OpenAISettings(TypedDict, total=False): Can be set via environment variable OPENAI_BASE_URL. org_id: This is usually optional unless your account belongs to multiple organizations. Can be set via environment variable OPENAI_ORG_ID. - chat_model_id: The OpenAI chat model ID to use, for example, gpt-3.5-turbo or gpt-4. - Can be set via environment variable OPENAI_CHAT_MODEL_ID. - responses_model_id: The OpenAI responses model ID to use, for example, gpt-4o or o1. - Can be set via environment variable OPENAI_RESPONSES_MODEL_ID. - embedding_model_id: The OpenAI embedding model ID to use, for example, text-embedding-3-small. - Can be set via environment variable OPENAI_EMBEDDING_MODEL_ID. + model: The OpenAI model to use, for example, gpt-4o or o1. + Can be set via environment variable OPENAI_MODEL. + embedding_model: The OpenAI embedding model to use, for example, text-embedding-3-small. + Can be set via environment variable OPENAI_EMBEDDING_MODEL. Examples: .. code-block:: python @@ -102,11 +99,11 @@ class OpenAISettings(TypedDict, total=False): # Using environment variables # Set OPENAI_API_KEY=sk-... - # Set OPENAI_CHAT_MODEL_ID=gpt-4 + # Set OPENAI_MODEL=gpt-4o settings = load_settings(OpenAISettings, env_prefix="OPENAI_") # Or passing parameters directly - settings = load_settings(OpenAISettings, env_prefix="OPENAI_", api_key="sk-...", chat_model_id="gpt-4") + settings = load_settings(OpenAISettings, env_prefix="OPENAI_", api_key="sk-...", model="gpt-4o") # Or loading from a .env file settings = load_settings(OpenAISettings, env_prefix="OPENAI_", env_file_path="path/to/.env") @@ -115,28 +112,64 @@ class OpenAISettings(TypedDict, total=False): api_key: SecretString | Callable[[], str | Awaitable[str]] | None base_url: str | None org_id: str | None - chat_model_id: str | None - responses_model_id: str | None - embedding_model_id: str | None + model: str | None + embedding_model: str | None + + +def get_api_key( + api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None, +) -> str | Callable[[], str | Awaitable[str]] | None: + """Get the appropriate API key value for client initialization. + + Args: + api_key: The API key parameter which can be a string, SecretString, callable, or None. + + Returns: + For callable API keys: returns the callable directly. + For SecretString: returns the unwrapped secret value. + For string/None API keys: returns as-is. + """ + if isinstance(api_key, SecretString): + return api_key.get_secret_value() + + # Check version compatibility for callable API keys + if callable(api_key): + _check_openai_version_for_callable_api_key() + + return api_key # Pass callable, string, or None directly to OpenAI SDK class OpenAIBase(SerializationMixin): - """Base class for OpenAI Clients.""" + """Base class for OpenAI Clients. + + .. deprecated:: + ``OpenAIBase`` is deprecated and only used by ``OpenAIAssistantsClient`` + and ``AzureOpenAIAssistantsClient``. New clients should manage ``client`` + and ``model`` directly in their own ``__init__``. + """ INJECTABLE: ClassVar[set[str]] = {"client"} - def __init__(self, *, model_id: str | None = None, client: AsyncOpenAI | None = None, **kwargs: Any) -> None: + def __init__( + self, *, model: str | None = None, model_id: str | None = None, client: AsyncOpenAI | None = None, **kwargs: Any + ) -> None: """Initialize OpenAIBase. Keyword Args: client: The AsyncOpenAI client instance. - model_id: The AI model ID to use. + model: The AI model to use. + model_id: Deprecated alias for ``model``. **kwargs: Additional keyword arguments. """ + if model_id is not None and model is None: + import warnings + + warnings.warn("model_id is deprecated, use model instead", DeprecationWarning, stacklevel=2) + model = model_id self.client = client - self.model_id = None - if model_id: - self.model_id = model_id.strip() + self.model: str | None = None + if model: + self.model = model.strip() # Call super().__init__() to continue MRO chain (e.g., RawChatClient) # Extract known kwargs that belong to other base classes @@ -201,13 +234,19 @@ def _get_api_key( class OpenAIConfigMixin(OpenAIBase): - """Internal class for configuring a connection to an OpenAI service.""" + """Internal class for configuring a connection to an OpenAI service. + + .. deprecated:: + ``OpenAIConfigMixin`` is deprecated and only used by ``OpenAIAssistantsClient`` + and ``AzureOpenAIAssistantsClient``. New clients handle configuration + directly in their own ``__init__``. + """ OTEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc] def __init__( self, - model_id: str, + model: str, api_key: str | Callable[[], str | Awaitable[str]] | None = None, org_id: str | None = None, default_headers: Mapping[str, str] | None = None, @@ -222,7 +261,7 @@ def __init__( different types of AI model interactions, like chat or text completion. Args: - model_id: OpenAI model identifier. Must be non-empty. + model: OpenAI model identifier. Must be non-empty. Default to a preset value. api_key: OpenAI API key for authentication, or a callable that returns an API key. Must be non-empty. (Optional) @@ -269,7 +308,7 @@ def __init__( self.default_headers = None args = { - "model_id": model_id, + "model": model, "client": client, } if instruction_role: diff --git a/python/packages/openai/agent_framework_openai/py.typed b/python/packages/openai/agent_framework_openai/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/packages/openai/pyproject.toml b/python/packages/openai/pyproject.toml new file mode 100644 index 0000000000..d278702787 --- /dev/null +++ b/python/packages/openai/pyproject.toml @@ -0,0 +1,97 @@ +[project] +name = "agent-framework-openai" +description = "OpenAI integration for Microsoft Agent Framework." +authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] +readme = "README.md" +requires-python = ">=3.10" +version = "1.0.0rc5" +license-files = ["LICENSE"] +urls.homepage = "https://aka.ms/agent-framework" +urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" +urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true" +urls.issues = "https://github.com/microsoft/agent-framework/issues" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", +] +dependencies = [ + "agent-framework-core>=1.0.0rc5", + "openai>=1.99.0,<3", + "packaging>=24.1,<25", +] + +[tool.uv] +prerelease = "if-necessary-or-explicit" +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'" +] + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" + +[tool.pytest.ini_options] +testpaths = 'tests' +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [] +timeout = 120 +markers = [ + "integration: marks tests as integration tests that require external services", +] + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.coverage.run] +omit = [ + "**/__init__.py" +] + +[tool.pyright] +extends = "../../pyproject.toml" +exclude = ['tests'] + +[tool.mypy] +plugins = ['pydantic.mypy'] +strict = true +python_version = "3.10" +ignore_missing_imports = true +disallow_untyped_defs = true +no_implicit_optional = true +check_untyped_defs = true +warn_return_any = true +show_error_codes = true +warn_unused_ignores = false +disallow_incomplete_defs = true +disallow_untyped_decorators = true + +[tool.bandit] +targets = ["agent_framework_openai"] +exclude_dirs = ["tests"] + +[tool.poe] +executor.type = "uv" +include = "../../shared_tasks.toml" + +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_openai" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_openai --cov-report=term-missing:skip-covered -n auto --dist worksteal tests' + +[build-system] +requires = ["flit-core >= 3.11,<4.0"] +build-backend = "flit_core.buildapi" diff --git a/python/packages/openai/tests/assets/sample_image.jpg b/python/packages/openai/tests/assets/sample_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ea6486656fd5b603af043e29b941c99845baea7a GIT binary patch literal 182161 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%0Dk-Pm;7s}kHuGa7vE${d#SHv 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 UcKRcD&9zq&ZLV4!^WzTS~D|(tTOER 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 B_m}_x literal 0 HcmV?d00001 diff --git a/python/packages/openai/tests/openai/conftest.py b/python/packages/openai/tests/openai/conftest.py new file mode 100644 index 0000000000..43cc3a3554 --- /dev/null +++ b/python/packages/openai/tests/openai/conftest.py @@ -0,0 +1,127 @@ +# Copyright (c) Microsoft. All rights reserved. + +from collections.abc import Generator +from typing import Any +from unittest.mock import patch + +from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExporter +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from pytest import fixture + + +# region Connector Settings fixtures +@fixture +def exclude_list(request: Any) -> list[str]: + """Fixture that returns a list of environment variables to exclude.""" + return request.param if hasattr(request, "param") else [] + + +@fixture +def override_env_param_dict(request: Any) -> dict[str, str]: + """Fixture that returns a dict of environment variables to override.""" + return request.param if hasattr(request, "param") else {} + + +@fixture() +def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore + """Fixture to set environment variables for OpenAISettings.""" + if exclude_list is None: + exclude_list = [] + + if override_env_param_dict is None: + override_env_param_dict = {} + + env_vars = { + "OPENAI_API_KEY": "test-dummy-key", + "OPENAI_ORG_ID": "test_org_id", + "OPENAI_MODEL": "test_model_id", + "OPENAI_EMBEDDING_MODEL": "test_embedding_model_id", + "OPENAI_TEXT_MODEL_ID": "test_text_model_id", + "OPENAI_TEXT_TO_IMAGE_MODEL_ID": "test_text_to_image_model_id", + "OPENAI_AUDIO_TO_TEXT_MODEL_ID": "test_audio_to_text_model_id", + "OPENAI_TEXT_TO_AUDIO_MODEL_ID": "test_text_to_audio_model_id", + "OPENAI_REALTIME_MODEL_ID": "test_realtime_model_id", + } + + env_vars.update(override_env_param_dict) # type: ignore + + for key, value in env_vars.items(): + if key in exclude_list: + monkeypatch.delenv(key, raising=False) # type: ignore + continue + monkeypatch.setenv(key, value) # type: ignore + + return env_vars + + +# region Observability fixtures +@fixture +def enable_instrumentation(request: Any) -> bool: + """Fixture that returns a boolean indicating if Otel is enabled.""" + return request.param if hasattr(request, "param") else True + + +@fixture +def enable_sensitive_data(request: Any) -> bool: + """Fixture that returns a boolean indicating if sensitive data is enabled.""" + return request.param if hasattr(request, "param") else True + + +@fixture +def span_exporter(monkeypatch, enable_instrumentation: bool, enable_sensitive_data: bool) -> Generator[SpanExporter]: + """Fixture to remove environment variables for ObservabilitySettings.""" + env_vars = [ + "ENABLE_INSTRUMENTATION", + "ENABLE_SENSITIVE_DATA", + "ENABLE_CONSOLE_EXPORTERS", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + "OTEL_EXPORTER_OTLP_PROTOCOL", + "OTEL_EXPORTER_OTLP_HEADERS", + "OTEL_EXPORTER_OTLP_TRACES_HEADERS", + "OTEL_EXPORTER_OTLP_METRICS_HEADERS", + "OTEL_EXPORTER_OTLP_LOGS_HEADERS", + "OTEL_SERVICE_NAME", + "OTEL_SERVICE_VERSION", + "OTEL_RESOURCE_ATTRIBUTES", + ] + + for key in env_vars: + monkeypatch.delenv(key, raising=False) # type: ignore + monkeypatch.setenv("ENABLE_INSTRUMENTATION", str(enable_instrumentation)) # type: ignore + if not enable_instrumentation: + enable_sensitive_data = False + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", str(enable_sensitive_data)) # type: ignore + import importlib + + import agent_framework.observability as observability + from opentelemetry import trace + + importlib.reload(observability) + + observability_settings = observability.ObservabilitySettings() + + if enable_instrumentation or enable_sensitive_data: + from opentelemetry.sdk.trace import TracerProvider + + tracer_provider = TracerProvider(resource=observability_settings._resource) + trace.set_tracer_provider(tracer_provider) + + monkeypatch.setattr(observability, "OBSERVABILITY_SETTINGS", observability_settings, raising=False) # type: ignore + + with ( + patch("agent_framework.observability.OBSERVABILITY_SETTINGS", observability_settings), + patch("agent_framework.observability.configure_otel_providers"), + ): + exporter = InMemorySpanExporter() + if enable_instrumentation or enable_sensitive_data: + tracer_provider = trace.get_tracer_provider() + if not hasattr(tracer_provider, "add_span_processor"): + raise RuntimeError("Tracer provider does not support adding span processors.") + + tracer_provider.add_span_processor(SimpleSpanProcessor(exporter)) # type: ignore + + yield exporter + exporter.clear() diff --git a/python/packages/core/tests/openai/test_assistant_provider.py b/python/packages/openai/tests/openai/test_assistant_provider.py similarity index 98% rename from python/packages/core/tests/openai/test_assistant_provider.py rename to python/packages/openai/tests/openai/test_assistant_provider.py index 2aa8c89f84..c05ea950a6 100644 --- a/python/packages/core/tests/openai/test_assistant_provider.py +++ b/python/packages/openai/tests/openai/test_assistant_provider.py @@ -5,12 +5,12 @@ from unittest.mock import AsyncMock, MagicMock import pytest +from agent_framework import Agent, normalize_tools, tool from openai.types.beta.assistant import Assistant from pydantic import BaseModel, Field -from agent_framework import Agent, normalize_tools, tool -from agent_framework.openai import OpenAIAssistantProvider, OpenAIAssistantsClient -from agent_framework.openai._shared import from_assistant_tools, to_assistant_tools +from agent_framework_openai import OpenAIAssistantProvider, OpenAIAssistantsClient +from agent_framework_openai._shared import from_assistant_tools, to_assistant_tools # region Test Helpers @@ -130,13 +130,12 @@ def test_init_fails_without_api_key(self) -> None: from unittest.mock import patch # Mock load_settings to return a dict with None for api_key - with patch("agent_framework.openai._assistant_provider.load_settings") as mock_load: + with patch("agent_framework_openai._assistant_provider.load_settings") as mock_load: mock_load.return_value = { "api_key": None, "org_id": None, "base_url": None, - "chat_model_id": None, - "responses_model_id": None, + "model": None, } with pytest.raises(ValueError) as exc_info: @@ -772,7 +771,7 @@ async def test_create_and_run_agent(self) -> None: agent = await provider.create_agent( name="IntegrationTestAgent", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful assistant. Respond briefly.", ) @@ -797,7 +796,7 @@ def get_current_time() -> str: agent = await provider.create_agent( name="TimeAgent", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful assistant.", tools=[get_current_time], ) diff --git a/python/packages/core/tests/openai/test_openai_assistants_client.py b/python/packages/openai/tests/openai/test_openai_assistants_client.py similarity index 96% rename from python/packages/core/tests/openai/test_openai_assistants_client.py rename to python/packages/openai/tests/openai/test_openai_assistants_client.py index 21f7173ca3..cfca86b539 100644 --- a/python/packages/core/tests/openai/test_openai_assistants_client.py +++ b/python/packages/openai/tests/openai/test_openai_assistants_client.py @@ -7,6 +7,18 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from agent_framework import ( + Agent, + AgentResponse, + AgentResponseUpdate, + AgentSession, + ChatResponse, + ChatResponseUpdate, + Content, + Message, + SupportsChatGetResponse, + tool, +) from openai.types.beta.threads import ( FileCitationAnnotation, FilePathAnnotation, @@ -22,19 +34,7 @@ from openai.types.beta.threads.runs import RunStep from pydantic import Field -from agent_framework import ( - Agent, - AgentResponse, - AgentResponseUpdate, - AgentSession, - ChatResponse, - ChatResponseUpdate, - Content, - Message, - SupportsChatGetResponse, - tool, -) -from agent_framework.openai import OpenAIAssistantsClient +from agent_framework_openai import OpenAIAssistantsClient skip_if_openai_integration_tests_disabled = pytest.mark.skipif( os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"), @@ -46,7 +46,7 @@ def create_test_openai_assistants_client( mock_async_openai: MagicMock, - model_id: str | None = None, + model: str | None = None, assistant_id: str | None = None, assistant_name: str | None = None, thread_id: str | None = None, @@ -54,7 +54,7 @@ def create_test_openai_assistants_client( ) -> OpenAIAssistantsClient: """Helper function to create OpenAIAssistantsClient instances for testing.""" client = OpenAIAssistantsClient( - model_id=model_id or "gpt-4", + model=model or "gpt-4", assistant_id=assistant_id, assistant_name=assistant_name, thread_id=thread_id, @@ -120,11 +120,11 @@ def mock_async_openai() -> MagicMock: def test_init_with_client(mock_async_openai: MagicMock) -> None: """Test OpenAIAssistantsClient initialization with existing client.""" client = create_test_openai_assistants_client( - mock_async_openai, model_id="gpt-4", assistant_id="existing-assistant-id", thread_id="test-thread-id" + mock_async_openai, model="gpt-4", assistant_id="existing-assistant-id", thread_id="test-thread-id" ) assert client.client is mock_async_openai - assert client.model_id == "gpt-4" + assert client.model == "gpt-4" assert client.assistant_id == "existing-assistant-id" assert client.thread_id == "test-thread-id" assert not client._should_delete_assistant # type: ignore @@ -137,7 +137,7 @@ def test_init_auto_create_client( ) -> None: """Test OpenAIAssistantsClient initialization with auto-created client.""" client = OpenAIAssistantsClient( - model_id=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + model=openai_unit_test_env["OPENAI_MODEL"], assistant_name="TestAssistant", api_key=openai_unit_test_env["OPENAI_API_KEY"], org_id=openai_unit_test_env["OPENAI_ORG_ID"], @@ -145,7 +145,7 @@ def test_init_auto_create_client( ) assert client.client is mock_async_openai - assert client.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert client.model == openai_unit_test_env["OPENAI_MODEL"] assert client.assistant_id is None assert client.assistant_name == "TestAssistant" assert not client._should_delete_assistant # type: ignore @@ -155,10 +155,10 @@ def test_init_validation_fail() -> None: """Test OpenAIAssistantsClient initialization with validation failure.""" with pytest.raises(ValueError): # Force failure by providing invalid model ID type - OpenAIAssistantsClient(model_id=123, api_key="valid-key") # type: ignore + OpenAIAssistantsClient(model=123, api_key="valid-key") # type: ignore -@pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_MODEL_ID"]], indirect=True) +@pytest.mark.parametrize("exclude_list", [["OPENAI_MODEL"]], indirect=True) def test_init_missing_model_id(openai_unit_test_env: dict[str, str]) -> None: """Test OpenAIAssistantsClient initialization with missing model ID.""" with pytest.raises(ValueError): @@ -169,7 +169,7 @@ def test_init_missing_model_id(openai_unit_test_env: dict[str, str]) -> None: def test_init_missing_api_key(openai_unit_test_env: dict[str, str]) -> None: """Test OpenAIAssistantsClient initialization with missing API key.""" with pytest.raises(ValueError): - OpenAIAssistantsClient(model_id="gpt-4") + OpenAIAssistantsClient(model="gpt-4") def test_init_with_default_headers(openai_unit_test_env: dict[str, str]) -> None: @@ -177,12 +177,12 @@ def test_init_with_default_headers(openai_unit_test_env: dict[str, str]) -> None default_headers = {"X-Unit-Test": "test-guid"} client = OpenAIAssistantsClient( - model_id="gpt-4", + model="gpt-4", api_key=openai_unit_test_env["OPENAI_API_KEY"], default_headers=default_headers, ) - assert client.model_id == "gpt-4" + assert client.model == "gpt-4" assert isinstance(client, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers @@ -208,7 +208,7 @@ async def test_get_assistant_id_or_create_create_new( mock_async_openai: MagicMock, ) -> None: """Test _get_assistant_id_or_create when creating a new assistant.""" - client = create_test_openai_assistants_client(mock_async_openai, model_id="gpt-4", assistant_name="TestAssistant") + client = create_test_openai_assistants_client(mock_async_openai, model="gpt-4", assistant_name="TestAssistant") assistant_id = await client._get_assistant_id_or_create() # type: ignore @@ -265,7 +265,7 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None: # Test basic initialization and to_dict client = OpenAIAssistantsClient( - model_id="gpt-4", + model="gpt-4", assistant_id="test-assistant-id", assistant_name="TestAssistant", thread_id="test-thread-id", @@ -276,7 +276,7 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None: dumped_settings = client.to_dict() - assert dumped_settings["model_id"] == "gpt-4" + assert dumped_settings["model"] == "gpt-4" assert dumped_settings["assistant_id"] == "test-assistant-id" assert dumped_settings["assistant_name"] == "TestAssistant" assert dumped_settings["thread_id"] == "test-thread-id" @@ -728,7 +728,7 @@ def test_parse_run_step_with_code_interpreter_tool_call(mock_async_openai: Magic """Test _parse_run_step_tool_call with code_interpreter type creates CodeInterpreterToolCallContent.""" client = create_test_openai_assistants_client( mock_async_openai, - model_id="test-model", + model="test-model", assistant_id="test-assistant", ) @@ -766,7 +766,7 @@ def test_parse_run_step_with_mcp_tool_call(mock_async_openai: MagicMock) -> None """Test _parse_run_step_tool_call with mcp type creates MCPServerToolCallContent.""" client = create_test_openai_assistants_client( mock_async_openai, - model_id="test-model", + model="test-model", assistant_id="test-assistant", ) @@ -806,7 +806,7 @@ def test_prepare_options_basic(mock_async_openai: MagicMock) -> None: # Create basic chat options as a dict options = { "max_tokens": 100, - "model_id": "gpt-4", + "model": "gpt-4", "temperature": 0.7, "top_p": 0.9, } @@ -1202,7 +1202,7 @@ def get_weather( @skip_if_openai_integration_tests_disabled async def test_get_response() -> None: """Test OpenAI Assistants Client response.""" - async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: + async with OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL) as openai_assistants_client: assert isinstance(openai_assistants_client, SupportsChatGetResponse) messages: list[Message] = [] @@ -1228,7 +1228,7 @@ async def test_get_response() -> None: @skip_if_openai_integration_tests_disabled async def test_get_response_tools() -> None: """Test OpenAI Assistants Client response with tools.""" - async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: + async with OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL) as openai_assistants_client: assert isinstance(openai_assistants_client, SupportsChatGetResponse) messages: list[Message] = [] @@ -1250,7 +1250,7 @@ async def test_get_response_tools() -> None: @skip_if_openai_integration_tests_disabled async def test_streaming() -> None: """Test OpenAI Assistants Client streaming response.""" - async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: + async with OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL) as openai_assistants_client: assert isinstance(openai_assistants_client, SupportsChatGetResponse) messages: list[Message] = [] @@ -1282,7 +1282,7 @@ async def test_streaming() -> None: @skip_if_openai_integration_tests_disabled async def test_streaming_tools() -> None: """Test OpenAI Assistants Client streaming response with tools.""" - async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: + async with OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL) as openai_assistants_client: assert isinstance(openai_assistants_client, SupportsChatGetResponse) messages: list[Message] = [] @@ -1314,7 +1314,7 @@ async def test_streaming_tools() -> None: async def test_with_existing_assistant() -> None: """Test OpenAI Assistants Client with existing assistant ID.""" # First create an assistant to use in the test - async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as temp_client: + async with OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL) as temp_client: # Get the assistant ID by triggering assistant creation messages = [Message(role="user", text="Hello")] await temp_client.get_response(messages=messages) @@ -1322,7 +1322,7 @@ async def test_with_existing_assistant() -> None: # Now test using the existing assistant async with OpenAIAssistantsClient( - model_id=INTEGRATION_TEST_MODEL, assistant_id=assistant_id + model=INTEGRATION_TEST_MODEL, assistant_id=assistant_id ) as openai_assistants_client: assert isinstance(openai_assistants_client, SupportsChatGetResponse) assert openai_assistants_client.assistant_id == assistant_id @@ -1343,7 +1343,7 @@ async def test_with_existing_assistant() -> None: @pytest.mark.skip(reason="OpenAI file search functionality is currently broken - tracked in GitHub issue") async def test_file_search() -> None: """Test OpenAI Assistants Client response.""" - async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: + async with OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL) as openai_assistants_client: assert isinstance(openai_assistants_client, SupportsChatGetResponse) messages: list[Message] = [] @@ -1370,7 +1370,7 @@ async def test_file_search() -> None: @pytest.mark.skip(reason="OpenAI file search functionality is currently broken - tracked in GitHub issue") async def test_file_search_streaming() -> None: """Test OpenAI Assistants Client response.""" - async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: + async with OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL) as openai_assistants_client: assert isinstance(openai_assistants_client, SupportsChatGetResponse) messages: list[Message] = [] @@ -1405,7 +1405,7 @@ async def test_file_search_streaming() -> None: async def test_openai_assistants_agent_basic_run(): """Test Agent basic run functionality with OpenAIAssistantsClient.""" async with Agent( - client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + client=OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL), ) as agent: # Run a simple query response = await agent.run("Hello! Please respond with 'Hello World' exactly.") @@ -1423,7 +1423,7 @@ async def test_openai_assistants_agent_basic_run(): async def test_openai_assistants_agent_basic_run_streaming(): """Test Agent basic streaming functionality with OpenAIAssistantsClient.""" async with Agent( - client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + client=OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL), ) as agent: # Run streaming query full_message: str = "" @@ -1444,7 +1444,7 @@ async def test_openai_assistants_agent_basic_run_streaming(): async def test_openai_assistants_agent_session_persistence(): """Test Agent session persistence across runs with OpenAIAssistantsClient.""" async with Agent( - client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + client=OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL), instructions="You are a helpful assistant with good memory.", ) as agent: # Create a new session that will be reused @@ -1477,7 +1477,7 @@ async def test_openai_assistants_agent_existing_session_id(): existing_session_id = None async with Agent( - client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + client=OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL), instructions="You are a helpful weather agent.", tools=[get_weather], ) as agent: @@ -1521,7 +1521,7 @@ async def test_openai_assistants_agent_code_interpreter(): """Test Agent with code interpreter through OpenAIAssistantsClient.""" async with Agent( - client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + client=OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL), instructions="You are a helpful assistant that can write and execute Python code.", tools=[OpenAIAssistantsClient.get_code_interpreter_tool()], ) as agent: @@ -1542,7 +1542,7 @@ async def test_agent_level_tool_persistence(): """Test that agent-level tools persist across multiple runs with OpenAI Assistants Client.""" async with Agent( - client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + client=OpenAIAssistantsClient(model=INTEGRATION_TEST_MODEL), instructions="You are a helpful assistant that uses available tools.", tools=[get_weather], # Agent-level tool ) as agent: @@ -1570,10 +1570,10 @@ def test_with_callable_api_key() -> None: async def get_api_key() -> str: return "test-api-key-123" - client = OpenAIAssistantsClient(model_id="gpt-4o", api_key=get_api_key) + client = OpenAIAssistantsClient(model="gpt-4o", api_key=get_api_key) # Verify client was created successfully - assert client.model_id == "gpt-4o" + assert client.model == "gpt-4o" # OpenAI SDK now manages callable API keys internally assert client.client is not None diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py similarity index 91% rename from python/packages/core/tests/openai/test_openai_responses_client.py rename to python/packages/openai/tests/openai/test_openai_chat_client.py index 6a2c9f5173..a29fbf443a 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -9,24 +9,6 @@ from unittest.mock import MagicMock, patch import pytest -from openai import BadRequestError -from openai.types.responses.response_reasoning_item import Summary -from openai.types.responses.response_reasoning_summary_text_delta_event import ( - ResponseReasoningSummaryTextDeltaEvent, -) -from openai.types.responses.response_reasoning_summary_text_done_event import ( - ResponseReasoningSummaryTextDoneEvent, -) -from openai.types.responses.response_reasoning_text_delta_event import ( - ResponseReasoningTextDeltaEvent, -) -from openai.types.responses.response_reasoning_text_done_event import ( - ResponseReasoningTextDoneEvent, -) -from openai.types.responses.response_text_delta_event import ResponseTextDeltaEvent -from pydantic import BaseModel -from pytest import param - from agent_framework import ( Agent, ChatOptions, @@ -47,9 +29,27 @@ ChatClientException, ChatClientInvalidRequestException, ) -from agent_framework.openai import OpenAIResponsesClient -from agent_framework.openai._exceptions import OpenAIContentFilterException -from agent_framework.openai._responses_client import OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY +from openai import BadRequestError +from openai.types.responses.response_reasoning_item import Summary +from openai.types.responses.response_reasoning_summary_text_delta_event import ( + ResponseReasoningSummaryTextDeltaEvent, +) +from openai.types.responses.response_reasoning_summary_text_done_event import ( + ResponseReasoningSummaryTextDoneEvent, +) +from openai.types.responses.response_reasoning_text_delta_event import ( + ResponseReasoningTextDeltaEvent, +) +from openai.types.responses.response_reasoning_text_done_event import ( + ResponseReasoningTextDoneEvent, +) +from openai.types.responses.response_text_delta_event import ResponseTextDeltaEvent +from pydantic import BaseModel +from pytest import param + +from agent_framework_openai import OpenAIChatClient +from agent_framework_openai._chat_client import OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY +from agent_framework_openai._exceptions import OpenAIContentFilterException skip_if_openai_integration_tests_disabled = pytest.mark.skipif( os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"), @@ -65,7 +65,7 @@ class OutputStruct(BaseModel): async def create_vector_store( - client: OpenAIResponsesClient, + client: OpenAIChatClient, ) -> tuple[str, Content]: """Create a vector store with sample documents for testing.""" file = await client.client.files.create( @@ -87,7 +87,7 @@ async def create_vector_store( return file.id, Content.from_hosted_vector_store(vector_store_id=vector_store.id) -async def delete_vector_store(client: OpenAIResponsesClient, file_id: str, vector_store_id: str) -> None: +async def delete_vector_store(client: OpenAIChatClient, file_id: str, vector_store_id: str) -> None: """Delete the vector store after tests.""" await client.client.vector_stores.delete(vector_store_id=vector_store_id) @@ -103,24 +103,24 @@ async def get_weather(location: Annotated[str, "The location as a city name"]) - def test_init(openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization - openai_responses_client = OpenAIResponsesClient() + openai_responses_client = OpenAIChatClient() - assert openai_responses_client.model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + assert openai_responses_client.model == openai_unit_test_env["OPENAI_MODEL"] assert isinstance(openai_responses_client, SupportsChatGetResponse) def test_init_validation_fail() -> None: # Test successful initialization with pytest.raises(ValueError): - OpenAIResponsesClient(api_key="34523", model_id={"test": "dict"}) # type: ignore + OpenAIChatClient(api_key="34523", model={"test": "dict"}) # type: ignore def test_init_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization model_id = "test_model_id" - openai_responses_client = OpenAIResponsesClient(model_id=model_id) + openai_responses_client = OpenAIChatClient(model=model_id) - assert openai_responses_client.model_id == model_id + assert openai_responses_client.model == model_id assert isinstance(openai_responses_client, SupportsChatGetResponse) @@ -128,11 +128,11 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: default_headers = {"X-Unit-Test": "test-guid"} # Test successful initialization - openai_responses_client = OpenAIResponsesClient( + openai_responses_client = OpenAIChatClient( default_headers=default_headers, ) - assert openai_responses_client.model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + assert openai_responses_client.model == openai_unit_test_env["OPENAI_MODEL"] assert isinstance(openai_responses_client, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers @@ -141,10 +141,10 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: assert openai_responses_client.client.default_headers[key] == value -@pytest.mark.parametrize("exclude_list", [["OPENAI_RESPONSES_MODEL_ID"]], indirect=True) +@pytest.mark.parametrize("exclude_list", [["OPENAI_MODEL"]], indirect=True) def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None: with pytest.raises(ValueError): - OpenAIResponsesClient() + OpenAIChatClient() @pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True) @@ -152,8 +152,8 @@ def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None: model_id = "test_model_id" with pytest.raises(ValueError): - OpenAIResponsesClient( - model_id=model_id, + OpenAIChatClient( + model=model_id, ) @@ -161,14 +161,14 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None: default_headers = {"X-Unit-Test": "test-guid"} settings = { - "model_id": openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"], + "model": openai_unit_test_env["OPENAI_MODEL"], "api_key": openai_unit_test_env["OPENAI_API_KEY"], "default_headers": default_headers, } - openai_responses_client = OpenAIResponsesClient.from_dict(settings) + openai_responses_client = OpenAIChatClient.from_dict(settings) dumped_settings = openai_responses_client.to_dict() - assert dumped_settings["model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + assert dumped_settings["model"] == openai_unit_test_env["OPENAI_MODEL"] # Assert that the default header we added is present in the dumped_settings default headers for key, value in default_headers.items(): assert key in dumped_settings["default_headers"] @@ -179,14 +179,14 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None: def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None: settings = { - "model_id": openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"], + "model": openai_unit_test_env["OPENAI_MODEL"], "api_key": openai_unit_test_env["OPENAI_API_KEY"], "org_id": openai_unit_test_env["OPENAI_ORG_ID"], } - openai_responses_client = OpenAIResponsesClient.from_dict(settings) + openai_responses_client = OpenAIChatClient.from_dict(settings) dumped_settings = openai_responses_client.to_dict() - assert dumped_settings["model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + assert dumped_settings["model"] == openai_unit_test_env["OPENAI_MODEL"] assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"] # Assert that the 'User-Agent' header is not present in the dumped_settings default headers assert "User-Agent" not in dumped_settings.get("default_headers", {}) @@ -195,7 +195,7 @@ def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None: async def test_get_response_with_invalid_input() -> None: """Test get_response with invalid inputs to trigger exception handling.""" - client = OpenAIResponsesClient(model_id="invalid-model", api_key="test-key") + client = OpenAIChatClient(model="invalid-model", api_key="test-key") # Test with empty messages which should trigger ChatClientInvalidRequestException with pytest.raises(ChatClientInvalidRequestException, match="Messages are required"): @@ -204,7 +204,7 @@ async def test_get_response_with_invalid_input() -> None: async def test_get_response_with_all_parameters() -> None: """Test get_response with all possible parameters to cover parameter handling logic.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test with comprehensive parameter set - should fail due to invalid API key with pytest.raises(ChatClientException): await client.get_response( @@ -214,7 +214,7 @@ async def test_get_response_with_all_parameters() -> None: "instructions": "You are a helpful assistant", "max_tokens": 100, "parallel_tool_calls": True, - "model_id": "gpt-4", + "model": "gpt-4", "previous_response_id": "prev-123", "reasoning": {"chain_of_thought": "enabled"}, "service_tier": "auto", @@ -236,10 +236,10 @@ async def test_get_response_with_all_parameters() -> None: @pytest.mark.asyncio async def test_web_search_tool_with_location() -> None: """Test web search tool with location parameters.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test web search tool with location using static method - web_search_tool = OpenAIResponsesClient.get_web_search_tool( + web_search_tool = OpenAIChatClient.get_web_search_tool( user_location={ "city": "Seattle", "country": "US", @@ -258,10 +258,10 @@ async def test_web_search_tool_with_location() -> None: async def test_code_interpreter_tool_variations() -> None: """Test HostedCodeInterpreterTool with and without file inputs.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test code interpreter using static method - code_tool = OpenAIResponsesClient.get_code_interpreter_tool() + code_tool = OpenAIChatClient.get_code_interpreter_tool() with pytest.raises(ChatClientException): await client.get_response( @@ -270,7 +270,7 @@ async def test_code_interpreter_tool_variations() -> None: ) # Test code interpreter with files using static method - code_tool_with_files = OpenAIResponsesClient.get_code_interpreter_tool(file_ids=["file1", "file2"]) + code_tool_with_files = OpenAIChatClient.get_code_interpreter_tool(file_ids=["file1", "file2"]) with pytest.raises(ChatClientException): await client.get_response( @@ -281,7 +281,7 @@ async def test_code_interpreter_tool_variations() -> None: async def test_content_filter_exception() -> None: """Test that content filter errors in get_response are properly handled.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Mock a BadRequestError with content_filter code mock_error = BadRequestError( @@ -302,10 +302,10 @@ async def test_content_filter_exception() -> None: async def test_hosted_file_search_tool_validation() -> None: """Test get_response HostedFileSearchTool validation.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test file search tool with vector store IDs - file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=["vs_123"]) + file_search_tool = OpenAIChatClient.get_file_search_tool(vector_store_ids=["vs_123"]) # Test using file search tool - may raise various exceptions depending on API response with pytest.raises((ValueError, ChatClientInvalidRequestException, ChatClientException)): @@ -317,7 +317,7 @@ async def test_hosted_file_search_tool_validation() -> None: async def test_chat_message_parsing_with_function_calls() -> None: """Test get_response message preparation with function call and result content types in conversation flow.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Create messages with function call and result content function_call = Content.from_function_call( @@ -342,7 +342,7 @@ async def test_chat_message_parsing_with_function_calls() -> None: async def test_response_format_parse_path() -> None: """Test get_response response_format parsing path.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Mock successful parse response mock_parsed_response = MagicMock() @@ -363,12 +363,12 @@ async def test_response_format_parse_path() -> None: ) assert response.response_id == "parsed_response_123" assert response.conversation_id == "parsed_response_123" - assert response.model_id == "test-model" + assert response.model == "test-model" async def test_response_format_parse_path_with_conversation_id() -> None: """Test get_response response_format parsing path with set conversation ID.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Mock successful parse response mock_parsed_response = MagicMock() @@ -390,12 +390,12 @@ async def test_response_format_parse_path_with_conversation_id() -> None: ) assert response.response_id == "parsed_response_123" assert response.conversation_id == "conversation_456" - assert response.model_id == "test-model" + assert response.model == "test-model" async def test_bad_request_error_non_content_filter() -> None: """Test get_response BadRequestError without content_filter.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Mock a BadRequestError without content_filter code mock_error = BadRequestError( @@ -417,7 +417,7 @@ async def test_bad_request_error_non_content_filter() -> None: async def test_streaming_content_filter_exception_handling() -> None: """Test that content filter errors in get_response(..., stream=True) are properly handled.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Mock the OpenAI client to raise a BadRequestError with content_filter code with patch.object(client.client.responses, "create") as mock_create: @@ -436,7 +436,7 @@ async def test_streaming_content_filter_exception_handling() -> None: def test_response_content_creation_with_annotations() -> None: """Test _parse_response_from_openai with different annotation types.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Create a mock response with annotated text content mock_response = MagicMock() @@ -476,7 +476,7 @@ def test_response_content_creation_with_annotations() -> None: def test_response_content_creation_with_refusal() -> None: """Test _parse_response_from_openai with refusal content.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Create a mock response with refusal content mock_response = MagicMock() @@ -506,7 +506,7 @@ def test_response_content_creation_with_refusal() -> None: def test_response_content_creation_with_reasoning() -> None: """Test _parse_response_from_openai with reasoning content.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Create a mock response with reasoning content mock_response = MagicMock() @@ -536,7 +536,7 @@ def test_response_content_creation_with_reasoning() -> None: def test_response_content_keeps_reasoning_and_function_calls_in_one_message() -> None: """Reasoning + function calls should parse into one assistant message.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.output_parsed = None @@ -589,7 +589,7 @@ def test_response_content_keeps_reasoning_and_function_calls_in_one_message() -> def test_response_content_creation_with_code_interpreter() -> None: """Test _parse_response_from_openai with code interpreter outputs.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Create a mock response with code interpreter outputs mock_response = MagicMock() @@ -630,7 +630,7 @@ def test_response_content_creation_with_code_interpreter() -> None: def test_get_shell_tool_basic() -> None: """Test get_shell_tool returns hosted shell config with default auto environment.""" - tool = OpenAIResponsesClient.get_shell_tool() + tool = OpenAIChatClient.get_shell_tool() assert tool.type == "shell" assert tool.environment.type == "container_auto" @@ -638,7 +638,7 @@ def test_get_shell_tool_basic() -> None: def test_get_shell_tool_rejects_local_without_func() -> None: """Local environment requires a local function executor.""" with pytest.raises(ValueError, match="Local shell requires func"): - OpenAIResponsesClient.get_shell_tool(environment={"type": "local"}) + OpenAIChatClient.get_shell_tool(environment={"type": "local"}) def test_get_shell_tool_rejects_environment_config_with_func() -> None: @@ -648,7 +648,7 @@ def local_exec(command: str) -> str: return command with pytest.raises(ValueError, match="environment config is not supported"): - OpenAIResponsesClient.get_shell_tool( + OpenAIChatClient.get_shell_tool( func=local_exec, environment={"type": "container_auto"}, ) @@ -656,12 +656,12 @@ def local_exec(command: str) -> str: def test_get_shell_tool_local_executor_maps_to_shell_tool() -> None: """Test local shell FunctionTool maps to OpenAI shell tool declaration.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") def local_exec(command: str) -> str: return command - local_shell_tool = OpenAIResponsesClient.get_shell_tool( + local_shell_tool = OpenAIChatClient.get_shell_tool( func=local_exec, approval_mode="never_require", ) @@ -680,7 +680,7 @@ def test_get_shell_tool_reuses_function_tool_instance() -> None: def run_shell(command: str) -> str: return command - shell_tool = OpenAIResponsesClient.get_shell_tool( + shell_tool = OpenAIChatClient.get_shell_tool( func=run_shell, description="Run local shell command", approval_mode="always_require", @@ -695,12 +695,12 @@ def run_shell(command: str) -> str: def test_response_content_creation_with_local_shell_call_maps_to_function_call() -> None: """Test local_shell_call is translated into function_call for invocation loop.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") def local_exec(command: str) -> str: return command - local_shell_tool = OpenAIResponsesClient.get_shell_tool(func=local_exec) + local_shell_tool = OpenAIChatClient.get_shell_tool(func=local_exec) mock_response = MagicMock() mock_response.output_parsed = None @@ -738,14 +738,14 @@ def local_exec(command: str) -> str: @pytest.mark.asyncio async def test_local_shell_tool_is_invoked_in_function_loop() -> None: """Test local shell call executes executor and sends local_shell_call_output.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") executed_commands: list[str] = [] def local_exec(command: str) -> str: executed_commands.append(command) return "Python 3.13.0" - local_shell_tool = OpenAIResponsesClient.get_shell_tool( + local_shell_tool = OpenAIChatClient.get_shell_tool( func=local_exec, approval_mode="never_require", ) @@ -810,14 +810,14 @@ def local_exec(command: str) -> str: @pytest.mark.asyncio async def test_shell_call_is_invoked_as_local_shell_function_loop() -> None: """Test shell_call maps to local function invocation and returns shell_call_output.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") executed_commands: list[str] = [] def local_exec(command: str) -> str: executed_commands.append(command) return "Python 3.13.0" - local_shell_tool = OpenAIResponsesClient.get_shell_tool( + local_shell_tool = OpenAIChatClient.get_shell_tool( func=local_exec, approval_mode="never_require", ) @@ -885,7 +885,7 @@ def local_exec(command: str) -> str: def test_response_content_creation_with_shell_call() -> None: """Test _parse_response_from_openai with shell_call output.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.output_parsed = None @@ -924,7 +924,7 @@ def test_response_content_creation_with_shell_call() -> None: def test_response_content_creation_with_shell_call_output() -> None: """Test _parse_response_from_openai with shell_call_output output.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.output_parsed = None @@ -970,7 +970,7 @@ def test_response_content_creation_with_shell_call_output() -> None: def test_response_content_creation_with_shell_call_timeout() -> None: """Test _parse_response_from_openai with shell_call_output that timed out.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.output_parsed = None @@ -1010,7 +1010,7 @@ def test_response_content_creation_with_shell_call_timeout() -> None: def test_response_content_creation_with_function_call() -> None: """Test _parse_response_from_openai with function call content.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Create a mock response with function call mock_response = MagicMock() @@ -1042,7 +1042,7 @@ def test_response_content_creation_with_function_call() -> None: def test_prepare_content_for_opentool_approval_response() -> None: """Test _prepare_content_for_openai with function approval response content.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test approved response function_call = Content.from_function_call( @@ -1065,7 +1065,7 @@ def test_prepare_content_for_opentool_approval_response() -> None: def test_prepare_content_for_openai_error_content() -> None: """Test _prepare_content_for_openai with error content.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") error_content = Content.from_error( message="Operation failed", @@ -1081,7 +1081,7 @@ def test_prepare_content_for_openai_error_content() -> None: def test_prepare_content_for_openai_usage_content() -> None: """Test _prepare_content_for_openai with usage content.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") usage_content = Content.from_usage( usage_details={ @@ -1099,7 +1099,7 @@ def test_prepare_content_for_openai_usage_content() -> None: def test_prepare_content_for_openai_hosted_vector_store_content() -> None: """Test _prepare_content_for_openai with hosted vector store content.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") vector_store_content = Content.from_hosted_vector_store( vector_store_id="vs_123", @@ -1113,7 +1113,7 @@ def test_prepare_content_for_openai_hosted_vector_store_content() -> None: def test_prepare_content_for_openai_text_uses_role_specific_type() -> None: """Text content should use input_text for user and output_text for assistant.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") text_content = Content.from_text(text="hello") @@ -1129,7 +1129,7 @@ def test_prepare_content_for_openai_text_uses_role_specific_type() -> None: def test_prepare_messages_for_openai_assistant_history_uses_output_text_with_annotations() -> None: """Assistant history should be output_text and include required annotations.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") messages = [ Message(role="user", text="What is async/await?"), @@ -1147,7 +1147,7 @@ def test_prepare_messages_for_openai_assistant_history_uses_output_text_with_ann def test_parse_response_from_openai_with_mcp_server_tool_result() -> None: """Test _parse_response_from_openai with MCP server tool result.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.output_parsed = None @@ -1186,7 +1186,7 @@ def test_parse_response_from_openai_with_mcp_server_tool_result() -> None: def test_parse_chunk_from_openai_with_mcp_call_result() -> None: """Test _parse_chunk_from_openai with MCP call output.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Mock event with MCP call that has output mock_event = MagicMock() @@ -1225,7 +1225,7 @@ def test_parse_chunk_from_openai_with_mcp_call_result() -> None: def test_prepare_message_for_openai_with_function_approval_response() -> None: """Test _prepare_message_for_openai with function approval response content in messages.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") function_call = Content.from_function_call( call_id="call_789", @@ -1258,7 +1258,7 @@ def test_prepare_message_for_openai_includes_reasoning_with_function_call() -> N function_call items are included. Stripping reasoning causes a 400 error: "function_call was provided without its required reasoning item". """ - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") reasoning = Content.from_text_reasoning( id="rs_abc123", @@ -1292,7 +1292,7 @@ def test_prepare_messages_for_openai_full_conversation_with_reasoning() -> None: This simulates the conversation history passed between agents in a workflow. The API requires reasoning items alongside function_calls. """ - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") messages = [ Message(role="user", contents=[Content.from_text(text="search for hotels")]), @@ -1351,7 +1351,7 @@ def test_prepare_messages_for_openai_full_conversation_with_reasoning() -> None: def test_prepare_message_for_openai_filters_error_content() -> None: """Test that error content in messages is handled properly.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") error_content = Content.from_error( message="Test error", @@ -1368,7 +1368,7 @@ def test_prepare_message_for_openai_filters_error_content() -> None: def test_chat_message_with_usage_content() -> None: """Test that usage content in messages is handled properly.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") usage_content = Content.from_usage( usage_details={ @@ -1388,7 +1388,7 @@ def test_chat_message_with_usage_content() -> None: def test_hosted_file_content_preparation() -> None: """Test _prepare_content_for_openai with hosted file content.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") hosted_file = Content.from_hosted_file( file_id="file_abc123", @@ -1403,7 +1403,7 @@ def test_hosted_file_content_preparation() -> None: def test_function_approval_response_with_mcp_tool_call() -> None: """Test function approval response content with MCP server tool call content.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mcp_call = Content.from_mcp_server_tool_call( call_id="mcp_call_999", @@ -1427,7 +1427,7 @@ def test_function_approval_response_with_mcp_tool_call() -> None: def test_response_format_with_conflicting_definitions() -> None: """Test that conflicting response_format definitions raise an error.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Mock response_format and text_config that conflict response_format = { @@ -1445,7 +1445,7 @@ def test_response_format_with_conflicting_definitions() -> None: def test_response_format_json_object_type() -> None: """Test response_format with json_object type.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") response_format = {"type": "json_object"} @@ -1457,7 +1457,7 @@ def test_response_format_json_object_type() -> None: def test_response_format_text_type() -> None: """Test response_format with text type.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") response_format = {"type": "text"} @@ -1469,7 +1469,7 @@ def test_response_format_text_type() -> None: def test_response_format_with_format_key() -> None: """Test response_format that already has a format key.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") response_format = { "format": { @@ -1488,7 +1488,7 @@ def test_response_format_with_format_key() -> None: def test_response_format_json_schema_no_name_uses_title() -> None: """Test json_schema response_format without name uses title from schema.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") response_format = { "type": "json_schema", @@ -1503,7 +1503,7 @@ def test_response_format_json_schema_no_name_uses_title() -> None: def test_response_format_json_schema_with_strict() -> None: """Test json_schema response_format with strict mode.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") response_format = { "type": "json_schema", @@ -1522,7 +1522,7 @@ def test_response_format_json_schema_with_strict() -> None: def test_response_format_json_schema_with_description() -> None: """Test json_schema response_format with description.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") response_format = { "type": "json_schema", @@ -1541,7 +1541,7 @@ def test_response_format_json_schema_with_description() -> None: def test_response_format_json_schema_missing_schema() -> None: """Test json_schema response_format without schema raises error.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") response_format = {"type": "json_schema", "json_schema": {"name": "NoSchema"}} @@ -1554,7 +1554,7 @@ def test_response_format_json_schema_missing_schema() -> None: def test_response_format_unsupported_type() -> None: """Test unsupported response_format type raises error.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") response_format = {"type": "unsupported_format"} @@ -1564,7 +1564,7 @@ def test_response_format_unsupported_type() -> None: def test_response_format_invalid_type() -> None: """Test invalid response_format type raises error.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") response_format = "invalid" # Not a Pydantic model or mapping @@ -1577,7 +1577,7 @@ def test_response_format_invalid_type() -> None: def test_parse_response_with_store_false() -> None: """Test _get_conversation_id returns None when store is False.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.id = "resp_123" @@ -1591,7 +1591,7 @@ def test_parse_response_with_store_false() -> None: def test_parse_response_uses_response_id_when_no_conversation() -> None: """Test _get_conversation_id returns response ID when no conversation exists.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.id = "resp_789" @@ -1604,7 +1604,7 @@ def test_parse_response_uses_response_id_when_no_conversation() -> None: def test_streaming_chunk_with_usage_only() -> None: """Test streaming chunk that only contains usage info.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -1631,10 +1631,10 @@ def test_streaming_chunk_with_usage_only() -> None: def test_prepare_tools_for_openai_with_mcp() -> None: """Test that MCP tool dict is converted to the correct response tool dict.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Use static method to create MCP tool - tool = OpenAIResponsesClient.get_mcp_tool( + tool = OpenAIChatClient.get_mcp_tool( name="My_MCP", url="https://mcp.example", allowed_tools=["tool_a", "tool_b"], @@ -1659,7 +1659,7 @@ def test_prepare_tools_for_openai_with_mcp() -> None: def test_prepare_tools_for_openai_single_function_tool() -> None: """Test that a single FunctionTool (not wrapped in a list) is handled correctly.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") @tool def hello(name: str) -> str: @@ -1684,9 +1684,9 @@ def hello(name: str) -> str: def test_prepare_tools_for_openai_single_dict_tool() -> None: """Test that a single dict tool (not wrapped in a list) is handled correctly.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") - web_tool = OpenAIResponsesClient.get_web_search_tool(search_context_size="low") + web_tool = OpenAIChatClient.get_web_search_tool(search_context_size="low") resp_tools = client._prepare_tools_for_openai(web_tool) assert isinstance(resp_tools, list) assert len(resp_tools) == 1 @@ -1696,7 +1696,7 @@ def test_prepare_tools_for_openai_single_dict_tool() -> None: def test_prepare_tools_for_openai_none() -> None: """Test that passing None returns an empty list.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") resp_tools = client._prepare_tools_for_openai(None) assert isinstance(resp_tools, list) @@ -1705,7 +1705,7 @@ def test_prepare_tools_for_openai_none() -> None: def test_parse_response_from_openai_with_mcp_approval_request() -> None: """Test that a non-streaming mcp_approval_request is parsed into FunctionApprovalRequestContent.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.output_parsed = None @@ -1742,7 +1742,7 @@ def test_responses_client_created_at_uses_utc( This is a regression test for the issue where created_at was using local time but labeling it as UTC (with 'Z' suffix). """ - client = OpenAIResponsesClient() + client = OpenAIChatClient() # Use a specific Unix timestamp: 1733011890 = 2024-12-01T00:31:30Z (UTC) utc_timestamp = 1733011890 @@ -1783,7 +1783,7 @@ def test_responses_client_created_at_uses_utc( def test_prepare_tools_for_openai_with_raw_image_generation() -> None: """Test that raw image_generation tool dict is handled correctly with parameter mapping.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test with raw tool dict using OpenAI parameters directly tool = { @@ -1809,7 +1809,7 @@ def test_prepare_tools_for_openai_with_raw_image_generation() -> None: def test_prepare_tools_for_openai_with_raw_image_generation_openai_responses_params() -> None: """Test raw image_generation tool with OpenAI-specific parameters.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test with OpenAI-specific parameters tool = { @@ -1841,7 +1841,7 @@ def test_prepare_tools_for_openai_with_raw_image_generation_openai_responses_par def test_prepare_tools_for_openai_with_raw_image_generation_minimal() -> None: """Test raw image_generation tool with minimal configuration.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test with minimal parameters (just type) tool = {"type": "image_generation"} @@ -1859,10 +1859,10 @@ def test_prepare_tools_for_openai_with_raw_image_generation_minimal() -> None: def test_prepare_tools_for_openai_with_image_generation_options() -> None: """Test image generation tool conversion with options.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Use static method to create image generation tool - tool = OpenAIResponsesClient.get_image_generation_tool( + tool = OpenAIChatClient.get_image_generation_tool( output_format="png", size="512x512", quality="high", @@ -1879,9 +1879,9 @@ def test_prepare_tools_for_openai_with_image_generation_options() -> None: def test_prepare_tools_for_openai_with_custom_image_generation_model() -> None: """Test image generation tool conversion with a custom model string.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") - tool = OpenAIResponsesClient.get_image_generation_tool(model="custom-image-model") + tool = OpenAIChatClient.get_image_generation_tool(model="custom-image-model") resp_tools = client._prepare_tools_for_openai([tool]) assert len(resp_tools) == 1 @@ -1892,7 +1892,7 @@ def test_prepare_tools_for_openai_with_custom_image_generation_model() -> None: def test_parse_chunk_from_openai_with_mcp_approval_request() -> None: """Test that a streaming mcp_approval_request event is parsed into FunctionApprovalRequestContent.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -1919,7 +1919,7 @@ async def test_end_to_end_mcp_approval_flow(span_exporter) -> None: """End-to-end mocked test: model issues an mcp_approval_request, user approves, client sends mcp_approval_response. """ - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # First mocked response: model issues an mcp_approval_request mock_response1 = MagicMock() @@ -1973,7 +1973,7 @@ async def test_end_to_end_mcp_approval_flow(span_exporter) -> None: def test_usage_details_basic() -> None: """Test _parse_usage_from_openai without cached or reasoning tokens.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_usage = MagicMock() mock_usage.input_tokens = 100 @@ -1991,7 +1991,7 @@ def test_usage_details_basic() -> None: def test_usage_details_with_cached_tokens() -> None: """Test _parse_usage_from_openai with cached input tokens.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_usage = MagicMock() mock_usage.input_tokens = 200 @@ -2009,7 +2009,7 @@ def test_usage_details_with_cached_tokens() -> None: def test_usage_details_with_reasoning_tokens() -> None: """Test _parse_usage_from_openai with reasoning tokens.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_usage = MagicMock() mock_usage.input_tokens = 150 @@ -2027,7 +2027,7 @@ def test_usage_details_with_reasoning_tokens() -> None: def test_get_metadata_from_response() -> None: """Test the _get_metadata_from_response method.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test with logprobs mock_output_with_logprobs = MagicMock() @@ -2047,7 +2047,7 @@ def test_get_metadata_from_response() -> None: def test_streaming_response_basic_structure() -> None: """Test that _parse_chunk_from_openai returns proper structure.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions(store=True) function_call_ids: dict[int, tuple[str, str]] = {} @@ -2059,14 +2059,14 @@ def test_streaming_response_basic_structure() -> None: # Should get a valid ChatResponseUpdate structure assert isinstance(response, ChatResponseUpdate) assert response.role == "assistant" - assert response.model_id == "test-model" + assert response.model == "test-model" assert isinstance(response.contents, list) assert response.raw_representation is mock_event def test_streaming_response_created_type() -> None: """Test streaming response with created type""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2085,7 +2085,7 @@ def test_streaming_response_created_type() -> None: def test_streaming_response_in_progress_type() -> None: """Test streaming response with in_progress type""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2104,7 +2104,7 @@ def test_streaming_response_in_progress_type() -> None: def test_streaming_annotation_added_with_file_path() -> None: """Test streaming annotation added event with file_path type extracts HostedFileContent.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2130,7 +2130,7 @@ def test_streaming_annotation_added_with_file_path() -> None: def test_streaming_annotation_added_with_file_citation() -> None: """Test streaming annotation added event with file_citation type extracts HostedFileContent.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2157,7 +2157,7 @@ def test_streaming_annotation_added_with_file_citation() -> None: def test_streaming_annotation_added_with_container_file_citation() -> None: """Test streaming annotation added event with container_file_citation type.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2188,7 +2188,7 @@ def test_streaming_annotation_added_with_container_file_citation() -> None: def test_streaming_annotation_added_with_unknown_type() -> None: """Test streaming annotation added event with unknown type is ignored.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2208,7 +2208,7 @@ def test_streaming_annotation_added_with_unknown_type() -> None: async def test_service_response_exception_includes_original_error_details() -> None: """Test that ChatClientException messages include original error details in the new format.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") messages = [Message(role="user", text="test message")] mock_response = MagicMock() @@ -2233,7 +2233,7 @@ async def test_service_response_exception_includes_original_error_details() -> N async def test_get_response_streaming_with_response_format() -> None: """Test get_response streaming with response_format.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") messages = [Message(role="user", text="Test streaming with format")] # It will fail due to invalid API key, but exercises the code path @@ -2252,7 +2252,7 @@ async def run_streaming(): def test_prepare_content_for_openai_image_content() -> None: """Test _prepare_content_for_openai with image content variations.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test image content with detail parameter and file_id image_content_with_detail = Content.from_uri( @@ -2276,7 +2276,7 @@ def test_prepare_content_for_openai_image_content() -> None: def test_prepare_content_for_openai_audio_content() -> None: """Test _prepare_content_for_openai with audio content variations.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test WAV audio content wav_content = Content.from_uri(uri="data:audio/wav;base64,abc123", media_type="audio/wav") @@ -2294,7 +2294,7 @@ def test_prepare_content_for_openai_audio_content() -> None: def test_prepare_content_for_openai_unsupported_content() -> None: """Test _prepare_content_for_openai with unsupported content types.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test unsupported audio format unsupported_audio = Content.from_uri(uri="data:audio/ogg;base64,ghi789", media_type="audio/ogg") @@ -2309,7 +2309,7 @@ def test_prepare_content_for_openai_unsupported_content() -> None: def test_prepare_content_for_openai_function_result_with_rich_items() -> None: """Test _prepare_content_for_openai with function_result containing rich items.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") image_content = Content.from_data(data=b"image_bytes", media_type="image/png") content = Content.from_function_result( @@ -2332,7 +2332,7 @@ def test_prepare_content_for_openai_function_result_with_rich_items() -> None: def test_prepare_content_for_openai_function_result_without_items() -> None: """Test _prepare_content_for_openai with plain string function_result.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") content = Content.from_function_result( call_id="call_plain", @@ -2348,7 +2348,7 @@ def test_prepare_content_for_openai_function_result_without_items() -> None: def test_parse_chunk_from_openai_code_interpreter() -> None: """Test _parse_chunk_from_openai with code_interpreter_call.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2372,7 +2372,7 @@ def test_parse_chunk_from_openai_code_interpreter() -> None: def test_parse_chunk_from_openai_code_interpreter_delta() -> None: """Test _parse_chunk_from_openai with code_interpreter_call_code delta events.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2401,7 +2401,7 @@ def test_parse_chunk_from_openai_code_interpreter_delta() -> None: def test_parse_chunk_from_openai_code_interpreter_done() -> None: """Test _parse_chunk_from_openai with code_interpreter_call_code done event.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2430,7 +2430,7 @@ def test_parse_chunk_from_openai_code_interpreter_done() -> None: def test_parse_chunk_from_openai_reasoning() -> None: """Test _parse_chunk_from_openai with reasoning content.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2454,7 +2454,7 @@ def test_parse_chunk_from_openai_reasoning() -> None: def test_prepare_content_for_openai_text_reasoning_comprehensive() -> None: """Test _prepare_content_for_openai with TextReasoningContent all additional properties.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test TextReasoningContent with all additional properties comprehensive_reasoning = Content.from_text_reasoning( @@ -2478,7 +2478,7 @@ def test_prepare_content_for_openai_text_reasoning_comprehensive() -> None: def test_streaming_reasoning_text_delta_event() -> None: """Test reasoning text delta event creates TextReasoningContent.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2504,7 +2504,7 @@ def test_streaming_reasoning_text_delta_event() -> None: def test_streaming_reasoning_text_done_event() -> None: """Test reasoning text done event creates TextReasoningContent with complete text.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2530,7 +2530,7 @@ def test_streaming_reasoning_text_done_event() -> None: def test_streaming_reasoning_summary_text_delta_event() -> None: """Test reasoning summary text delta event creates TextReasoningContent.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2555,7 +2555,7 @@ def test_streaming_reasoning_summary_text_delta_event() -> None: def test_streaming_reasoning_summary_text_done_event() -> None: """Test reasoning summary text done event creates TextReasoningContent with complete text.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2581,7 +2581,7 @@ def test_streaming_reasoning_summary_text_done_event() -> None: def test_streaming_reasoning_events_preserve_metadata() -> None: """Test that reasoning events preserve metadata like regular text events.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} @@ -2619,7 +2619,7 @@ def test_streaming_reasoning_events_preserve_metadata() -> None: def test_parse_response_from_openai_image_generation_raw_base64(): """Test image generation response parsing with raw base64 string.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Create a mock response with raw base64 image data (PNG signature) mock_response = MagicMock() @@ -2657,7 +2657,7 @@ def test_parse_response_from_openai_image_generation_raw_base64(): def test_parse_response_from_openai_image_generation_existing_data_uri(): """Test image generation response parsing with existing data URI.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Create a mock response with existing data URI mock_response = MagicMock() @@ -2694,7 +2694,7 @@ def test_parse_response_from_openai_image_generation_existing_data_uri(): def test_parse_response_from_openai_image_generation_format_detection(): """Test different image format detection from base64 data.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Test JPEG detection jpeg_signature = b"\xff\xd8\xff" @@ -2749,7 +2749,7 @@ def test_parse_response_from_openai_image_generation_format_detection(): def test_parse_response_from_openai_image_generation_fallback(): """Test image generation with invalid base64 falls back to PNG.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Create a mock response with invalid base64 mock_response = MagicMock() @@ -2783,7 +2783,7 @@ def test_parse_response_from_openai_image_generation_fallback(): async def test_prepare_options_store_parameter_handling() -> None: - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") messages = [Message(role="user", text="Test message")] test_conversation_id = "test-conversation-123" @@ -2809,7 +2809,7 @@ async def test_prepare_options_store_parameter_handling() -> None: async def test_conversation_id_precedence_kwargs_over_options() -> None: """When both kwargs and options contain conversation_id, kwargs wins.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") messages = [Message(role="user", text="Hello")] # options has a stale response id, kwargs carries the freshest one @@ -2845,7 +2845,7 @@ def _create_mock_responses_text_response(*, response_id: str) -> MagicMock: async def test_instructions_sent_first_turn_then_skipped_for_continuation() -> None: - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = _create_mock_responses_text_response(response_id="resp_123") with patch.object(client.client.responses, "create", return_value=mock_response) as mock_create: @@ -2878,7 +2878,7 @@ async def test_instructions_sent_first_turn_then_skipped_for_continuation() -> N async def test_instructions_not_repeated_for_continuation_ids( conversation_id: str, ) -> None: - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = _create_mock_responses_text_response(response_id="resp_456") with patch.object(client.client.responses, "create", return_value=mock_response) as mock_create: @@ -2894,7 +2894,7 @@ async def test_instructions_not_repeated_for_continuation_ids( async def test_instructions_included_without_conversation_id() -> None: - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = _create_mock_responses_text_response(response_id="resp_new") with patch.object(client.client.responses, "create", return_value=mock_response) as mock_create: @@ -2911,15 +2911,15 @@ async def test_instructions_included_without_conversation_id() -> None: def test_with_callable_api_key() -> None: - """Test OpenAIResponsesClient initialization with callable API key.""" + """Test OpenAIChatClient initialization with callable API key.""" async def get_api_key() -> str: return "test-api-key-123" - client = OpenAIResponsesClient(model_id="gpt-4o", api_key=get_api_key) + client = OpenAIChatClient(model="gpt-4o", api_key=get_api_key) # Verify client was created successfully - assert client.model_id == "gpt-4o" + assert client.model == "gpt-4o" # OpenAI SDK now manages callable API keys internally assert client.client is not None @@ -2945,7 +2945,7 @@ async def get_api_key() -> str: param("stop", ["END"], False, id="stop"), param("allow_multiple_tool_calls", True, False, id="allow_multiple_tool_calls"), param("tool_choice", "none", True, id="tool_choice_none"), - # OpenAIResponsesOptions - just verify they don't fail + # OpenAIChatOptions - just verify they don't fail param("safety_identifier", "user-hash-abc123", False, id="safety_identifier"), param("truncation", "auto", False, id="truncation"), param("top_logprobs", 5, False, id="top_logprobs"), @@ -2998,13 +2998,13 @@ async def test_integration_options( option_value: Any, needs_validation: bool, ) -> None: - """Parametrized test covering all ChatOptions and OpenAIResponsesOptions. + """Parametrized test covering all ChatOptions and OpenAIChatOptions. Tests both streaming and non-streaming modes for each option to ensure they don't cause failures. Options marked with needs_validation also check that the feature actually works correctly. """ - openai_responses_client = OpenAIResponsesClient() + openai_responses_client = OpenAIChatClient() # Need at least 2 iterations for tool_choice tests: one to get function call, one to get final response openai_responses_client.function_invocation_configuration["max_iterations"] = 2 @@ -3075,11 +3075,11 @@ async def test_integration_options( @pytest.mark.integration @skip_if_openai_integration_tests_disabled async def test_integration_web_search() -> None: - client = OpenAIResponsesClient(model_id="gpt-5") + client = OpenAIChatClient(model="gpt-5") for streaming in [False, True]: # Use static method for web search tool - web_search_tool = OpenAIResponsesClient.get_web_search_tool() + web_search_tool = OpenAIChatClient.get_web_search_tool() content = { "messages": [ Message( @@ -3104,7 +3104,7 @@ async def test_integration_web_search() -> None: assert "Zoey" in response.text # Test that the client will use the web search tool with location - web_search_tool_with_location = OpenAIResponsesClient.get_web_search_tool( + web_search_tool_with_location = OpenAIChatClient.get_web_search_tool( user_location={"country": "US", "city": "Seattle"}, ) content = { @@ -3134,13 +3134,13 @@ async def test_integration_web_search() -> None: @pytest.mark.integration @skip_if_openai_integration_tests_disabled async def test_integration_file_search() -> None: - openai_responses_client = OpenAIResponsesClient() + openai_responses_client = OpenAIChatClient() assert isinstance(openai_responses_client, SupportsChatGetResponse) file_id, vector_store = await create_vector_store(openai_responses_client) # Use static method for file search tool - file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) + file_search_tool = OpenAIChatClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) # Test that the client will use the file search tool response = await openai_responses_client.get_response( messages=[ @@ -3168,13 +3168,13 @@ async def test_integration_file_search() -> None: @pytest.mark.integration @skip_if_openai_integration_tests_disabled async def test_integration_streaming_file_search() -> None: - openai_responses_client = OpenAIResponsesClient() + openai_responses_client = OpenAIChatClient() assert isinstance(openai_responses_client, SupportsChatGetResponse) file_id, vector_store = await create_vector_store(openai_responses_client) # Use static method for file search tool - file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) + file_search_tool = OpenAIChatClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) # Test that the client will use the web search tool response = openai_responses_client.get_streaming_response( messages=[ @@ -3217,7 +3217,7 @@ def get_test_image() -> Content: """Return a test image for analysis.""" return Content.from_data(data=image_bytes, media_type="image/jpeg") - client = OpenAIResponsesClient() + client = OpenAIChatClient() client.function_invocation_configuration["max_iterations"] = 2 for streaming in [False, True]: @@ -3254,7 +3254,7 @@ async def test_integration_agent_replays_local_tool_history_without_stale_fc_id( async def search_hotels(city: Annotated[str, "The city to search for hotels in"]) -> str: return f"The only hotel option in {city} is {hotel_code}." - client = OpenAIResponsesClient() + client = OpenAIChatClient() client.function_invocation_configuration["max_iterations"] = 2 agent = Agent( @@ -3291,7 +3291,7 @@ async def search_hotels(city: Annotated[str, "The city to search for hotels in"] def test_continuation_token_json_serializable() -> None: """Test that OpenAIContinuationToken is a plain dict and JSON-serializable.""" - from agent_framework.openai import OpenAIContinuationToken + from agent_framework_openai import OpenAIContinuationToken token = OpenAIContinuationToken(response_id="resp_abc123") assert token["response_id"] == "resp_abc123" @@ -3304,7 +3304,7 @@ def test_continuation_token_json_serializable() -> None: def test_chat_response_with_continuation_token() -> None: """Test that ChatResponse accepts and stores continuation_token.""" - from agent_framework.openai import OpenAIContinuationToken + from agent_framework_openai import OpenAIContinuationToken token = OpenAIContinuationToken(response_id="resp_123") response = ChatResponse( @@ -3326,7 +3326,7 @@ def test_chat_response_without_continuation_token() -> None: def test_chat_response_update_with_continuation_token() -> None: """Test that ChatResponseUpdate accepts and stores continuation_token.""" - from agent_framework.openai import OpenAIContinuationToken + from agent_framework_openai import OpenAIContinuationToken token = OpenAIContinuationToken(response_id="resp_456") update = ChatResponseUpdate( @@ -3341,7 +3341,8 @@ def test_chat_response_update_with_continuation_token() -> None: def test_agent_response_with_continuation_token() -> None: """Test that AgentResponse accepts and stores continuation_token.""" from agent_framework import AgentResponse - from agent_framework.openai import OpenAIContinuationToken + + from agent_framework_openai import OpenAIContinuationToken token = OpenAIContinuationToken(response_id="resp_789") response = AgentResponse( @@ -3355,7 +3356,8 @@ def test_agent_response_with_continuation_token() -> None: def test_agent_response_update_with_continuation_token() -> None: """Test that AgentResponseUpdate accepts and stores continuation_token.""" from agent_framework import AgentResponseUpdate - from agent_framework.openai import OpenAIContinuationToken + + from agent_framework_openai import OpenAIContinuationToken token = OpenAIContinuationToken(response_id="resp_012") update = AgentResponseUpdate( @@ -3369,7 +3371,7 @@ def test_agent_response_update_with_continuation_token() -> None: def test_parse_response_from_openai_with_background_in_progress() -> None: """Test that _parse_response_from_openai sets continuation_token when status is in_progress.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.output_parsed = None @@ -3394,7 +3396,7 @@ def test_parse_response_from_openai_with_background_in_progress() -> None: def test_parse_response_from_openai_with_background_queued() -> None: """Test that _parse_response_from_openai sets continuation_token when status is queued.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.output_parsed = None @@ -3419,7 +3421,7 @@ def test_parse_response_from_openai_with_background_queued() -> None: def test_parse_response_from_openai_with_background_completed() -> None: """Test that _parse_response_from_openai does NOT set continuation_token when status is completed.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = MagicMock() mock_response.output_parsed = None @@ -3449,7 +3451,7 @@ def test_parse_response_from_openai_with_background_completed() -> None: def test_streaming_response_in_progress_sets_continuation_token() -> None: """Test that _parse_chunk_from_openai sets continuation_token for in_progress events.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options: dict[str, Any] = {} function_call_ids: dict[int, tuple[str, str]] = {} @@ -3469,7 +3471,7 @@ def test_streaming_response_in_progress_sets_continuation_token() -> None: def test_streaming_response_created_with_in_progress_status_sets_continuation_token() -> None: """Test that response.created with in_progress status sets continuation_token.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options: dict[str, Any] = {} function_call_ids: dict[int, tuple[str, str]] = {} @@ -3489,7 +3491,7 @@ def test_streaming_response_created_with_in_progress_status_sets_continuation_to def test_streaming_response_completed_no_continuation_token() -> None: """Test that response.completed does NOT set continuation_token.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") chat_options: dict[str, Any] = {} function_call_ids: dict[int, tuple[str, str]] = {} @@ -3527,11 +3529,11 @@ def test_map_chat_to_agent_update_preserves_continuation_token() -> None: async def test_prepare_options_excludes_continuation_token() -> None: """Test that _prepare_options does not pass continuation_token to OpenAI API.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") messages = [Message(role="user", contents=[Content.from_text(text="Hello")])] options: dict[str, Any] = { - "model_id": "test-model", + "model": "test-model", "continuation_token": {"response_id": "resp_123"}, "background": True, } @@ -3553,7 +3555,7 @@ def test_parse_response_from_openai_function_call_includes_status() -> None: """Test _parse_response_from_openai includes status in function call additional_properties.""" from openai.types.responses import ResponseFunctionToolCall - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") # Create a real ResponseFunctionToolCall object mock_function_call_item = ResponseFunctionToolCall( @@ -3592,7 +3594,7 @@ def test_parse_response_from_openai_function_call_includes_status() -> None: async def test_prepare_messages_for_openai_does_not_replay_fc_id_when_loaded_from_history() -> None: """Loaded history must not replay provider-ephemeral Responses function call IDs.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") provider = InMemoryHistoryProvider() session = AgentSession(session_id="thread-1") @@ -3663,7 +3665,7 @@ async def test_prepare_messages_for_openai_does_not_replay_fc_id_when_loaded_fro def test_prepare_messages_for_openai_keeps_live_fc_id_separate_from_replayed_history() -> None: """Replayed history must not borrow a live Responses function call ID with the same call_id.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") history_message = Message( role="assistant", @@ -3697,7 +3699,7 @@ def test_prepare_messages_for_openai_keeps_live_fc_id_separate_from_replayed_his def test_prepare_messages_for_openai_filters_empty_fc_id() -> None: """Test _prepare_messages_for_openai correctly filters empty fc_id values from call_id_to_id mapping.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") messages = [ Message(role="user", contents=[Content.from_text(text="check hotels")]), @@ -3745,7 +3747,7 @@ def test_prepare_messages_for_openai_filters_empty_fc_id() -> None: def test_prepare_messages_for_openai_filters_none_fc_id() -> None: """Test _prepare_messages_for_openai correctly filters None fc_id values.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + client = OpenAIChatClient(model="test-model", api_key="test-key") messages = [ Message( diff --git a/python/packages/core/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_completion_client.py similarity index 93% rename from python/packages/core/tests/openai/test_openai_chat_client.py rename to python/packages/openai/tests/openai/test_openai_chat_completion_client.py index 86e8b115d6..391432958f 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_completion_client.py @@ -6,12 +6,6 @@ from unittest.mock import MagicMock, patch import pytest -from openai import BadRequestError -from openai.types.chat.chat_completion import ChatCompletion, Choice -from openai.types.chat.chat_completion_message import ChatCompletionMessage -from pydantic import BaseModel -from pytest import param - from agent_framework import ( ChatResponse, Content, @@ -20,8 +14,14 @@ tool, ) from agent_framework.exceptions import ChatClientException -from agent_framework.openai import OpenAIChatClient -from agent_framework.openai._exceptions import OpenAIContentFilterException +from openai import BadRequestError +from openai.types.chat.chat_completion import ChatCompletion, Choice +from openai.types.chat.chat_completion_message import ChatCompletionMessage +from pydantic import BaseModel +from pytest import param + +from agent_framework_openai import OpenAIChatCompletionClient +from agent_framework_openai._exceptions import OpenAIContentFilterException skip_if_openai_integration_tests_disabled = pytest.mark.skipif( os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"), @@ -31,24 +31,24 @@ def test_init(openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization - open_ai_chat_completion = OpenAIChatClient() + open_ai_chat_completion = OpenAIChatCompletionClient() - assert open_ai_chat_completion.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert open_ai_chat_completion.model == openai_unit_test_env["OPENAI_MODEL"] assert isinstance(open_ai_chat_completion, SupportsChatGetResponse) def test_init_validation_fail() -> None: # Test successful initialization with pytest.raises(ValueError): - OpenAIChatClient(api_key="34523", model_id={"test": "dict"}) # type: ignore + OpenAIChatCompletionClient(api_key="34523", model={"test": "dict"}) # type: ignore def test_init_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization model_id = "test_model_id" - open_ai_chat_completion = OpenAIChatClient(model_id=model_id) + open_ai_chat_completion = OpenAIChatCompletionClient(model=model_id) - assert open_ai_chat_completion.model_id == model_id + assert open_ai_chat_completion.model == model_id assert isinstance(open_ai_chat_completion, SupportsChatGetResponse) @@ -56,11 +56,11 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: default_headers = {"X-Unit-Test": "test-guid"} # Test successful initialization - open_ai_chat_completion = OpenAIChatClient( + open_ai_chat_completion = OpenAIChatCompletionClient( default_headers=default_headers, ) - assert open_ai_chat_completion.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert open_ai_chat_completion.model == openai_unit_test_env["OPENAI_MODEL"] assert isinstance(open_ai_chat_completion, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers @@ -71,7 +71,7 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: def test_init_base_url(openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization - open_ai_chat_completion = OpenAIChatClient(base_url="http://localhost:1234/v1") + open_ai_chat_completion = OpenAIChatCompletionClient(base_url="http://localhost:1234/v1") assert str(open_ai_chat_completion.client.base_url) == "http://localhost:1234/v1/" @@ -82,19 +82,19 @@ def test_init_base_url_from_settings_env() -> None: os.environ, { "OPENAI_API_KEY": "dummy", - "OPENAI_CHAT_MODEL_ID": "gpt-5", + "OPENAI_MODEL": "gpt-5", "OPENAI_BASE_URL": "https://custom-openai-endpoint.com/v1", }, ): - client = OpenAIChatClient() - assert client.model_id == "gpt-5" + client = OpenAIChatCompletionClient() + assert client.model == "gpt-5" assert str(client.client.base_url) == "https://custom-openai-endpoint.com/v1/" -@pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_MODEL_ID"]], indirect=True) +@pytest.mark.parametrize("exclude_list", [["OPENAI_MODEL"]], indirect=True) def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None: with pytest.raises(ValueError): - OpenAIChatClient() + OpenAIChatCompletionClient() @pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True) @@ -102,8 +102,8 @@ def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None: model_id = "test_model_id" with pytest.raises(ValueError): - OpenAIChatClient( - model_id=model_id, + OpenAIChatCompletionClient( + model=model_id, ) @@ -111,14 +111,14 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None: default_headers = {"X-Unit-Test": "test-guid"} settings = { - "model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + "model": openai_unit_test_env["OPENAI_MODEL"], "api_key": openai_unit_test_env["OPENAI_API_KEY"], "default_headers": default_headers, } - open_ai_chat_completion = OpenAIChatClient.from_dict(settings) + open_ai_chat_completion = OpenAIChatCompletionClient.from_dict(settings) dumped_settings = open_ai_chat_completion.to_dict() - assert dumped_settings["model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert dumped_settings["model"] == openai_unit_test_env["OPENAI_MODEL"] # Assert that the default header we added is present in the dumped_settings default headers for key, value in default_headers.items(): assert key in dumped_settings["default_headers"] @@ -129,14 +129,14 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None: def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None: settings = { - "model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + "model": openai_unit_test_env["OPENAI_MODEL"], "api_key": openai_unit_test_env["OPENAI_API_KEY"], "org_id": openai_unit_test_env["OPENAI_ORG_ID"], } - open_ai_chat_completion = OpenAIChatClient.from_dict(settings) + open_ai_chat_completion = OpenAIChatCompletionClient.from_dict(settings) dumped_settings = open_ai_chat_completion.to_dict() - assert dumped_settings["model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert dumped_settings["model"] == openai_unit_test_env["OPENAI_MODEL"] assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"] # Assert that the 'User-Agent' header is not present in the dumped_settings default headers assert "User-Agent" not in dumped_settings.get("default_headers", {}) @@ -146,7 +146,7 @@ async def test_content_filter_exception_handling( openai_unit_test_env: dict[str, str], ) -> None: """Test that content filter errors are properly handled.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() messages = [Message(role="user", text="test message")] # Create a mock BadRequestError with content_filter code @@ -168,7 +168,7 @@ async def test_content_filter_exception_handling( def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None: """Test that unsupported tool types are passed through unchanged.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Create a random object that's not a FunctionTool, dict, or callable # This simulates an unsupported tool type that gets passed through @@ -192,7 +192,7 @@ def test_prepare_tools_with_single_function_tool( openai_unit_test_env: dict[str, str], ) -> None: """Test that a single FunctionTool is accepted for tool preparation.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() @tool(approval_mode="never_require") def test_function(query: str) -> str: @@ -224,7 +224,7 @@ def get_weather(location: str) -> str: async def test_exception_message_includes_original_error_details() -> None: """Test that exception messages include original error details in the new format.""" - client = OpenAIChatClient(model_id="test-model", api_key="test-key") + client = OpenAIChatCompletionClient(model="test-model", api_key="test-key") messages = [Message(role="user", text="test message")] mock_response = MagicMock() @@ -284,7 +284,7 @@ def test_chat_response_content_order_text_before_tool_calls( ], ) - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() response = client._parse_response_from_openai(mock_response, {}) # Verify we have both text and tool call content @@ -305,7 +305,7 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s Note: In practice, FunctionTool.invoke() always returns a pre-parsed string. These tests verify that the OpenAI client correctly passes through string results. """ - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Test with empty list serialized as JSON string (pre-serialized result passed to from_function_result) message_with_empty_list = Message( @@ -343,7 +343,7 @@ def test_function_result_exception_handling(openai_unit_test_env: dict[str, str] Feel free to remove this test in case there's another new behavior. """ - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Test with exception (no result) test_exception = ValueError("Test error message") @@ -369,7 +369,7 @@ def test_function_result_with_rich_items_warns_and_omits( ) -> None: """Test that function_result with items logs a warning and omits rich items.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() image_content = Content.from_data(data=b"image_bytes", media_type="image/png") message = Message( role="tool", @@ -381,7 +381,7 @@ def test_function_result_with_rich_items_warns_and_omits( ], ) - with patch("agent_framework.openai._chat_client.logger") as mock_logger: + with patch("agent_framework_openai._chat_completion_client.logger") as mock_logger: openai_messages = client._prepare_message_for_openai(message) # Warning should be logged @@ -409,7 +409,7 @@ def test_prepare_content_for_openai_data_content_image( openai_unit_test_env: dict[str, str], ) -> None: """Test _prepare_content_for_openai converts DataContent with image media type to OpenAI format.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Test DataContent with image media type image_data_content = Content.from_uri( @@ -466,7 +466,7 @@ def test_prepare_content_for_openai_image_url_detail( openai_unit_test_env: dict[str, str], ) -> None: """Test _prepare_content_for_openai includes the detail field in image_url when specified.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Test image with detail set to "high" image_with_detail = Content.from_uri( @@ -559,7 +559,7 @@ def test_prepare_content_for_openai_document_file_mapping( openai_unit_test_env: dict[str, str], ) -> None: """Test _prepare_content_for_openai converts document files (PDF, DOCX, etc.) to OpenAI file format.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Test PDF without filename - should omit filename in OpenAI payload pdf_data_content = Content.from_uri( @@ -668,7 +668,7 @@ def test_parse_text_reasoning_content_from_response( ) -> None: """Test that TextReasoningContent is correctly parsed from OpenAI response with reasoning_details.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Mock response with reasoning_details mock_reasoning_details = { @@ -721,7 +721,7 @@ def test_parse_text_reasoning_content_from_streaming_chunk( from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice from openai.types.chat.chat_completion_chunk import ChoiceDelta as ChunkChoiceDelta - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Mock streaming chunk with reasoning_details mock_reasoning_details = { @@ -767,7 +767,7 @@ def test_prepare_message_with_text_reasoning_content( openai_unit_test_env: dict[str, str], ) -> None: """Test that TextReasoningContent with protected_data is correctly prepared for OpenAI.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Create message with text_reasoning content that has protected_data # text_reasoning is meant to be added to an existing message, so include text content first @@ -806,7 +806,7 @@ def test_prepare_message_with_only_text_reasoning_content( Reasoning models (e.g. gpt-5-mini) may produce reasoning_details without text content, which previously caused an IndexError when preparing messages. """ - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() mock_reasoning_data = { "effort": "high", @@ -840,7 +840,7 @@ def test_prepare_message_with_text_reasoning_before_text( Regression test for https://github.com/microsoft/agent-framework/issues/4384 """ - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() mock_reasoning_data = { "effort": "medium", @@ -876,7 +876,7 @@ def test_prepare_message_with_text_reasoning_before_function_call( Regression test for https://github.com/microsoft/agent-framework/issues/4384 """ - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() mock_reasoning_data = { "effort": "medium", @@ -911,7 +911,7 @@ def test_function_approval_content_is_skipped_in_preparation( openai_unit_test_env: dict[str, str], ) -> None: """Test that function approval request and response content are skipped.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Create approval request function_call = Content.from_function_call( @@ -962,7 +962,7 @@ def test_usage_content_in_streaming_response( from openai.types.chat.chat_completion_chunk import ChatCompletionChunk from openai.types.completion_usage import CompletionUsage - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Mock streaming chunk with usage data (typically last chunk) mock_usage = CompletionUsage( @@ -1008,7 +1008,7 @@ def test_streaming_chunk_with_usage_and_text( ) from openai.types.completion_usage import CompletionUsage - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() mock_chunk = ChatCompletionChunk( id="test-chunk", @@ -1041,7 +1041,7 @@ def test_parse_text_with_refusal(openai_unit_test_env: dict[str, str]) -> None: from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_message import ChatCompletionMessage - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Mock response with refusal mock_response = ChatCompletion( @@ -1074,12 +1074,12 @@ def test_parse_text_with_refusal(openai_unit_test_env: dict[str, str]) -> None: def test_prepare_options_without_model_id(openai_unit_test_env: dict[str, str]) -> None: """Test that prepare_options raises error when model_id is not set.""" - client = OpenAIChatClient() - client.model_id = None # Remove model_id + client = OpenAIChatCompletionClient() + client.model = None # Remove model_id messages = [Message(role="user", text="test")] - with pytest.raises(ValueError, match="model_id must be a non-empty string"): + with pytest.raises(ValueError, match="model must be a non-empty string"): client._prepare_options(messages, {}) @@ -1087,7 +1087,7 @@ def test_prepare_options_without_messages(openai_unit_test_env: dict[str, str]) """Test that prepare_options raises error when messages are missing.""" from agent_framework.exceptions import ChatClientInvalidRequestException - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() with pytest.raises(ChatClientInvalidRequestException, match="Messages are required"): client._prepare_options([], {}) @@ -1097,10 +1097,10 @@ def test_prepare_tools_with_web_search_no_location( openai_unit_test_env: dict[str, str], ) -> None: """Test preparing web search tool without user location.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Web search tool using static method - web_search_tool = OpenAIChatClient.get_web_search_tool() + web_search_tool = OpenAIChatCompletionClient.get_web_search_tool() result = client._prepare_tools_for_openai([web_search_tool]) @@ -1113,7 +1113,7 @@ def test_prepare_options_with_instructions( openai_unit_test_env: dict[str, str], ) -> None: """Test that instructions are prepended as system message.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() messages = [Message(role="user", text="Hello")] options = {"instructions": "You are a helpful assistant."} @@ -1129,7 +1129,7 @@ def test_prepare_options_with_instructions( def test_prepare_message_with_author_name(openai_unit_test_env: dict[str, str]) -> None: """Test that author_name is included in prepared message.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() message = Message( role="user", @@ -1147,7 +1147,7 @@ def test_prepare_message_with_tool_result_author_name( openai_unit_test_env: dict[str, str], ) -> None: """Test that author_name is not included for TOOL role messages.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Tool messages should not have 'name' field (it's for function name instead) message = Message( @@ -1171,7 +1171,7 @@ def test_prepare_system_message_content_is_string( Some OpenAI-compatible endpoints (e.g. NVIDIA NIM) reject system messages with list content. See https://github.com/microsoft/agent-framework/issues/1407 """ - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() message = Message(role="system", contents=[Content.from_text(text="You are a helpful assistant.")]) @@ -1187,7 +1187,7 @@ def test_prepare_developer_message_content_is_string( openai_unit_test_env: dict[str, str], ) -> None: """Test that developer message content is a plain string, not a list.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() message = Message(role="developer", contents=[Content.from_text(text="Follow these rules.")]) @@ -1203,7 +1203,7 @@ def test_prepare_system_message_multiple_text_contents_joined( openai_unit_test_env: dict[str, str], ) -> None: """Test that system messages with multiple text contents are joined into a single string.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() message = Message( role="system", @@ -1229,7 +1229,7 @@ def test_prepare_user_message_text_content_is_string( Some OpenAI-compatible endpoints (e.g. Foundry Local) cannot deserialize the list format. See https://github.com/microsoft/agent-framework/issues/4084 """ - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() message = Message(role="user", contents=[Content.from_text(text="Hello")]) @@ -1245,7 +1245,7 @@ def test_prepare_user_message_multimodal_content_remains_list( openai_unit_test_env: dict[str, str], ) -> None: """Test that multimodal user message content remains a list.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() message = Message( role="user", @@ -1266,7 +1266,7 @@ def test_prepare_assistant_message_text_content_is_string( openai_unit_test_env: dict[str, str], ) -> None: """Test that text-only assistant message content is flattened to a plain string.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() message = Message(role="assistant", contents=[Content.from_text(text="Sure, I can help.")]) @@ -1282,7 +1282,7 @@ def test_tool_choice_required_with_function_name( openai_unit_test_env: dict[str, str], ) -> None: """Test that tool_choice with required mode and function name is correctly prepared.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() messages = [Message(role="user", text="test")] options = { @@ -1300,7 +1300,7 @@ def test_tool_choice_required_with_function_name( def test_response_format_dict_passthrough(openai_unit_test_env: dict[str, str]) -> None: """Test that response_format as dict is passed through directly.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() messages = [Message(role="user", text="test")] custom_format = { @@ -1319,7 +1319,7 @@ def test_multiple_function_calls_in_single_message( openai_unit_test_env: dict[str, str], ) -> None: """Test that multiple function calls in a message are correctly prepared.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Create message with multiple function calls message = Message( @@ -1344,7 +1344,7 @@ def test_prepare_options_removes_parallel_tool_calls_when_no_tools( openai_unit_test_env: dict[str, str], ) -> None: """Test that parallel_tool_calls is removed when no tools are present.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() messages = [Message(role="user", text="test")] options = {"allow_multiple_tool_calls": True} @@ -1357,7 +1357,7 @@ def test_prepare_options_removes_parallel_tool_calls_when_no_tools( def test_prepare_options_excludes_conversation_id(openai_unit_test_env: dict[str, str]) -> None: """Test that conversation_id is excluded from prepared options for chat completions.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() messages = [Message(role="user", text="test")] options = {"conversation_id": "12345", "temperature": 0.7} @@ -1374,7 +1374,7 @@ async def test_streaming_exception_handling( openai_unit_test_env: dict[str, str], ) -> None: """Test that streaming errors are properly handled.""" - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() messages = [Message(role="user", text="test")] # Create a mock error during streaming @@ -1414,7 +1414,7 @@ class OutputStruct(BaseModel): param("presence_penalty", 0.3, False, id="presence_penalty"), param("stop", ["END"], False, id="stop"), param("allow_multiple_tool_calls", True, False, id="allow_multiple_tool_calls"), - # OpenAIChatOptions - just verify they don't fail + # OpenAIChatCompletionOptions - just verify they don't fail param("logit_bias", {"50256": -1}, False, id="logit_bias"), param( "prediction", @@ -1470,13 +1470,13 @@ async def test_integration_options( option_value: Any, needs_validation: bool, ) -> None: - """Parametrized test covering all ChatOptions and OpenAIChatOptions. + """Parametrized test covering all ChatOptions and OpenAIChatCompletionOptions. Tests both streaming and non-streaming modes for each option to ensure they don't cause failures. Options marked with needs_validation also check that the feature actually works correctly. """ - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() # Need at least 2 iterations for tool_choice tests: one to get function call, one to get final response client.function_invocation_configuration["max_iterations"] = 2 @@ -1551,11 +1551,11 @@ async def test_integration_options( @pytest.mark.integration @skip_if_openai_integration_tests_disabled async def test_integration_web_search() -> None: - client = OpenAIChatClient(model_id="gpt-4o-search-preview") + client = OpenAIChatCompletionClient(model="gpt-4o-search-preview") for streaming in [False, True]: # Use static method for web search tool - web_search_tool = OpenAIChatClient.get_web_search_tool() + web_search_tool = OpenAIChatCompletionClient.get_web_search_tool() content = { "messages": [ Message( @@ -1580,7 +1580,7 @@ async def test_integration_web_search() -> None: assert "Zoey" in response.text # Test that the client will use the web search tool with location - web_search_tool_with_location = OpenAIChatClient.get_web_search_tool( + web_search_tool_with_location = OpenAIChatCompletionClient.get_web_search_tool( web_search_options={ "user_location": { "type": "approximate", diff --git a/python/packages/core/tests/openai/test_openai_chat_client_base.py b/python/packages/openai/tests/openai/test_openai_chat_completion_client_base.py similarity index 92% rename from python/packages/core/tests/openai/test_openai_chat_client_base.py rename to python/packages/openai/tests/openai/test_openai_chat_completion_client_base.py index 82838120d7..3f5cbcddfb 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client_base.py +++ b/python/packages/openai/tests/openai/test_openai_chat_completion_client_base.py @@ -5,6 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from agent_framework import ChatResponseUpdate, Message +from agent_framework.exceptions import ChatClientException from openai import AsyncStream from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions from openai.types.chat import ChatCompletion, ChatCompletionChunk @@ -14,9 +16,7 @@ from openai.types.chat.chat_completion_message import ChatCompletionMessage from pydantic import BaseModel -from agent_framework import ChatResponseUpdate, Message -from agent_framework.exceptions import ChatClientException -from agent_framework.openai import OpenAIChatClient +from agent_framework_openai import OpenAIChatCompletionClient async def mock_async_process_chat_stream_response(_): @@ -69,10 +69,10 @@ async def test_cmc( mock_create.return_value = mock_chat_completion_response chat_history.append(Message(role="user", text="hello world")) - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() await openai_chat_completion.get_response(messages=chat_history) mock_create.assert_awaited_once_with( - model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + model=openai_unit_test_env["OPENAI_MODEL"], stream=False, messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore ) @@ -88,12 +88,12 @@ async def test_cmc_chat_options( mock_create.return_value = mock_chat_completion_response chat_history.append(Message(role="user", text="hello world")) - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() await openai_chat_completion.get_response( messages=chat_history, ) mock_create.assert_awaited_once_with( - model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + model=openai_unit_test_env["OPENAI_MODEL"], stream=False, messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore ) @@ -110,12 +110,12 @@ async def test_cmc_no_fcc_in_response( chat_history.append(Message(role="user", text="hello world")) orig_chat_history = deepcopy(chat_history) - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() await openai_chat_completion.get_response( messages=chat_history, ) mock_create.assert_awaited_once_with( - model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + model=openai_unit_test_env["OPENAI_MODEL"], stream=False, messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore ) @@ -135,7 +135,7 @@ async def test_cmc_structured_output_no_fcc( class Test(BaseModel): name: str - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() await openai_chat_completion.get_response( messages=chat_history, response_format=Test, @@ -153,7 +153,7 @@ async def test_scmc_chat_options( mock_create.return_value = mock_streaming_chat_completion_response chat_history.append(Message(role="user", text="hello world")) - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() async for msg in openai_chat_completion.get_response( stream=True, messages=chat_history, @@ -162,7 +162,7 @@ async def test_scmc_chat_options( assert msg.message_id is not None assert msg.response_id is not None mock_create.assert_awaited_once_with( - model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + model=openai_unit_test_env["OPENAI_MODEL"], stream=True, stream_options={"include_usage": True}, messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore @@ -179,7 +179,7 @@ async def test_cmc_general_exception( mock_create.return_value = mock_chat_completion_response chat_history.append(Message(role="user", text="hello world")) - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() with pytest.raises(ChatClientException): await openai_chat_completion.get_response( messages=chat_history, @@ -196,10 +196,10 @@ async def test_cmc_additional_properties( mock_create.return_value = mock_chat_completion_response chat_history.append(Message(role="user", text="hello world")) - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() await openai_chat_completion.get_response(messages=chat_history, options={"reasoning_effort": "low"}) mock_create.assert_awaited_once_with( - model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + model=openai_unit_test_env["OPENAI_MODEL"], stream=False, messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore reasoning_effort="low", @@ -235,14 +235,14 @@ async def test_get_streaming( chat_history.append(Message(role="user", text="hello world")) orig_chat_history = deepcopy(chat_history) - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() async for msg in openai_chat_completion.get_response( stream=True, messages=chat_history, ): assert isinstance(msg, ChatResponseUpdate) mock_create.assert_awaited_once_with( - model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + model=openai_unit_test_env["OPENAI_MODEL"], stream=True, stream_options={"include_usage": True}, messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore @@ -275,14 +275,14 @@ async def test_get_streaming_singular( chat_history.append(Message(role="user", text="hello world")) orig_chat_history = deepcopy(chat_history) - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() async for msg in openai_chat_completion.get_response( stream=True, messages=chat_history, ): assert isinstance(msg, ChatResponseUpdate) mock_create.assert_awaited_once_with( - model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + model=openai_unit_test_env["OPENAI_MODEL"], stream=True, stream_options={"include_usage": True}, messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore @@ -318,7 +318,7 @@ async def test_get_streaming_structured_output_no_fcc( class Test(BaseModel): name: str - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() async for msg in openai_chat_completion.get_response( stream=True, messages=chat_history, @@ -339,7 +339,7 @@ async def test_get_streaming_no_fcc_in_response( chat_history.append(Message(role="user", text="hello world")) orig_chat_history = deepcopy(chat_history) - openai_chat_completion = OpenAIChatClient() + openai_chat_completion = OpenAIChatCompletionClient() [ msg async for msg in openai_chat_completion.get_response( @@ -348,7 +348,7 @@ async def test_get_streaming_no_fcc_in_response( ) ] mock_create.assert_awaited_once_with( - model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + model=openai_unit_test_env["OPENAI_MODEL"], stream=True, stream_options={"include_usage": True}, messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore @@ -378,7 +378,7 @@ def test_chat_response_created_at_uses_utc(openai_unit_test_env: dict[str, str]) object="chat.completion", ) - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() response = client._parse_response_from_openai(mock_response, {}) # Verify that created_at is correctly formatted as UTC @@ -410,7 +410,7 @@ def test_chat_response_update_created_at_uses_utc(openai_unit_test_env: dict[str object="chat.completion.chunk", ) - client = OpenAIChatClient() + client = OpenAIChatCompletionClient() response_update = client._parse_response_update_from_openai(mock_chunk) # Verify that created_at is correctly formatted as UTC diff --git a/python/packages/core/tests/openai/test_openai_embedding_client.py b/python/packages/openai/tests/openai/test_openai_embedding_client.py similarity index 89% rename from python/packages/core/tests/openai/test_openai_embedding_client.py rename to python/packages/openai/tests/openai/test_openai_embedding_client.py index 72c7e4121d..7117040ffc 100644 --- a/python/packages/core/tests/openai/test_openai_embedding_client.py +++ b/python/packages/openai/tests/openai/test_openai_embedding_client.py @@ -10,7 +10,7 @@ from openai.types import Embedding as OpenAIEmbedding from openai.types.create_embedding_response import Usage -from agent_framework.openai import ( +from agent_framework_openai import ( OpenAIEmbeddingClient, OpenAIEmbeddingOptions, ) @@ -36,7 +36,7 @@ def _make_openai_response( def openai_unit_test_env(monkeypatch: pytest.MonkeyPatch) -> None: """Set up environment variables for OpenAI embedding client.""" monkeypatch.setenv("OPENAI_API_KEY", "test-api-key") - monkeypatch.setenv("OPENAI_EMBEDDING_MODEL_ID", "text-embedding-3-small") + monkeypatch.setenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small") # --- OpenAI unit tests --- @@ -44,26 +44,26 @@ def openai_unit_test_env(monkeypatch: pytest.MonkeyPatch) -> None: def test_openai_construction_with_explicit_params() -> None: client = OpenAIEmbeddingClient( - model_id="text-embedding-3-small", + model="text-embedding-3-small", api_key="test-key", ) - assert client.model_id == "text-embedding-3-small" + assert client.model == "text-embedding-3-small" def test_openai_construction_from_env(openai_unit_test_env: None) -> None: client = OpenAIEmbeddingClient() - assert client.model_id == "text-embedding-3-small" + assert client.model == "text-embedding-3-small" def test_openai_construction_missing_api_key_raises(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("OPENAI_API_KEY", raising=False) with pytest.raises(ValueError, match="API key is required"): - OpenAIEmbeddingClient(model_id="text-embedding-3-small") + OpenAIEmbeddingClient(model="text-embedding-3-small") def test_openai_construction_missing_model_raises(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.delenv("OPENAI_EMBEDDING_MODEL_ID", raising=False) - with pytest.raises(ValueError, match="model ID is required"): + monkeypatch.delenv("OPENAI_EMBEDDING_MODEL", raising=False) + with pytest.raises(ValueError, match="embedding model is required"): OpenAIEmbeddingClient(api_key="test-key") @@ -81,7 +81,7 @@ async def test_openai_get_embeddings(openai_unit_test_env: None) -> None: assert len(result) == 2 assert result[0].vector == [0.1, 0.2, 0.3] assert result[1].vector == [0.4, 0.5, 0.6] - assert result[0].model_id == "text-embedding-3-small" + assert result[0].model == "text-embedding-3-small" assert result[0].dimensions == 3 @@ -167,12 +167,12 @@ async def test_openai_base64_decoding(openai_unit_test_env: None) -> None: async def test_openai_error_when_no_model_id() -> None: client = OpenAIEmbeddingClient.__new__(OpenAIEmbeddingClient) - client.model_id = None + client.model = None client.client = MagicMock() client.additional_properties = {} client.otel_provider_name = "openai" - with pytest.raises(ValueError, match="model_id is required"): + with pytest.raises(ValueError, match="model is required"): await client.get_embeddings(["test"]) @@ -202,7 +202,7 @@ async def test_openai_empty_values_returns_empty(openai_unit_test_env: None) -> @pytest.mark.integration async def test_integration_openai_get_embeddings() -> None: """End-to-end test of OpenAI embedding generation.""" - client = OpenAIEmbeddingClient(model_id="text-embedding-3-small") + client = OpenAIEmbeddingClient(model="text-embedding-3-small") result = await client.get_embeddings(["hello world"]) @@ -210,7 +210,7 @@ async def test_integration_openai_get_embeddings() -> None: assert isinstance(result[0].vector, list) assert len(result[0].vector) > 0 assert all(isinstance(v, float) for v in result[0].vector) - assert result[0].model_id is not None + assert result[0].model is not None assert result.usage is not None assert result.usage["input_token_count"] > 0 @@ -220,7 +220,7 @@ async def test_integration_openai_get_embeddings() -> None: @pytest.mark.integration async def test_integration_openai_get_embeddings_multiple() -> None: """Test embedding generation for multiple inputs.""" - client = OpenAIEmbeddingClient(model_id="text-embedding-3-small") + client = OpenAIEmbeddingClient(model="text-embedding-3-small") result = await client.get_embeddings(["hello", "world", "test"]) @@ -234,7 +234,7 @@ async def test_integration_openai_get_embeddings_multiple() -> None: @pytest.mark.integration async def test_integration_openai_get_embeddings_with_dimensions() -> None: """Test embedding generation with custom dimensions.""" - client = OpenAIEmbeddingClient(model_id="text-embedding-3-small") + client = OpenAIEmbeddingClient(model="text-embedding-3-small") options: OpenAIEmbeddingOptions = {"dimensions": 256} result = await client.get_embeddings(["hello world"], options=options) diff --git a/python/packages/purview/agent_framework_purview/_client.py b/python/packages/purview/agent_framework_purview/_client.py index e592f34da5..af5c3f8224 100644 --- a/python/packages/purview/agent_framework_purview/_client.py +++ b/python/packages/purview/agent_framework_purview/_client.py @@ -6,12 +6,12 @@ import inspect import json import logging -from typing import Any, Literal, TypeVar, overload +from collections.abc import Awaitable, Callable +from typing import Any, Literal, TypeVar, Union, overload from uuid import uuid4 import httpx from agent_framework import AGENT_FRAMEWORK_USER_AGENT -from agent_framework.azure._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider from agent_framework.observability import get_tracer from azure.core.credentials import TokenCredential from azure.core.credentials_async import AsyncTokenCredential @@ -34,6 +34,9 @@ ) from ._settings import PurviewSettings, get_purview_scopes +AzureCredentialTypes = Union[TokenCredential, AsyncTokenCredential] +AzureTokenProvider = Callable[[], Union[str, Awaitable[str]]] + logger = logging.getLogger("agent_framework.purview") ResponseT = TypeVar("ResponseT") diff --git a/python/packages/purview/agent_framework_purview/_middleware.py b/python/packages/purview/agent_framework_purview/_middleware.py index c0e89a04a5..0b7442b47d 100644 --- a/python/packages/purview/agent_framework_purview/_middleware.py +++ b/python/packages/purview/agent_framework_purview/_middleware.py @@ -2,9 +2,11 @@ import logging from collections.abc import Awaitable, Callable +from typing import Union from agent_framework import AgentContext, AgentMiddleware, ChatContext, ChatMiddleware, MiddlewareTermination -from agent_framework.azure._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider +from azure.core.credentials import TokenCredential +from azure.core.credentials_async import AsyncTokenCredential from ._cache import CacheProvider from ._client import PurviewClient @@ -13,6 +15,9 @@ from ._processor import ScopedContentProcessor from ._settings import PurviewSettings +AzureCredentialTypes = Union[TokenCredential, AsyncTokenCredential] +AzureTokenProvider = Callable[[], Union[str, Awaitable[str]]] + logger = logging.getLogger("agent_framework.purview") diff --git a/python/samples/01-get-started/01_hello_agent.py b/python/samples/01-get-started/01_hello_agent.py index 167aa8065c..2127893221 100644 --- a/python/samples/01-get-started/01_hello_agent.py +++ b/python/samples/01-get-started/01_hello_agent.py @@ -1,39 +1,31 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -import os -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() """ Hello Agent — Simplest possible agent -This sample creates a minimal agent using AzureOpenAIResponsesClient via an +This sample creates a minimal agent using FoundryChatClient via an Azure AI Foundry project endpoint, and runs it in both non-streaming and streaming modes. There are XML tags in all of the get started samples, those are used to display the same code in the docs repo. - -Environment variables: - AZURE_AI_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint - AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME — Model deployment name (e.g. gpt-4o) """ async def main() -> None: # - credential = AzureCliCredential() - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], - credential=credential, + client = FoundryChatClient( + project_endpoint="https://your-project.services.ai.azure.com", + model="gpt-4o", + credential=AzureCliCredential(), ) - agent = client.as_agent( + agent = Agent( + client=client, name="HelloAgent", instructions="You are a friendly assistant. Keep your answers brief.", ) diff --git a/python/samples/01-get-started/02_add_tools.py b/python/samples/01-get-started/02_add_tools.py index 06108bb388..8d576bd32b 100644 --- a/python/samples/01-get-started/02_add_tools.py +++ b/python/samples/01-get-started/02_add_tools.py @@ -1,28 +1,19 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -import os from random import randint from typing import Annotated -from agent_framework import tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential -from dotenv import load_dotenv from pydantic import Field -# Load environment variables from .env file -load_dotenv() - """ Add Tools — Give your agent a function tool This sample shows how to define a function tool with the @tool decorator and wire it into an agent so the model can call it. - -Environment variables: - AZURE_AI_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint - AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME — Model deployment name (e.g. gpt-4o) """ @@ -40,18 +31,18 @@ def get_weather( async def main() -> None: - credential = AzureCliCredential() - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], - credential=credential, + client = FoundryChatClient( + project_endpoint="https://your-project.services.ai.azure.com", + model="gpt-4o", + credential=AzureCliCredential(), ) # - agent = client.as_agent( + agent = Agent( + client=client, name="WeatherAgent", instructions="You are a helpful weather agent. Use the get_weather tool to answer questions.", - tools=get_weather, + tools=[get_weather], ) # diff --git a/python/samples/01-get-started/03_multi_turn.py b/python/samples/01-get-started/03_multi_turn.py index 16a0f0060a..9725bd8bbf 100644 --- a/python/samples/01-get-started/03_multi_turn.py +++ b/python/samples/01-get-started/03_multi_turn.py @@ -1,37 +1,29 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -import os -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() """ Multi-Turn Conversations — Use AgentSession to maintain context This sample shows how to keep conversation history across multiple calls by reusing the same session object. - -Environment variables: - AZURE_AI_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint - AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME — Model deployment name (e.g. gpt-4o) """ async def main() -> None: # - credential = AzureCliCredential() - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], - credential=credential, + client = FoundryChatClient( + project_endpoint="https://your-project.services.ai.azure.com", + model="gpt-4o", + credential=AzureCliCredential(), ) - agent = client.as_agent( + agent = Agent( + client=client, name="ConversationAgent", instructions="You are a friendly assistant. Keep your answers brief.", ) diff --git a/python/samples/01-get-started/04_memory.py b/python/samples/01-get-started/04_memory.py index c554be7337..82bf484a45 100644 --- a/python/samples/01-get-started/04_memory.py +++ b/python/samples/01-get-started/04_memory.py @@ -1,16 +1,11 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -import os from typing import Any -from agent_framework import AgentSession, BaseContextProvider, SessionContext -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, AgentSession, BaseContextProvider, SessionContext +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() """ Agent Memory with Context Providers and Session State @@ -18,10 +13,6 @@ Context providers inject dynamic context into each agent call. This sample shows a provider that stores the user's name in session state and personalizes responses — the name persists across turns via the session. - -Environment variables: - AZURE_AI_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint - AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME — Model deployment name (e.g. gpt-4o) """ @@ -73,14 +64,14 @@ async def after_run( async def main() -> None: # - credential = AzureCliCredential() - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], - credential=credential, + client = FoundryChatClient( + project_endpoint="https://your-project.services.ai.azure.com", + model="gpt-4o", + credential=AzureCliCredential(), ) - agent = client.as_agent( + agent = Agent( + client=client, name="MemoryAgent", instructions="You are a friendly assistant.", context_providers=[UserMemoryProvider()], diff --git a/python/samples/01-get-started/06_host_your_agent.py b/python/samples/01-get-started/06_host_your_agent.py index 6bc87b48b4..f967d03f8e 100644 --- a/python/samples/01-get-started/06_host_your_agent.py +++ b/python/samples/01-get-started/06_host_your_agent.py @@ -4,45 +4,36 @@ # fmt: off from typing import Any -from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient +from agent_framework import Agent +from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.identity import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() """Host your agent with Azure Functions. - This sample shows the Python hosting pattern used in docs: -- Create an agent with `AzureOpenAIChatClient` +- Create an agent with `FoundryChatClient` - Register it with `AgentFunctionApp` - Run with Azure Functions Core Tools (`func start`) - Prerequisites: pip install agent-framework-azurefunctions --pre - -Environment variables: - AZURE_OPENAI_ENDPOINT - AZURE_OPENAI_CHAT_DEPLOYMENT_NAME """ # def _create_agent() -> Any: """Create a hosted agent backed by Azure OpenAI.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + return Agent( + client=FoundryChatClient( + project_endpoint="https://your-project.services.ai.azure.com", + model="gpt-4o", + credential=AzureCliCredential(), + ), name="HostedAgent", instructions="You are a helpful assistant hosted in Azure Functions.", ) - - # - # app = AgentFunctionApp(agents=[_create_agent()], enable_health_check=True, max_poll_retries=50) # - - if __name__ == "__main__": print("Start the Functions host with: func start") print("Then call: POST /api/agents/HostedAgent/run") diff --git a/python/samples/02-agents/auto_retry.py b/python/samples/02-agents/auto_retry.py index 0a5169ad3d..a16f1092da 100644 --- a/python/samples/02-agents/auto_retry.py +++ b/python/samples/02-agents/auto_retry.py @@ -15,8 +15,8 @@ from collections.abc import Awaitable, Callable from typing import Any, TypeVar, cast -from agent_framework import ChatContext, ChatMiddleware, SupportsChatGetResponse, chat_middleware -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework import Agent, ChatContext, ChatMiddleware, SupportsChatGetResponse, chat_middleware +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from openai import RateLimitError @@ -104,7 +104,7 @@ async def _with_retry(): @with_rate_limit_retry() -class RetryingAzureOpenAIChatClient(AzureOpenAIChatClient): +class RetryingFoundryChatClient(FoundryChatClient): """Azure OpenAI Chat client with class-decorator-based retry behavior.""" @@ -186,7 +186,8 @@ async def class_decorator_example() -> None: # For authentication, run `az login` command in terminal or replace # AzureCliCredential with your preferred authentication option. - agent = RetryingAzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + agent = Agent( + client=RetryingFoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", ) @@ -204,7 +205,8 @@ async def class_based_middleware_example() -> None: # For authentication, run `az login` command in terminal or replace # AzureCliCredential with your preferred authentication option. - agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + agent = Agent( + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", middleware=[RateLimitRetryMiddleware(max_attempts=3)], ) @@ -223,7 +225,8 @@ async def function_based_middleware_example() -> None: # For authentication, run `az login` command in terminal or replace # AzureCliCredential with your preferred authentication option. - agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + agent = Agent( + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", middleware=[rate_limit_retry_middleware], ) @@ -239,7 +242,7 @@ async def main() -> None: print("=== Auto-Retry Rate Limiting Sample ===") print( "Demonstrates two approaches for automatic retry on rate limit (429) errors.\n" - "Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME (and optionally\n" + "Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (and optionally\n" "AZURE_OPENAI_API_KEY) before running, or populate a .env file." ) diff --git a/python/samples/02-agents/background_responses.py b/python/samples/02-agents/background_responses.py index 777c348b0a..002dc17a34 100644 --- a/python/samples/02-agents/background_responses.py +++ b/python/samples/02-agents/background_responses.py @@ -29,7 +29,7 @@ agent = Agent( name="researcher", instructions="You are a helpful research assistant. Be concise.", - client=OpenAIResponsesClient(model_id="o3"), + client=OpenAIResponsesClient(model="o3"), ) diff --git a/python/samples/02-agents/chat_client/built_in_chat_clients.py b/python/samples/02-agents/chat_client/built_in_chat_clients.py index 8560afcf4f..f047935bf2 100644 --- a/python/samples/02-agents/chat_client/built_in_chat_clients.py +++ b/python/samples/02-agents/chat_client/built_in_chat_clients.py @@ -7,8 +7,8 @@ from agent_framework import SupportsChatGetResponse, tool from agent_framework.azure import ( - AzureAIAgentClient, AzureOpenAIAssistantsClient, + FoundryChatClient, ) from agent_framework.openai import OpenAIAssistantsClient from azure.identity import AzureCliCredential @@ -71,15 +71,14 @@ def get_client(client_name: ClientName) -> SupportsChatGetResponse[Any]: from agent_framework.amazon import BedrockChatClient from agent_framework.anthropic import AnthropicClient from agent_framework.azure import ( - AzureOpenAIChatClient, - AzureOpenAIResponsesClient, + FoundryChatClient, ) from agent_framework.ollama import OllamaChatClient - from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient + from agent_framework.openai import OpenAIResponsesClient # 1. Create OpenAI clients. if client_name == "openai_chat": - return OpenAIChatClient() + return FoundryChatClient() if client_name == "openai_responses": return OpenAIResponsesClient() if client_name == "openai_assistants": @@ -93,13 +92,13 @@ def get_client(client_name: ClientName) -> SupportsChatGetResponse[Any]: # 2. Create Azure OpenAI clients. if client_name == "azure_openai_chat": - return AzureOpenAIChatClient(credential=AzureCliCredential()) + return FoundryChatClient(credential=AzureCliCredential()) if client_name == "azure_openai_responses": - return AzureOpenAIResponsesClient(credential=AzureCliCredential(), api_version="preview") + return FoundryChatClient(credential=AzureCliCredential(), api_version="preview") if client_name == "azure_openai_responses_foundry": - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + return FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], credential=AzureCliCredential(), ) if client_name == "azure_openai_assistants": @@ -107,7 +106,7 @@ def get_client(client_name: ClientName) -> SupportsChatGetResponse[Any]: # 3. Create Azure AI client. if client_name == "azure_ai_agent": - return AzureAIAgentClient(credential=AsyncAzureCliCredential()) + return FoundryChatClient(credential=AsyncAzureCliCredential()) raise ValueError(f"Unsupported client name: {client_name}") @@ -123,7 +122,7 @@ async def main(client_name: ClientName = "openai_chat") -> None: print(f"User: {message}") # 2. Run with context-managed clients. - if isinstance(client, OpenAIAssistantsClient | AzureOpenAIAssistantsClient | AzureAIAgentClient): + if isinstance(client, OpenAIAssistantsClient | AzureOpenAIAssistantsClient | FoundryChatClient): async with client: if stream: response_stream = client.get_response(message, stream=True, options={"tools": get_weather}) diff --git a/python/samples/02-agents/chat_client/chat_response_cancellation.py b/python/samples/02-agents/chat_client/chat_response_cancellation.py index aff3536602..a5fde65b27 100644 --- a/python/samples/02-agents/chat_client/chat_response_cancellation.py +++ b/python/samples/02-agents/chat_client/chat_response_cancellation.py @@ -3,7 +3,7 @@ import asyncio from agent_framework import Message -from agent_framework.openai import OpenAIChatClient +from agent_framework.azure import FoundryChatClient from dotenv import load_dotenv # Load environment variables from .env file @@ -23,10 +23,10 @@ async def main() -> None: Creates a task for the chat request, waits briefly, then cancels it to show proper cleanup. Configuration: - - OpenAI model ID: Use "model_id" parameter or "OPENAI_CHAT_MODEL_ID" environment variable + - OpenAI model ID: Use "model_id" parameter or "OPENAI_MODEL" environment variable - OpenAI API key: Use "api_key" parameter or "OPENAI_API_KEY" environment variable """ - client = OpenAIChatClient() + client = FoundryChatClient() try: task = asyncio.create_task( diff --git a/python/samples/02-agents/chat_client/custom_chat_client.py b/python/samples/02-agents/chat_client/custom_chat_client.py index 7a9aaa95f6..c677c09d8b 100644 --- a/python/samples/02-agents/chat_client/custom_chat_client.py +++ b/python/samples/02-agents/chat_client/custom_chat_client.py @@ -7,6 +7,7 @@ from typing import Any, ClassVar, TypeAlias, TypedDict from agent_framework import ( + Agent, BaseChatClient, ChatMiddlewareLayer, ChatResponse, @@ -159,7 +160,8 @@ async def main() -> None: print(f"Direct response: {direct_response.messages[0].text}") # Create an agent using the custom chat client - echo_agent = echo_client.as_agent( + echo_agent = Agent( + client=echo_client, name="EchoAgent", instructions="You are a helpful assistant that echoes back what users say.", ) diff --git a/python/samples/02-agents/context_providers/azure_ai_foundry_memory.py b/python/samples/02-agents/context_providers/azure_ai_foundry_memory.py index f7662d1e2f..4af973d592 100644 --- a/python/samples/02-agents/context_providers/azure_ai_foundry_memory.py +++ b/python/samples/02-agents/context_providers/azure_ai_foundry_memory.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from agent_framework import Agent, InMemoryHistoryProvider -from agent_framework.azure import AzureOpenAIResponsesClient, FoundryMemoryProvider +from agent_framework.azure import FoundryChatClient, FoundryMemoryProvider from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( MemoryStoreDefaultDefinition, @@ -31,8 +31,8 @@ rather than chat history. The memory store is deleted at the end of the run. Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT environment variable -2. Set AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME for the chat/responses model +1. Set FOUNDRY_PROJECT_ENDPOINT environment variable +2. Set FOUNDRY_MODEL for the chat/responses model 3. Set AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME for the embedding model 4. Deploy both a chat model (e.g. gpt-4) and an embedding model (e.g. text-embedding-3-small) """ @@ -40,7 +40,7 @@ async def main() -> None: - endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] async with ( AzureCliCredential() as credential, AIProjectClient(endpoint=endpoint, credential=credential) as project_client, @@ -54,7 +54,7 @@ async def main() -> None: user_profile_details="Avoid irrelevant or sensitive data, such as age, financials, precise location, and credentials", ) memory_store_definition = MemoryStoreDefaultDefinition( - chat_model=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + chat_model=os.environ["FOUNDRY_MODEL"], embedding_model=os.environ["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"], options=options, ) @@ -75,7 +75,7 @@ async def main() -> None: print("==========================================") # Create the chat client - client = AzureOpenAIResponsesClient(project_client=project_client) + client = FoundryChatClient(project_client=project_client) # Create the Foundry Memory context provider memory_provider = FoundryMemoryProvider( project_client=project_client, diff --git a/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py b/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py index d3c618d8a8..07dad2fdd9 100644 --- a/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py +++ b/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py @@ -4,7 +4,7 @@ import os from agent_framework import Agent -from agent_framework.azure import AzureAIAgentClient, AzureAISearchContextProvider +from agent_framework.azure import AzureAISearchContextProvider, FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv @@ -31,8 +31,8 @@ Environment variables: - AZURE_SEARCH_ENDPOINT: Your Azure AI Search endpoint - - AZURE_SEARCH_API_KEY: (Optional) API key - if not provided, uses DefaultAzureCredential - - AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint + - AZURE_SEARCH_API_KEY: (Optional) API key - if not provided, uses AzureCliCredential + - FOUNDRY_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint - AZURE_AI_MODEL_DEPLOYMENT_NAME: Your model deployment name (e.g., "gpt-4o") For using an existing Knowledge Base (recommended): @@ -57,7 +57,7 @@ async def main() -> None: # Get configuration from environment search_endpoint = os.environ["AZURE_SEARCH_ENDPOINT"] search_key = os.environ.get("AZURE_SEARCH_API_KEY") - project_endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] + project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] model_deployment = os.environ.get("AZURE_AI_MODEL_DEPLOYMENT_NAME", "gpt-4o") # Agentic mode requires exactly ONE of: knowledge_base_name OR index_name @@ -99,7 +99,7 @@ async def main() -> None: credential=AzureCliCredential() if not search_key else None, mode="agentic", azure_openai_resource_url=azure_openai_resource_url, - model_deployment_name=model_deployment, + model_model=model_deployment, # Optional: Configure retrieval behavior knowledge_base_output_mode="extractive_data", # or "answer_synthesis" retrieval_reasoning_effort="minimal", # or "medium", "low" @@ -109,9 +109,9 @@ async def main() -> None: # Create agent with search context provider async with ( search_provider, - AzureAIAgentClient( + FoundryChatClient( project_endpoint=project_endpoint, - model_deployment_name=model_deployment, + model_model=model_deployment, credential=AzureCliCredential(), ) as client, Agent( diff --git a/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py b/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py index 2217cefd23..42578cd914 100644 --- a/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py +++ b/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py @@ -4,7 +4,7 @@ import os from agent_framework import Agent -from agent_framework.azure import AzureAIAgentClient, AzureAISearchContextProvider, AzureOpenAIEmbeddingClient +from agent_framework.azure import AzureAISearchContextProvider, AzureOpenAIEmbeddingClient, FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv @@ -26,9 +26,9 @@ 2. An Azure AI Foundry project with a model deployment 3. Set the following environment variables: - AZURE_SEARCH_ENDPOINT: Your Azure AI Search endpoint - - AZURE_SEARCH_API_KEY: (Optional) Your search API key - if not provided, uses DefaultAzureCredential for Entra ID + - AZURE_SEARCH_API_KEY: (Optional) Your search API key - if not provided, uses AzureCliCredential for Entra ID - AZURE_SEARCH_INDEX_NAME: Your search index name - - AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint + - FOUNDRY_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint - AZURE_AI_MODEL_DEPLOYMENT_NAME: Your model deployment name (e.g., "gpt-4o") - AZURE_OPENAI_EMBEDDING_MODEL_ID: (Optional) Your embedding model for hybrid search (e.g., "text-embedding-3-small") - AZURE_OPENAI_ENDPOINT: (Optional) Your Azure OpenAI resource URL, required if using an OpenAI embedding model for hybrid search @@ -51,7 +51,7 @@ async def main() -> None: search_endpoint = os.environ["AZURE_SEARCH_ENDPOINT"] search_key = os.environ.get("AZURE_SEARCH_API_KEY") index_name = os.environ["AZURE_SEARCH_INDEX_NAME"] - project_endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] + project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] model_deployment = os.environ.get("AZURE_AI_MODEL_DEPLOYMENT_NAME", "gpt-4o") openai_endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT") embedding_model = os.environ.get("AZURE_OPENAI_EMBEDDING_MODEL_ID", "text-embedding-3-small") @@ -60,7 +60,7 @@ async def main() -> None: if openai_endpoint and embedding_model: embedding_client = AzureOpenAIEmbeddingClient( endpoint=openai_endpoint, - deployment_name=embedding_model, + model=embedding_model, credential=credential, ) @@ -83,9 +83,9 @@ async def main() -> None: # Create agent with search context provider async with ( search_provider, - AzureAIAgentClient( + FoundryChatClient( project_endpoint=project_endpoint, - model_deployment_name=model_deployment, + model_model=model_deployment, credential=credential, ) as client, Agent( diff --git a/python/samples/02-agents/context_providers/mem0/mem0_basic.py b/python/samples/02-agents/context_providers/mem0/mem0_basic.py index e5bbf478cf..2e1862172f 100644 --- a/python/samples/02-agents/context_providers/mem0/mem0_basic.py +++ b/python/samples/02-agents/context_providers/mem0/mem0_basic.py @@ -3,8 +3,8 @@ import asyncio import uuid -from agent_framework import tool -from agent_framework.azure import AzureAIAgentClient +from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient from agent_framework.mem0 import Mem0ContextProvider from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv @@ -30,19 +30,17 @@ def retrieve_company_report(company_code: str, detailed: bool) -> str: async def main() -> None: """Example of memory usage with Mem0 context provider.""" - print("=== Mem0 Context Provider Example ===") - # Each record in Mem0 should be associated with agent_id or user_id or application_id or thread_id. # In this example, we associate Mem0 records with user_id. user_id = str(uuid.uuid4()) - # For Azure authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. # For Mem0 authentication, set Mem0 API key via "api_key" parameter or MEM0_API_KEY environment variable. async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="FriendlyAssistant", instructions="You are a friendly assistant.", tools=retrieve_company_report, @@ -56,33 +54,23 @@ async def main() -> None: print(f"User: {query}") result = await agent.run(query) print(f"Agent: {result}\n") - # Now tell the agent the company code and the report format that you want to use # and it should be able to invoke the tool and return the report. query = "I always work with CNTS and I always want a detailed report format. Please remember and retrieve it." - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - # Mem0 processes and indexes memories asynchronously. # Wait for memories to be indexed before querying in a new thread. # In production, consider implementing retry logic or using Mem0's # eventual consistency handling instead of a fixed delay. print("Waiting for memories to be processed...") await asyncio.sleep(12) # Empirically determined delay for Mem0 indexing - print("\nRequest within a new session:") # Create a new session for the agent. # The new session has no context of the previous conversation. session = agent.create_session() - # Since we have the mem0 component in the session, the agent should be able to # retrieve the company report without asking for clarification, as it will # be able to remember the user preferences from Mem0 component. - query = "Please retrieve my company report" - print(f"User: {query}") result = await agent.run(query, session=session) - print(f"Agent: {result}\n") if __name__ == "__main__": diff --git a/python/samples/02-agents/context_providers/mem0/mem0_oss.py b/python/samples/02-agents/context_providers/mem0/mem0_oss.py index ca8e907da9..5463ff47c8 100644 --- a/python/samples/02-agents/context_providers/mem0/mem0_oss.py +++ b/python/samples/02-agents/context_providers/mem0/mem0_oss.py @@ -3,8 +3,8 @@ import asyncio import uuid -from agent_framework import tool -from agent_framework.azure import AzureAIAgentClient +from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient from agent_framework.mem0 import Mem0ContextProvider from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv @@ -31,13 +31,10 @@ def retrieve_company_report(company_code: str, detailed: bool) -> str: async def main() -> None: """Example of memory usage with local Mem0 OSS context provider.""" - print("=== Mem0 Context Provider Example ===") - # Each record in Mem0 should be associated with agent_id or user_id or application_id or thread_id. # In this example, we associate Mem0 records with user_id. user_id = str(uuid.uuid4()) - # For Azure authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. # By default, local Mem0 authenticates to your OpenAI using the OPENAI_API_KEY environment variable. @@ -45,7 +42,8 @@ async def main() -> None: local_mem0_client = AsyncMemory() async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="FriendlyAssistant", instructions="You are a friendly assistant.", tools=retrieve_company_report, @@ -59,27 +57,17 @@ async def main() -> None: print(f"User: {query}") result = await agent.run(query) print(f"Agent: {result}\n") - # Now tell the agent the company code and the report format that you want to use # and it should be able to invoke the tool and return the report. query = "I always work with CNTS and I always want a detailed report format. Please remember and retrieve it." - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - print("\nRequest within a new session:") - # Create a new session for the agent. # The new session has no context of the previous conversation. session = agent.create_session() - # Since we have the mem0 component in the session, the agent should be able to # retrieve the company report without asking for clarification, as it will # be able to remember the user preferences from Mem0 component. - query = "Please retrieve my company report" - print(f"User: {query}") result = await agent.run(query, session=session) - print(f"Agent: {result}\n") if __name__ == "__main__": diff --git a/python/samples/02-agents/context_providers/mem0/mem0_sessions.py b/python/samples/02-agents/context_providers/mem0/mem0_sessions.py index 8bda4a575e..c4cc41a957 100644 --- a/python/samples/02-agents/context_providers/mem0/mem0_sessions.py +++ b/python/samples/02-agents/context_providers/mem0/mem0_sessions.py @@ -3,8 +3,8 @@ import asyncio import uuid -from agent_framework import tool -from agent_framework.azure import AzureAIAgentClient +from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient from agent_framework.mem0 import Mem0ContextProvider from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv @@ -37,7 +37,8 @@ async def example_global_thread_scope() -> None: async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="GlobalMemoryAssistant", instructions="You are an assistant that remembers user preferences across conversations.", tools=get_user_preferences, @@ -78,7 +79,8 @@ async def example_per_operation_thread_scope() -> None: async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="ScopedMemoryAssistant", instructions="You are an assistant with thread-scoped memory.", tools=get_user_preferences, @@ -129,7 +131,8 @@ async def example_multiple_agents() -> None: async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="PersonalAssistant", instructions="You are a personal assistant that helps with personal tasks.", context_providers=[ @@ -139,7 +142,8 @@ async def example_multiple_agents() -> None: ) ], ) as personal_agent, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="WorkAssistant", instructions="You are a work assistant that helps with professional tasks.", context_providers=[ diff --git a/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py b/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py index 5adbb53ef3..dff63a2ac0 100644 --- a/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py +++ b/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """Azure Managed Redis History Provider with Azure AD Authentication @@ -17,15 +18,15 @@ Environment Variables: - AZURE_REDIS_HOST: Your Azure Managed Redis host (e.g., myredis.redis.cache.windows.net) - - AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint - - AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: Azure OpenAI Responses deployment name + - FOUNDRY_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint + - FOUNDRY_MODEL: Azure OpenAI Responses deployment name - AZURE_USER_OBJECT_ID: Your Azure AD User Object ID for authentication """ import asyncio import os -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.redis import RedisHistoryProvider from azure.identity import AzureCliCredential from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential @@ -81,14 +82,14 @@ async def main() -> None: ) # 3. Create chat client - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], credential=AzureCliCredential(), ) # 4. Create agent with Azure Redis history provider - agent = client.as_agent( + agent = Agent(client=client, name="AzureRedisAssistant", instructions="You are a helpful assistant.", context_providers=[history_provider], diff --git a/python/samples/02-agents/context_providers/redis/redis_basics.py b/python/samples/02-agents/context_providers/redis/redis_basics.py index 662ed5e971..85e4e5791b 100644 --- a/python/samples/02-agents/context_providers/redis/redis_basics.py +++ b/python/samples/02-agents/context_providers/redis/redis_basics.py @@ -30,8 +30,8 @@ import asyncio import os -from agent_framework import Message, tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, Message, tool +from agent_framework.azure import FoundryChatClient from agent_framework.redis import RedisContextProvider from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -99,11 +99,11 @@ def search_flights(origin_airport_code: str, destination_airport_code: str, deta ) -def create_chat_client() -> AzureOpenAIResponsesClient: +def create_chat_client() -> FoundryChatClient: """Create an Azure OpenAI Responses client using a Foundry project endpoint.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + return FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], credential=AzureCliCredential(), ) @@ -121,7 +121,7 @@ async def main() -> None: # Create a provider with partition scope and OpenAI embeddings # Please set OPENAI_API_KEY to use the OpenAI vectorizer. - # For chat responses, also set AZURE_AI_PROJECT_ENDPOINT and AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME. + # For chat responses, also set FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL. # We attach an embedding vectorizer so the provider can perform hybrid (text + vector) # retrieval. If you prefer text-only retrieval, instantiate RedisContextProvider without the @@ -206,7 +206,7 @@ async def main() -> None: client = create_chat_client() # Create agent wired to the Redis context provider. The provider automatically # persists conversational details and surfaces relevant context on each turn. - agent = client.as_agent( + agent = Agent(client=client, name="MemoryEnhancedAssistant", instructions=( "You are a helpful assistant. Personalize replies using provided context. " @@ -249,7 +249,7 @@ async def main() -> None: # Create agent exposing the flight search tool. Tool outputs are captured by the # provider and become retrievable context for later turns. client = create_chat_client() - agent = client.as_agent( + agent = Agent(client=client, name="MemoryEnhancedAssistant", instructions=( "You are a helpful assistant. Personalize replies using provided context. " diff --git a/python/samples/02-agents/context_providers/redis/redis_conversation.py b/python/samples/02-agents/context_providers/redis/redis_conversation.py index 8e8199fc78..2e8000cd95 100644 --- a/python/samples/02-agents/context_providers/redis/redis_conversation.py +++ b/python/samples/02-agents/context_providers/redis/redis_conversation.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """Redis Context Provider: Basic usage and agent integration @@ -21,7 +22,7 @@ import asyncio import os -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.redis import RedisContextProvider from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -64,14 +65,14 @@ async def main() -> None: ) # Create chat client for the agent - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], credential=AzureCliCredential(), ) # Create agent wired to the Redis context provider. The provider automatically # persists conversational details and surfaces relevant context on each turn. - agent = client.as_agent( + agent = Agent(client=client, name="MemoryEnhancedAssistant", instructions=( "You are a helpful assistant. Personalize replies using provided context. " diff --git a/python/samples/02-agents/context_providers/redis/redis_sessions.py b/python/samples/02-agents/context_providers/redis/redis_sessions.py index 48cf0e596b..1b881a1f24 100644 --- a/python/samples/02-agents/context_providers/redis/redis_sessions.py +++ b/python/samples/02-agents/context_providers/redis/redis_sessions.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """Redis Context Provider: Thread scoping examples @@ -29,7 +30,7 @@ import asyncio import os -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.redis import RedisContextProvider from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -45,12 +46,12 @@ # Please set OPENAI_API_KEY to use the OpenAI vectorizer. -# For chat responses, also set AZURE_AI_PROJECT_ENDPOINT and AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME. -def create_chat_client() -> AzureOpenAIResponsesClient: +# For chat responses, also set FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL. +def create_chat_client() -> FoundryChatClient: """Create an Azure OpenAI Responses client using a Foundry project endpoint.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + return FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], credential=AzureCliCredential(), ) @@ -71,7 +72,7 @@ async def example_global_thread_scope() -> None: user_id="threads_demo_user", ) - agent = client.as_agent( + agent = Agent(client=client, name="GlobalMemoryAssistant", instructions=( "You are a helpful assistant. Personalize replies using provided context. " @@ -128,7 +129,7 @@ async def example_per_operation_thread_scope() -> None: vector_distance_metric="cosine", ) - agent = client.as_agent( + agent = Agent(client=client, name="ScopedMemoryAssistant", instructions="You are an assistant with thread-scoped memory.", context_providers=[provider], @@ -191,7 +192,7 @@ async def example_multiple_agents() -> None: vector_distance_metric="cosine", ) - personal_agent = client.as_agent( + personal_agent = Agent(client=client, name="PersonalAssistant", instructions="You are a personal assistant that helps with personal tasks.", context_providers=[personal_provider], @@ -210,7 +211,7 @@ async def example_multiple_agents() -> None: vector_distance_metric="cosine", ) - work_agent = client.as_agent( + work_agent = Agent(client=client, name="WorkAssistant", instructions="You are a work assistant that helps with professional tasks.", context_providers=[work_provider], diff --git a/python/samples/02-agents/context_providers/simple_context_provider.py b/python/samples/02-agents/context_providers/simple_context_provider.py index 87bf1a5a16..baa59d7f6a 100644 --- a/python/samples/02-agents/context_providers/simple_context_provider.py +++ b/python/samples/02-agents/context_providers/simple_context_provider.py @@ -6,7 +6,7 @@ from typing import Any from agent_framework import Agent, AgentSession, BaseContextProvider, SessionContext, SupportsChatGetResponse -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import BaseModel @@ -89,9 +89,9 @@ async def before_run( async def main(): - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], credential=AzureCliCredential(), ) diff --git a/python/samples/02-agents/conversations/custom_history_provider.py b/python/samples/02-agents/conversations/custom_history_provider.py index a8fc3974a0..59d63b70c0 100644 --- a/python/samples/02-agents/conversations/custom_history_provider.py +++ b/python/samples/02-agents/conversations/custom_history_provider.py @@ -4,7 +4,7 @@ from collections.abc import Sequence from typing import Any -from agent_framework import AgentSession, BaseHistoryProvider, Message +from agent_framework import Agent, AgentSession, BaseHistoryProvider, Message from agent_framework.openai import OpenAIChatClient from dotenv import load_dotenv @@ -54,7 +54,8 @@ async def main() -> None: # OpenAI Chat Client is used as an example here, # other chat clients can be used as well. - agent = OpenAIChatClient().as_agent( + agent = Agent( + client=OpenAIChatClient(), name="CustomBot", instructions="You are a helpful assistant that remembers our conversation.", # Use custom history provider. diff --git a/python/samples/02-agents/conversations/redis_history_provider.py b/python/samples/02-agents/conversations/redis_history_provider.py index 17e1094775..6829382dc8 100644 --- a/python/samples/02-agents/conversations/redis_history_provider.py +++ b/python/samples/02-agents/conversations/redis_history_provider.py @@ -4,7 +4,7 @@ import os from uuid import uuid4 -from agent_framework import AgentSession +from agent_framework import Agent, AgentSession from agent_framework.openai import OpenAIChatClient from agent_framework.redis import RedisHistoryProvider from dotenv import load_dotenv @@ -36,7 +36,8 @@ async def example_manual_memory_store() -> None: ) # Create agent with Redis history provider - agent = OpenAIChatClient().as_agent( + agent = Agent( + client=OpenAIChatClient(), name="RedisBot", instructions="You are a helpful assistant that remembers our conversation using Redis.", context_providers=[redis_provider], @@ -75,7 +76,8 @@ async def example_user_session_management() -> None: ) # Create agent with history provider - agent = OpenAIChatClient().as_agent( + agent = Agent( + client=OpenAIChatClient(), name="SessionBot", instructions="You are a helpful assistant. Keep track of user preferences.", context_providers=[redis_provider], @@ -114,7 +116,8 @@ async def example_conversation_persistence() -> None: redis_url=REDIS_URL, ) - agent = OpenAIChatClient().as_agent( + agent = Agent( + client=OpenAIChatClient(), name="PersistentBot", instructions="You are a helpful assistant. Remember our conversation history.", context_providers=[redis_provider], @@ -163,7 +166,8 @@ async def example_session_serialization() -> None: redis_url=REDIS_URL, ) - agent = OpenAIChatClient().as_agent( + agent = Agent( + client=OpenAIChatClient(), name="SerializationBot", instructions="You are a helpful assistant.", context_providers=[redis_provider], @@ -206,7 +210,8 @@ async def example_message_limits() -> None: max_messages=3, # Keep only 3 most recent messages ) - agent = OpenAIChatClient().as_agent( + agent = Agent( + client=OpenAIChatClient(), name="LimitBot", instructions="You are a helpful assistant with limited memory.", context_providers=[redis_provider], diff --git a/python/samples/02-agents/conversations/suspend_resume_session.py b/python/samples/02-agents/conversations/suspend_resume_session.py index 24c641d06a..b391ca3671 100644 --- a/python/samples/02-agents/conversations/suspend_resume_session.py +++ b/python/samples/02-agents/conversations/suspend_resume_session.py @@ -2,9 +2,8 @@ import asyncio -from agent_framework import AgentSession -from agent_framework.azure import AzureAIAgentClient -from agent_framework.openai import OpenAIChatClient +from agent_framework import Agent, AgentSession +from agent_framework.azure import FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv @@ -24,10 +23,11 @@ async def suspend_resume_service_managed_session() -> None: """Demonstrates how to suspend and resume a service-managed session.""" print("=== Suspend-Resume Service-Managed Session ===") - # AzureAIAgentClient supports service-managed sessions. + # FoundryChatClient supports service-managed sessions. async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="MemoryBot", instructions="You are a helpful assistant that remembers our conversation." ) as agent, ): @@ -60,7 +60,8 @@ async def suspend_resume_in_memory_session() -> None: # OpenAI Chat Client is used as an example here, # other chat clients can be used as well. - agent = OpenAIChatClient().as_agent( + agent = Agent( + client=FoundryChatClient(), name="MemoryBot", instructions="You are a helpful assistant that remembers our conversation." ) diff --git a/python/samples/02-agents/declarative/azure_openai_responses_agent.py b/python/samples/02-agents/declarative/azure_openai_responses_agent.py index cda02a4e90..a8988e8311 100644 --- a/python/samples/02-agents/declarative/azure_openai_responses_agent.py +++ b/python/samples/02-agents/declarative/azure_openai_responses_agent.py @@ -15,11 +15,9 @@ async def main(): # get the path current_path = Path(__file__).parent yaml_path = current_path.parent.parent.parent.parent / "agent-samples" / "azure" / "AzureOpenAIResponses.yaml" - # load the yaml from the path with yaml_path.open("r") as f: yaml_str = f.read() - # create the agent from the yaml agent = AgentFactory(client_kwargs={"credential": AzureCliCredential()}).create_agent_from_yaml(yaml_str) # use the agent diff --git a/python/samples/02-agents/declarative/get_weather_agent.py b/python/samples/02-agents/declarative/get_weather_agent.py index 75d62bc1a9..9e8118c10b 100644 --- a/python/samples/02-agents/declarative/get_weather_agent.py +++ b/python/samples/02-agents/declarative/get_weather_agent.py @@ -4,7 +4,7 @@ from random import randint from typing import Literal -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.declarative import AgentFactory from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -15,7 +15,6 @@ def get_weather(location: str, unit: Literal["celsius", "fahrenheit"] = "celsius") -> str: """A simple function tool to get weather information.""" - return f"The weather in {location} is {randint(-10, 30) if unit == 'celsius' else randint(30, 100)} degrees {unit}." @@ -24,14 +23,12 @@ async def main(): # get the path current_path = Path(__file__).parent yaml_path = current_path.parent.parent.parent.parent / "agent-samples" / "chatclient" / "GetWeather.yaml" - # load the yaml from the path with yaml_path.open("r") as f: yaml_str = f.read() - # create the AgentFactory with a chat client and bindings agent_factory = AgentFactory( - client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), bindings={"get_weather": get_weather}, ) # create the agent from the yaml diff --git a/python/samples/02-agents/declarative/inline_yaml.py b/python/samples/02-agents/declarative/inline_yaml.py index 1c7b052e4f..3f8b5158cd 100644 --- a/python/samples/02-agents/declarative/inline_yaml.py +++ b/python/samples/02-agents/declarative/inline_yaml.py @@ -16,7 +16,7 @@ Prerequisites: - `pip install agent-framework-azure-ai agent-framework-declarative --pre` - Set the following environment variables in a .env file or your environment: - - AZURE_AI_PROJECT_ENDPOINT + - FOUNDRY_PROJECT_ENDPOINT - AZURE_OPENAI_MODEL """ @@ -33,7 +33,7 @@ async def main(): id: =Env.AZURE_OPENAI_MODEL connection: kind: remote - endpoint: =Env.AZURE_AI_PROJECT_ENDPOINT + endpoint: =Env.FOUNDRY_PROJECT_ENDPOINT """ # create the agent from the yaml async with ( diff --git a/python/samples/02-agents/declarative/mcp_tool_yaml.py b/python/samples/02-agents/declarative/mcp_tool_yaml.py index 366771b903..fd6c233034 100644 --- a/python/samples/02-agents/declarative/mcp_tool_yaml.py +++ b/python/samples/02-agents/declarative/mcp_tool_yaml.py @@ -132,9 +132,9 @@ async def run_azure_ai_example(): print("Example 2: Azure AI with Foundry Connection Reference") print("=" * 60) - from azure.identity import DefaultAzureCredential + from azure.identity import AzureCliCredential - factory = AgentFactory(client_kwargs={"credential": DefaultAzureCredential()}) + factory = AgentFactory(client_kwargs={"credential": AzureCliCredential()}) print("\nCreating agent from YAML definition...") # Use async method for provider-based agent creation diff --git a/python/samples/02-agents/declarative/microsoft_learn_agent.py b/python/samples/02-agents/declarative/microsoft_learn_agent.py index a42806eb92..fc5994da21 100644 --- a/python/samples/02-agents/declarative/microsoft_learn_agent.py +++ b/python/samples/02-agents/declarative/microsoft_learn_agent.py @@ -12,11 +12,9 @@ async def main(): """Create an agent from a declarative yaml specification and run it.""" - # get the path current_path = Path(__file__).parent yaml_path = current_path.parent.parent.parent.parent / "agent-samples" / "foundry" / "MicrosoftLearnAgent.yaml" - # create the agent from the yaml async with ( AzureCliCredential() as credential, diff --git a/python/samples/02-agents/declarative/openai_responses_agent.py b/python/samples/02-agents/declarative/openai_responses_agent.py index cc4c8fb92a..1a78c8ab7a 100644 --- a/python/samples/02-agents/declarative/openai_responses_agent.py +++ b/python/samples/02-agents/declarative/openai_responses_agent.py @@ -11,15 +11,12 @@ async def main(): """Create an agent from a declarative yaml specification and run it.""" - # get the path current_path = Path(__file__).parent yaml_path = current_path.parent.parent.parent.parent / "agent-samples" / "openai" / "OpenAIResponses.yaml" - # load the yaml from the path with yaml_path.open("r") as f: yaml_str = f.read() - # create the agent from the yaml agent = AgentFactory(safe_mode=False).create_agent_from_yaml(yaml_str) # use the agent diff --git a/python/samples/02-agents/devui/azure_responses_agent/agent.py b/python/samples/02-agents/devui/azure_responses_agent/agent.py index 2d952d729a..03750411b1 100644 --- a/python/samples/02-agents/devui/azure_responses_agent/agent.py +++ b/python/samples/02-agents/devui/azure_responses_agent/agent.py @@ -7,13 +7,13 @@ - Audio inputs - And other multimodal content -The Chat Completions API (AzureOpenAIChatClient) does NOT support PDF uploads. +The Chat Completions API (FoundryChatClient) does NOT support PDF uploads. Use this agent when you need to process documents or other file types. Required environment variables: - AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint -- AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: Deployment name for Responses API - (falls back to AZURE_OPENAI_CHAT_DEPLOYMENT_NAME if not set) +- FOUNDRY_MODEL: Deployment name for Responses API + (falls back to FOUNDRY_MODEL if not set) - AZURE_OPENAI_API_KEY: Your API key (or use Azure CLI auth) """ @@ -22,7 +22,7 @@ from typing import Annotated from agent_framework import Agent, tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from dotenv import load_dotenv # Load environment variables from .env file @@ -32,8 +32,8 @@ # Get deployment name - try responses-specific env var first, fall back to chat deployment _deployment_name = os.environ.get( - "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", - os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", ""), + "FOUNDRY_MODEL", + os.environ.get("FOUNDRY_MODEL", ""), ) # Get endpoint - try responses-specific env var first, fall back to default @@ -89,8 +89,8 @@ def extract_key_points( For PDFs, you can read and understand the text, tables, and structure. For images, you can describe what you see and extract any text. """, - client=AzureOpenAIResponsesClient( - deployment_name=_deployment_name, + client=FoundryChatClient( + model=_deployment_name, endpoint=_endpoint, api_version="2025-03-01-preview", # Required for Responses API ), @@ -117,7 +117,7 @@ def main(): logger.info("") logger.info("Required environment variables:") logger.info(" - AZURE_OPENAI_ENDPOINT") - logger.info(" - AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME") + logger.info(" - FOUNDRY_MODEL") logger.info(" - AZURE_OPENAI_API_KEY (or use Azure CLI auth)") logger.info("") diff --git a/python/samples/02-agents/devui/foundry_agent/__init__.py b/python/samples/02-agents/devui/foundry_agent/__init__.py index 0ecbfc3802..bf77e4ff2a 100644 --- a/python/samples/02-agents/devui/foundry_agent/__init__.py +++ b/python/samples/02-agents/devui/foundry_agent/__init__.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. """Weather agent sample for DevUI testing.""" - from .agent import agent __all__ = ["agent"] diff --git a/python/samples/02-agents/devui/foundry_agent/agent.py b/python/samples/02-agents/devui/foundry_agent/agent.py index 62e335646b..1ba59d174f 100644 --- a/python/samples/02-agents/devui/foundry_agent/agent.py +++ b/python/samples/02-agents/devui/foundry_agent/agent.py @@ -9,7 +9,7 @@ from typing import Annotated from agent_framework import Agent, tool -from agent_framework.azure import AzureAIAgentClient +from agent_framework.azure import FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -51,9 +51,9 @@ def get_forecast( # Agent instance following Agent Framework conventions agent = Agent( name="FoundryWeatherAgent", - client=AzureAIAgentClient( - project_endpoint=os.environ.get("AZURE_AI_PROJECT_ENDPOINT"), - model_deployment_name=os.environ.get("FOUNDRY_MODEL_DEPLOYMENT_NAME"), + client=FoundryChatClient( + project_endpoint=os.environ.get("FOUNDRY_PROJECT_ENDPOINT"), + model_model=os.environ.get("FOUNDRY_MODEL_DEPLOYMENT_NAME"), credential=AzureCliCredential(), ), instructions=""" diff --git a/python/samples/02-agents/devui/in_memory_mode.py b/python/samples/02-agents/devui/in_memory_mode.py index b1e43a9a7f..2d70fe3e60 100644 --- a/python/samples/02-agents/devui/in_memory_mode.py +++ b/python/samples/02-agents/devui/in_memory_mode.py @@ -18,7 +18,7 @@ handler, tool, ) -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from agent_framework.devui import serve from dotenv import load_dotenv from typing_extensions import Never @@ -79,9 +79,9 @@ def main(): logger = logging.getLogger(__name__) # Create Azure OpenAI chat client - client = AzureOpenAIChatClient( + client = FoundryChatClient( api_key=os.environ.get("AZURE_OPENAI_API_KEY"), - deployment_name=os.environ["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], + model=os.environ["FOUNDRY_MODEL"], endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"), api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-10-21"), ) diff --git a/python/samples/02-agents/devui/spam_workflow/__init__.py b/python/samples/02-agents/devui/spam_workflow/__init__.py index 9801f7433a..1903f792bb 100644 --- a/python/samples/02-agents/devui/spam_workflow/__init__.py +++ b/python/samples/02-agents/devui/spam_workflow/__init__.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. """Spam detection workflow sample for DevUI testing.""" - from .workflow import workflow __all__ = ["workflow"] diff --git a/python/samples/02-agents/devui/weather_agent_azure/__init__.py b/python/samples/02-agents/devui/weather_agent_azure/__init__.py index 0ecbfc3802..bf77e4ff2a 100644 --- a/python/samples/02-agents/devui/weather_agent_azure/__init__.py +++ b/python/samples/02-agents/devui/weather_agent_azure/__init__.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. """Weather agent sample for DevUI testing.""" - from .agent import agent __all__ = ["agent"] diff --git a/python/samples/02-agents/devui/weather_agent_azure/agent.py b/python/samples/02-agents/devui/weather_agent_azure/agent.py index 527f32a21d..884274e9a8 100644 --- a/python/samples/02-agents/devui/weather_agent_azure/agent.py +++ b/python/samples/02-agents/devui/weather_agent_azure/agent.py @@ -20,7 +20,7 @@ function_middleware, tool, ) -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from agent_framework_devui import register_cleanup from dotenv import load_dotenv @@ -152,7 +152,7 @@ def send_email( and forecasts for any location. Always be helpful and provide detailed weather information when asked. """, - client=AzureOpenAIChatClient( + client=FoundryChatClient( api_key=os.environ.get("AZURE_OPENAI_API_KEY", ""), ), tools=[get_weather, get_forecast, send_email], diff --git a/python/samples/02-agents/devui/workflow_agents/__init__.py b/python/samples/02-agents/devui/workflow_agents/__init__.py index 67fc70ac2f..56b84d36cb 100644 --- a/python/samples/02-agents/devui/workflow_agents/__init__.py +++ b/python/samples/02-agents/devui/workflow_agents/__init__.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. """Sequential Agents Workflow - Writer → Reviewer.""" - from .workflow import workflow __all__ = ["workflow"] diff --git a/python/samples/02-agents/devui/workflow_agents/workflow.py b/python/samples/02-agents/devui/workflow_agents/workflow.py index a8a6293e35..68638d64ad 100644 --- a/python/samples/02-agents/devui/workflow_agents/workflow.py +++ b/python/samples/02-agents/devui/workflow_agents/workflow.py @@ -17,8 +17,8 @@ import os from typing import Any -from agent_framework import AgentExecutorResponse, WorkflowBuilder -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework import Agent, AgentExecutorResponse, WorkflowBuilder +from agent_framework.azure import FoundryChatClient from dotenv import load_dotenv from pydantic import BaseModel @@ -63,10 +63,10 @@ def is_approved(message: Any) -> bool: # Create Azure OpenAI chat client -client = AzureOpenAIChatClient(api_key=os.environ.get("AZURE_OPENAI_API_KEY", "")) +client = FoundryChatClient(api_key=os.environ.get("AZURE_OPENAI_API_KEY", "")) # Create Writer agent - generates content -writer = client.as_agent( +writer = Agent(client=client, name="Writer", instructions=( "You are an excellent content writer. " @@ -76,7 +76,7 @@ def is_approved(message: Any) -> bool: ) # Create Reviewer agent - evaluates and provides structured feedback -reviewer = client.as_agent( +reviewer = Agent(client=client, name="Reviewer", instructions=( "You are an expert content reviewer. " @@ -94,7 +94,7 @@ def is_approved(message: Any) -> bool: ) # Create Editor agent - improves content based on feedback -editor = client.as_agent( +editor = Agent(client=client, name="Editor", instructions=( "You are a skilled editor. " @@ -105,7 +105,7 @@ def is_approved(message: Any) -> bool: ) # Create Publisher agent - formats content for publication -publisher = client.as_agent( +publisher = Agent(client=client, name="Publisher", instructions=( "You are a publishing agent. " @@ -115,7 +115,7 @@ def is_approved(message: Any) -> bool: ) # Create Summarizer agent - creates final publication report -summarizer = client.as_agent( +summarizer = Agent(client=client, name="Summarizer", instructions=( "You are a summarizer agent. " diff --git a/python/samples/02-agents/embeddings/openai_embeddings.py b/python/samples/02-agents/embeddings/openai_embeddings.py index 62d044fd72..001b6593f5 100644 --- a/python/samples/02-agents/embeddings/openai_embeddings.py +++ b/python/samples/02-agents/embeddings/openai_embeddings.py @@ -21,7 +21,7 @@ async def main() -> None: """Generate embeddings with OpenAI.""" - client = OpenAIEmbeddingClient(model_id="text-embedding-3-small") + client = OpenAIEmbeddingClient(model="text-embedding-3-small") # 1. Generate a single embedding. result = await client.get_embeddings(["Hello, world!"]) diff --git a/python/samples/02-agents/mcp/agent_as_mcp_server.py b/python/samples/02-agents/mcp/agent_as_mcp_server.py index 2769a95931..97cbcf3f75 100644 --- a/python/samples/02-agents/mcp/agent_as_mcp_server.py +++ b/python/samples/02-agents/mcp/agent_as_mcp_server.py @@ -3,7 +3,7 @@ from typing import Annotated, Any import anyio -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -27,7 +27,7 @@ ], "env": { "OPENAI_API_KEY": "", - "OPENAI_RESPONSES_MODEL_ID": "", + "OPENAI_MODEL": "", } } } @@ -56,7 +56,8 @@ def get_item_price( async def run() -> None: # Define an agent # Agent's name and description provide better context for AI model - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="RestaurantAgent", description="Answer questions about the menu.", tools=[get_specials, get_item_price], diff --git a/python/samples/02-agents/mcp/mcp_github_pat.py b/python/samples/02-agents/mcp/mcp_github_pat.py index cea266f789..63d70a344d 100644 --- a/python/samples/02-agents/mcp/mcp_github_pat.py +++ b/python/samples/02-agents/mcp/mcp_github_pat.py @@ -21,7 +21,7 @@ 2. Environment variables: - GITHUB_PAT: Your GitHub Personal Access Token (required) - OPENAI_API_KEY: Your OpenAI API key (required) - - OPENAI_RESPONSES_MODEL_ID: Your OpenAI model ID (required) + - OPENAI_MODEL: Your OpenAI model ID (required) """ diff --git a/python/samples/02-agents/middleware/agent_and_run_level_middleware.py b/python/samples/02-agents/middleware/agent_and_run_level_middleware.py index 158d90daee..ade4ff5e71 100644 --- a/python/samples/02-agents/middleware/agent_and_run_level_middleware.py +++ b/python/samples/02-agents/middleware/agent_and_run_level_middleware.py @@ -7,13 +7,14 @@ from typing import Annotated from agent_framework import ( + Agent, AgentContext, AgentMiddleware, AgentResponse, FunctionInvocationContext, tool, ) -from agent_framework.azure import AzureAIAgentClient +from agent_framework.azure import FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -194,7 +195,8 @@ async def main() -> None: # authentication option. async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="WeatherAgent", instructions="You are a helpful weather assistant.", tools=get_weather, diff --git a/python/samples/02-agents/middleware/chat_middleware.py b/python/samples/02-agents/middleware/chat_middleware.py index 48caf9369b..6ee1c59fad 100644 --- a/python/samples/02-agents/middleware/chat_middleware.py +++ b/python/samples/02-agents/middleware/chat_middleware.py @@ -6,6 +6,7 @@ from typing import Annotated from agent_framework import ( + Agent, ChatContext, ChatMiddleware, ChatResponse, @@ -14,7 +15,7 @@ chat_middleware, tool, ) -from agent_framework.azure import AzureAIAgentClient +from agent_framework.azure import FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -150,7 +151,8 @@ async def class_based_chat_middleware() -> None: # authentication option. async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="EnhancedChatAgent", instructions="You are a helpful AI assistant.", # Register class-based middleware at agent level (applies to all runs) @@ -172,7 +174,8 @@ async def function_based_chat_middleware() -> None: async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="FunctionMiddlewareAgent", instructions="You are a helpful AI assistant.", # Register function-based middleware at agent level @@ -202,7 +205,8 @@ async def run_level_middleware() -> None: async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="RunLevelAgent", instructions="You are a helpful AI assistant.", tools=get_weather, diff --git a/python/samples/02-agents/middleware/class_based_middleware.py b/python/samples/02-agents/middleware/class_based_middleware.py index bce31315ee..311a5f3fce 100644 --- a/python/samples/02-agents/middleware/class_based_middleware.py +++ b/python/samples/02-agents/middleware/class_based_middleware.py @@ -7,6 +7,7 @@ from typing import Annotated from agent_framework import ( + Agent, AgentContext, AgentMiddleware, AgentResponse, @@ -15,7 +16,7 @@ Message, tool, ) -from agent_framework.azure import AzureAIAgentClient +from agent_framework.azure import FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -105,7 +106,8 @@ async def main() -> None: # authentication option. async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="WeatherAgent", instructions="You are a helpful weather assistant.", tools=get_weather, diff --git a/python/samples/02-agents/middleware/decorator_middleware.py b/python/samples/02-agents/middleware/decorator_middleware.py index e02e47e252..c0536760a1 100644 --- a/python/samples/02-agents/middleware/decorator_middleware.py +++ b/python/samples/02-agents/middleware/decorator_middleware.py @@ -4,11 +4,12 @@ import datetime from agent_framework import ( + Agent, agent_middleware, function_middleware, tool, ) -from agent_framework.azure import AzureAIAgentClient +from agent_framework.azure import FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv @@ -79,7 +80,8 @@ async def main() -> None: # authentication option. async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="TimeAgent", instructions="You are a helpful time assistant. Call get_current_time when asked about time.", tools=get_current_time, diff --git a/python/samples/02-agents/middleware/exception_handling_with_middleware.py b/python/samples/02-agents/middleware/exception_handling_with_middleware.py index 22bd374567..7e53901790 100644 --- a/python/samples/02-agents/middleware/exception_handling_with_middleware.py +++ b/python/samples/02-agents/middleware/exception_handling_with_middleware.py @@ -4,8 +4,8 @@ from collections.abc import Awaitable, Callable from typing import Annotated -from agent_framework import FunctionInvocationContext, tool -from agent_framework.azure import AzureAIAgentClient +from agent_framework import Agent, FunctionInvocationContext, tool +from agent_framework.azure import FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -66,7 +66,8 @@ async def main() -> None: # authentication option. async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="DataAgent", instructions="You are a helpful data assistant. Use the data service tool to fetch information for users.", tools=unstable_data_service, diff --git a/python/samples/02-agents/middleware/function_based_middleware.py b/python/samples/02-agents/middleware/function_based_middleware.py index f0ea0f2f26..664ae341c7 100644 --- a/python/samples/02-agents/middleware/function_based_middleware.py +++ b/python/samples/02-agents/middleware/function_based_middleware.py @@ -7,11 +7,12 @@ from typing import Annotated from agent_framework import ( + Agent, AgentContext, FunctionInvocationContext, tool, ) -from agent_framework.azure import AzureAIAgentClient +from agent_framework.azure import FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -92,7 +93,8 @@ async def main() -> None: # authentication option. async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="WeatherAgent", instructions="You are a helpful weather assistant.", tools=get_weather, diff --git a/python/samples/02-agents/middleware/middleware_termination.py b/python/samples/02-agents/middleware/middleware_termination.py index 89acce1b6f..878642a036 100644 --- a/python/samples/02-agents/middleware/middleware_termination.py +++ b/python/samples/02-agents/middleware/middleware_termination.py @@ -6,6 +6,7 @@ from typing import Annotated from agent_framework import ( + Agent, AgentContext, AgentMiddleware, AgentResponse, @@ -13,7 +14,7 @@ MiddlewareTermination, tool, ) -from agent_framework.azure import AzureAIAgentClient +from agent_framework.azure import FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -118,7 +119,8 @@ async def pre_termination_middleware() -> None: print("\n--- Example 1: Pre-termination MiddlewareTypes ---") async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="WeatherAgent", instructions="You are a helpful weather assistant.", tools=get_weather, @@ -145,7 +147,8 @@ async def post_termination_middleware() -> None: print("\n--- Example 2: Post-termination MiddlewareTypes ---") async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="WeatherAgent", instructions="You are a helpful weather assistant.", tools=get_weather, diff --git a/python/samples/02-agents/middleware/override_result_with_middleware.py b/python/samples/02-agents/middleware/override_result_with_middleware.py index 5ed4d2a937..14bf42acd0 100644 --- a/python/samples/02-agents/middleware/override_result_with_middleware.py +++ b/python/samples/02-agents/middleware/override_result_with_middleware.py @@ -7,6 +7,7 @@ from typing import Annotated from agent_framework import ( + Agent, AgentContext, AgentResponse, AgentResponseUpdate, @@ -188,9 +189,10 @@ async def main() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = OpenAIResponsesClient( - middleware=[validate_weather_middleware, weather_override_middleware], - ).as_agent( + agent = Agent( + client=OpenAIResponsesClient( + middleware=[validate_weather_middleware, weather_override_middleware], + ), name="WeatherAgent", instructions="You are a helpful weather assistant. Use the weather tool to get current conditions.", tools=get_weather, diff --git a/python/samples/02-agents/middleware/runtime_context_delegation.py b/python/samples/02-agents/middleware/runtime_context_delegation.py index 7aa19b3437..e241dd0511 100644 --- a/python/samples/02-agents/middleware/runtime_context_delegation.py +++ b/python/samples/02-agents/middleware/runtime_context_delegation.py @@ -4,8 +4,8 @@ from collections.abc import Awaitable, Callable from typing import Annotated -from agent_framework import FunctionInvocationContext, function_middleware, tool -from agent_framework.openai import OpenAIChatClient +from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool +from agent_framework.azure import FoundryChatClient from dotenv import load_dotenv from pydantic import Field @@ -149,10 +149,10 @@ async def pattern_1_single_agent_with_closure() -> None: print("Use case: Single agent with multiple tools sharing runtime context") print() - client = OpenAIChatClient(model_id="gpt-4o-mini") + client = FoundryChatClient(model="gpt-4o-mini") # Create agent with both tools and shared context via middleware - communication_agent = client.as_agent( + communication_agent = Agent(client=client, name="communication_agent", instructions=( "You are a communication assistant that can send emails and notifications. " @@ -294,17 +294,17 @@ async def sms_kwargs_tracker(context: FunctionInvocationContext, call_next: Call print(f"[SMSAgent] Received runtime context: {list(context.kwargs.keys())}") await call_next() - client = OpenAIChatClient(model_id="gpt-4o-mini") + client = FoundryChatClient(model="gpt-4o-mini") # Create specialized sub-agents - email_agent = client.as_agent( + email_agent = Agent(client=client, name="email_agent", instructions="You send emails using the send_email_v2 tool.", tools=[send_email_v2], middleware=[email_kwargs_tracker], ) - sms_agent = client.as_agent( + sms_agent = Agent(client=client, name="sms_agent", instructions="You send SMS messages using the send_sms tool.", tools=[send_sms], @@ -312,7 +312,7 @@ async def sms_kwargs_tracker(context: FunctionInvocationContext, call_next: Call ) # Create coordinator that delegates to sub-agents - coordinator = client.as_agent( + coordinator = Agent(client=client, name="coordinator", instructions=( "You coordinate communication tasks. " @@ -396,10 +396,10 @@ async def pattern_3_hierarchical_with_middleware() -> None: auth_middleware = AuthContextMiddleware() - client = OpenAIChatClient(model_id="gpt-4o-mini") + client = FoundryChatClient(model="gpt-4o-mini") # Sub-agent with validation middleware - protected_agent = client.as_agent( + protected_agent = Agent(client=client, name="protected_agent", instructions="You perform protected operations that require authentication.", tools=[protected_operation], @@ -407,7 +407,7 @@ async def pattern_3_hierarchical_with_middleware() -> None: ) # Coordinator delegates to protected agent - coordinator = client.as_agent( + coordinator = Agent(client=client, name="coordinator", instructions="You coordinate protected operations. Delegate to protected_executor.", tools=[ diff --git a/python/samples/02-agents/middleware/session_behavior_middleware.py b/python/samples/02-agents/middleware/session_behavior_middleware.py index 2efe896fae..34e13a4bc1 100644 --- a/python/samples/02-agents/middleware/session_behavior_middleware.py +++ b/python/samples/02-agents/middleware/session_behavior_middleware.py @@ -5,11 +5,12 @@ from typing import Annotated from agent_framework import ( + Agent, AgentContext, InMemoryHistoryProvider, tool, ) -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -81,7 +82,8 @@ async def main() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + agent = Agent( + client=FoundryChatClient(credential=AzureCliCredential()), name="WeatherAgent", instructions="You are a helpful weather assistant.", tools=get_weather, diff --git a/python/samples/02-agents/middleware/shared_state_middleware.py b/python/samples/02-agents/middleware/shared_state_middleware.py index fbe9f54c92..f6b92a9bf2 100644 --- a/python/samples/02-agents/middleware/shared_state_middleware.py +++ b/python/samples/02-agents/middleware/shared_state_middleware.py @@ -6,10 +6,11 @@ from typing import Annotated from agent_framework import ( + Agent, FunctionInvocationContext, tool, ) -from agent_framework.azure import AzureAIAgentClient +from agent_framework.azure import FoundryChatClient from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -103,7 +104,8 @@ async def main() -> None: # authentication option. async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + Agent( + client=FoundryChatClient(credential=credential), name="UtilityAgent", instructions="You are a helpful assistant that can provide weather information and current time.", tools=[get_weather, get_time], diff --git a/python/samples/02-agents/multimodal_input/azure_chat_multimodal.py b/python/samples/02-agents/multimodal_input/azure_chat_multimodal.py index 5883f184cf..58cca596f0 100644 --- a/python/samples/02-agents/multimodal_input/azure_chat_multimodal.py +++ b/python/samples/02-agents/multimodal_input/azure_chat_multimodal.py @@ -3,7 +3,7 @@ import asyncio from agent_framework import Content, Message -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -21,12 +21,11 @@ def create_sample_image() -> str: async def test_image() -> None: """Test image analysis with Azure OpenAI.""" # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. Requires AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME + # authentication option. Requires AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL # environment variables to be set. # Alternatively, you can pass deployment_name explicitly: - # client = AzureOpenAIChatClient(credential=AzureCliCredential(), deployment_name="your-deployment-name") - client = AzureOpenAIChatClient(credential=AzureCliCredential()) - + # client = FoundryChatClient(credential=AzureCliCredential(), model="your-deployment-name") + client = FoundryChatClient(credential=AzureCliCredential()) image_uri = create_sample_image() message = Message( role="user", @@ -35,7 +34,6 @@ async def test_image() -> None: Content.from_uri(uri=image_uri, media_type="image/png"), ], ) - response = await client.get_response([message]) print(f"Image Response: {response}") diff --git a/python/samples/02-agents/multimodal_input/azure_responses_multimodal.py b/python/samples/02-agents/multimodal_input/azure_responses_multimodal.py index 8c7dd76f7e..0c2f73b4c2 100644 --- a/python/samples/02-agents/multimodal_input/azure_responses_multimodal.py +++ b/python/samples/02-agents/multimodal_input/azure_responses_multimodal.py @@ -4,7 +4,7 @@ from pathlib import Path from agent_framework import Content, Message -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -30,11 +30,11 @@ def create_sample_image() -> str: async def test_image() -> None: """Test image analysis with Azure OpenAI Responses API.""" # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. Requires AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME + # authentication option. Requires AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL # environment variables to be set. # Alternatively, you can pass deployment_name explicitly: - # client = AzureOpenAIResponsesClient(credential=AzureCliCredential(), deployment_name="your-deployment-name") - client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) + # client = FoundryChatClient(credential=AzureCliCredential(), model="your-deployment-name") + client = FoundryChatClient(credential=AzureCliCredential()) image_uri = create_sample_image() message = Message( @@ -51,7 +51,7 @@ async def test_image() -> None: async def test_pdf() -> None: """Test PDF document analysis with Azure OpenAI Responses API.""" - client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) + client = FoundryChatClient(credential=AzureCliCredential()) pdf_bytes = load_sample_pdf() message = Message( diff --git a/python/samples/02-agents/multimodal_input/openai_chat_multimodal.py b/python/samples/02-agents/multimodal_input/openai_chat_multimodal.py index 1e0849d8d6..1f6c0a72ba 100644 --- a/python/samples/02-agents/multimodal_input/openai_chat_multimodal.py +++ b/python/samples/02-agents/multimodal_input/openai_chat_multimodal.py @@ -6,7 +6,7 @@ from pathlib import Path from agent_framework import Content, Message -from agent_framework.openai import OpenAIChatClient +from agent_framework.azure import FoundryChatClient from dotenv import load_dotenv # Load environment variables from .env file @@ -46,7 +46,7 @@ def create_sample_audio() -> str: async def test_image() -> None: """Test image analysis with OpenAI.""" - client = OpenAIChatClient(model_id="gpt-4o") + client = FoundryChatClient(model="gpt-4o") image_uri = create_sample_image() message = Message( @@ -63,7 +63,7 @@ async def test_image() -> None: async def test_audio() -> None: """Test audio analysis with OpenAI.""" - client = OpenAIChatClient(model_id="gpt-4o-audio-preview") + client = FoundryChatClient(model="gpt-4o-audio-preview") audio_uri = create_sample_audio() message = Message( @@ -80,7 +80,7 @@ async def test_audio() -> None: async def test_pdf() -> None: """Test PDF document analysis with OpenAI.""" - client = OpenAIChatClient(model_id="gpt-4o") + client = FoundryChatClient(model="gpt-4o") pdf_bytes = load_sample_pdf() message = Message( diff --git a/python/samples/02-agents/observability/advanced_manual_setup_console_output.py b/python/samples/02-agents/observability/advanced_manual_setup_console_output.py index af7fcc6287..52ada5b808 100644 --- a/python/samples/02-agents/observability/advanced_manual_setup_console_output.py +++ b/python/samples/02-agents/observability/advanced_manual_setup_console_output.py @@ -6,8 +6,8 @@ from typing import Annotated from agent_framework import Message, tool +from agent_framework.azure import FoundryChatClient from agent_framework.observability import enable_instrumentation -from agent_framework.openai import OpenAIChatClient from dotenv import load_dotenv from opentelemetry._logs import set_logger_provider from opentelemetry.metrics import set_meter_provider @@ -115,7 +115,7 @@ async def run_chat_client() -> None: 2 spans with gen_ai.operation.name=execute_tool """ - client = OpenAIChatClient() + client = FoundryChatClient() message = "What's the weather in Amsterdam and in Paris?" print(f"User: {message}") print("Assistant: ", end="") diff --git a/python/samples/02-agents/observability/agent_observability.py b/python/samples/02-agents/observability/agent_observability.py index ae4057d2f0..b46ebed261 100644 --- a/python/samples/02-agents/observability/agent_observability.py +++ b/python/samples/02-agents/observability/agent_observability.py @@ -5,8 +5,8 @@ from typing import Annotated from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient from agent_framework.observability import configure_otel_providers, get_tracer -from agent_framework.openai import OpenAIChatClient from dotenv import load_dotenv from opentelemetry.trace import SpanKind from opentelemetry.trace.span import format_trace_id @@ -47,7 +47,7 @@ async def main(): print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}") agent = Agent( - client=OpenAIChatClient(), + client=FoundryChatClient(), tools=get_weather, name="WeatherAgent", instructions="You are a weather assistant.", diff --git a/python/samples/02-agents/observability/agent_with_foundry_tracing.py b/python/samples/02-agents/observability/agent_with_foundry_tracing.py index 00780e8f12..34177d1215 100644 --- a/python/samples/02-agents/observability/agent_with_foundry_tracing.py +++ b/python/samples/02-agents/observability/agent_with_foundry_tracing.py @@ -35,7 +35,7 @@ So ensure you have the `azure-monitor-opentelemetry` package installed. """ -# For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable +# For loading the `FOUNDRY_PROJECT_ENDPOINT` environment variable load_dotenv() logger = logging.getLogger(__name__) @@ -57,7 +57,7 @@ async def get_weather( async def main(): async with ( AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, + AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=credential) as project_client, ): # This will enable tracing and configure the application to send telemetry data to the # Application Insights instance attached to the Azure AI project. diff --git a/python/samples/02-agents/observability/azure_ai_agent_observability.py b/python/samples/02-agents/observability/azure_ai_agent_observability.py deleted file mode 100644 index 286dba33ec..0000000000 --- a/python/samples/02-agents/observability/azure_ai_agent_observability.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os -from random import randint -from typing import Annotated - -from agent_framework import Agent, tool -from agent_framework.azure import AzureAIClient -from agent_framework.observability import get_tracer -from azure.ai.projects.aio import AIProjectClient -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from opentelemetry.trace import SpanKind -from opentelemetry.trace.span import format_trace_id -from pydantic import Field - -""" -This sample shows you can setup telemetry for an Azure AI agent. -It uses the Azure AI client to setup the telemetry, this calls out to -Azure AI for the connection string of the attached Application Insights -instance. - -You must add an Application Insights instance to your Azure AI project -for this sample to work. -""" - -# For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable -load_dotenv() - - -# NOTE: approval_mode="never_require" is for sample brevity. -# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -async def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def main(): - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - AzureAIClient(project_client=project_client) as client, - ): - # This will enable tracing and configure the application to send telemetry data to the - # Application Insights instance attached to the Azure AI project. - # This will override any existing configuration. - await client.configure_azure_monitor(enable_live_metrics=True) - - questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"] - - with get_tracer().start_as_current_span("Single Agent Chat", kind=SpanKind.CLIENT) as current_span: - print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}") - - agent = Agent( - client=client, - tools=get_weather, - name="WeatherAgent", - instructions="You are a weather assistant.", - id="edvan-weather-agent", - ) - session = agent.create_session() - for question in questions: - print(f"\nUser: {question}") - print(f"{agent.name}: ", end="") - async for update in agent.run(question, session=session, stream=True): - if update.text: - print(update.text, end="") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/anthropic/anthropic_advanced.py b/python/samples/02-agents/providers/anthropic/anthropic_advanced.py index 9fac03a645..1bdadd5de9 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_advanced.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_advanced.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. - import asyncio +from agent_framework import Agent from agent_framework.anthropic import AnthropicChatOptions, AnthropicClient from dotenv import load_dotenv @@ -31,7 +31,7 @@ async def main() -> None: # Create web search tool configuration using instance method web_search_tool = client.get_web_search_tool() - agent = client.as_agent( + agent = Agent(client=client, name="DocsAgent", instructions="You are a helpful agent for both Microsoft docs questions and general questions.", tools=[mcp_tool, web_search_tool], diff --git a/python/samples/02-agents/providers/anthropic/anthropic_basic.py b/python/samples/02-agents/providers/anthropic/anthropic_basic.py index 4a97571456..5c62aa82c8 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_basic.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_basic.py @@ -4,7 +4,7 @@ from random import randint from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.anthropic import AnthropicClient from dotenv import load_dotenv @@ -34,7 +34,8 @@ async def non_streaming_example() -> None: """Example of non-streaming response (get the complete result at once).""" print("=== Non-streaming Response Example ===") - agent = AnthropicClient().as_agent( + agent = Agent( + client=AnthropicClient(), name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, @@ -50,7 +51,8 @@ async def streaming_example() -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") - agent = AnthropicClient().as_agent( + agent = Agent( + client=AnthropicClient(), name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, diff --git a/python/samples/02-agents/providers/anthropic/anthropic_foundry.py b/python/samples/02-agents/providers/anthropic/anthropic_foundry.py index 481f3bb6b4..b07766de98 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_foundry.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_foundry.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. - import asyncio +from agent_framework import Agent from agent_framework.anthropic import AnthropicClient from anthropic import AsyncAnthropicFoundry from dotenv import load_dotenv @@ -42,7 +42,7 @@ async def main() -> None: # Create web search tool configuration using instance method web_search_tool = client.get_web_search_tool() - agent = client.as_agent( + agent = Agent(client=client, name="DocsAgent", instructions="You are a helpful agent for both Microsoft docs questions and general questions.", tools=[mcp_tool, web_search_tool], diff --git a/python/samples/02-agents/providers/anthropic/anthropic_skills.py b/python/samples/02-agents/providers/anthropic/anthropic_skills.py index 9296b1d9a2..a93874c269 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_skills.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_skills.py @@ -4,7 +4,7 @@ import logging from pathlib import Path -from agent_framework import Content +from agent_framework import Agent, Content from agent_framework.anthropic import AnthropicChatOptions, AnthropicClient from dotenv import load_dotenv @@ -35,7 +35,8 @@ async def main() -> None: # Create a agent with the pptx skill enabled # Skills also need the code interpreter tool to function - agent = client.as_agent( + agent = Agent( + client=client, name="DocsAgent", instructions="You are a helpful agent for creating powerpoint presentations.", tools=client.get_code_interpreter_tool(), diff --git a/python/samples/02-agents/providers/azure/README.md b/python/samples/02-agents/providers/azure/README.md new file mode 100644 index 0000000000..87b492fe16 --- /dev/null +++ b/python/samples/02-agents/providers/azure/README.md @@ -0,0 +1,269 @@ +# Azure OpenAI Agent Examples + +This folder contains examples demonstrating different ways to create and use agents with the different Azure OpenAI chat client from the `agent_framework.azure` package. + +## Examples + +| File | Description | +|------|-------------| +| [`azure_assistants_basic.py`](azure_assistants_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIAssistantsClient`. Shows both streaming and non-streaming responses with automatic assistant creation and cleanup. | +| [`azure_assistants_with_code_interpreter.py`](azure_assistants_with_code_interpreter.py) | Shows how to use `AzureOpenAIAssistantsClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | +| [`azure_assistants_with_existing_assistant.py`](azure_assistants_with_existing_assistant.py) | Shows how to work with a pre-existing assistant by providing the assistant ID to the Azure Assistants client. Demonstrates proper cleanup of manually created assistants. | +| [`azure_assistants_with_explicit_settings.py`](azure_assistants_with_explicit_settings.py) | Shows how to initialize an agent with a specific assistants client, configuring settings explicitly including endpoint and deployment name. | +| [`azure_assistants_with_function_tools.py`](azure_assistants_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | +| [`azure_assistants_with_session.py`](azure_assistants_with_session.py) | Demonstrates session management with Azure agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | +| [`azure_chat_client_basic.py`](azure_chat_client_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIChatClient`. Shows both streaming and non-streaming responses for chat-based interactions with Azure OpenAI models. | +| [`azure_chat_client_with_explicit_settings.py`](azure_chat_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific chat client, configuring settings explicitly including endpoint and deployment name. | +| [`azure_chat_client_with_function_tools.py`](azure_chat_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | +| [`azure_chat_client_with_session.py`](azure_chat_client_with_session.py) | Demonstrates session management with Azure agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | +| [`azure_responses_client_basic.py`](azure_responses_client_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with Azure OpenAI models. | +| [`azure_responses_client_code_interpreter_files.py`](azure_responses_client_code_interpreter_files.py) | Demonstrates using `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with file uploads for data analysis. Shows how to create, upload, and analyze CSV files using Python code execution with Azure OpenAI Responses. | +| [`azure_responses_client_image_analysis.py`](azure_responses_client_image_analysis.py) | Shows how to use Azure OpenAI Responses for image analysis and vision tasks. Demonstrates multi-modal messages combining text and image content using remote URLs. | +| [`azure_responses_client_with_code_interpreter.py`](azure_responses_client_with_code_interpreter.py) | Shows how to use `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | +| [`azure_responses_client_with_explicit_settings.py`](azure_responses_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific responses client, configuring settings explicitly including endpoint and deployment name. | +| [`azure_responses_client_with_file_search.py`](azure_responses_client_with_file_search.py) | Demonstrates using `AzureOpenAIResponsesClient.get_file_search_tool()` with Azure OpenAI Responses Client for direct document-based question answering and information retrieval from vector stores. | +| [`azure_responses_client_with_foundry.py`](azure_responses_client_with_foundry.py) | Shows how to create an agent using an Azure AI Foundry project endpoint instead of a direct Azure OpenAI endpoint. Requires the `azure-ai-projects` package. | +| [`azure_responses_client_with_function_tools.py`](azure_responses_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | +| [`azure_responses_client_with_hosted_mcp.py`](azure_responses_client_with_hosted_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with hosted Model Context Protocol (MCP) servers using `AzureOpenAIResponsesClient.get_mcp_tool()` for extended functionality. | +| [`azure_responses_client_with_local_mcp.py`](azure_responses_client_with_local_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with local Model Context Protocol (MCP) servers using MCPStreamableHTTPTool for extended functionality. | +| [`azure_responses_client_with_session.py`](azure_responses_client_with_session.py) | Demonstrates session management with Azure agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | + +## Environment Variables + +Make sure to set the following environment variables before running the examples: + +- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint +- `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`: The name of your Azure OpenAI chat model deployment +- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: The name of your Azure OpenAI Responses deployment + +For the Foundry project sample (`azure_responses_client_with_foundry.py`), also set: +- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint + +Optionally, you can set: +- `AZURE_OPENAI_API_VERSION`: The API version to use (default is `2024-02-15-preview`) +- `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (if not using `AzureCliCredential`) +- `AZURE_OPENAI_BASE_URL`: Your Azure OpenAI base URL (if different from the endpoint) + +## Authentication + +All examples use `AzureCliCredential` for authentication. Run `az login` in your terminal before running the examples, or replace `AzureCliCredential` with your preferred authentication method. + +## Required role-based access control (RBAC) roles + +To access the Azure OpenAI API, your Azure account or service principal needs one of the following RBAC roles assigned to the Azure OpenAI resource: + +- **Cognitive Services OpenAI User**: Provides read access to Azure OpenAI resources and the ability to call the inference APIs. This is the minimum role required for running these examples. +- **Cognitive Services OpenAI Contributor**: Provides full access to Azure OpenAI resources, including the ability to create, update, and delete deployments and models. + +For most scenarios, the **Cognitive Services OpenAI User** role is sufficient. You can assign this role through the Azure portal under the Azure OpenAI resource's "Access control (IAM)" section. + +For more detailed information about Azure OpenAI RBAC roles, see: [Role-based access control for Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/role-based-access-control) +# Azure AI Agent Examples + +This folder contains examples demonstrating different ways to create and use agents with the Azure AI client from the `agent_framework.azure` package. These examples use the `AzureAIClient` with the `azure-ai-projects` 2.x (V2) API surface (see [changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-projects/CHANGELOG.md#200b1-2025-11-11)). For V1 (`azure-ai-agents` 1.x) samples using `AzureAIAgentClient`, see the [Azure AI V1 examples folder](../azure_ai_agent/). When using preview-only agent creation features on GA SDK versions, create `AIProjectClient` with `allow_preview=True`. + +## Examples + +| File | Description | +|------|-------------| +| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIProjectAgentProvider`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. | +| [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive guide to `AzureAIProjectAgentProvider` methods: `create_agent()` for creating new agents, `get_agent()` for retrieving existing agents (by name, reference, or details), and `as_agent()` for wrapping SDK objects without HTTP calls. | +| [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation by using `provider.get_agent()` to retrieve the latest version. | +| [`azure_ai_with_agent_as_tool.py`](azure_ai_with_agent_as_tool.py) | Shows how to use the agent-as-tool pattern with Azure AI agents, where one agent delegates work to specialized sub-agents wrapped as tools using `as_tool()`. Demonstrates hierarchical agent architectures. | +| [`azure_ai_with_agent_to_agent.py`](azure_ai_with_agent_to_agent.py) | Shows how to use Agent-to-Agent (A2A) capabilities with Azure AI agents to enable communication with other agents using the A2A protocol. Requires an A2A connection configured in your Azure AI project. | +| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Shows how to use Azure AI Search with Azure AI agents to search through indexed data and answer user questions with proper citations. Requires an Azure AI Search connection and index configured in your Azure AI project. | +| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to search the web for current information and provide grounded responses with citations. Requires a Bing connection configured in your Azure AI project. | +| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to search custom search instances and provide responses with relevant results. Requires a Bing Custom Search connection and instance configured in your Azure AI project. | +| [`azure_ai_with_browser_automation.py`](azure_ai_with_browser_automation.py) | Shows how to use Browser Automation with Azure AI agents to perform automated web browsing tasks and provide responses based on web interactions. Requires a Browser Automation connection configured in your Azure AI project. | +| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use `AzureAIClient.get_code_interpreter_tool()` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. | +| [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. | +| [`azure_ai_with_code_interpreter_file_download.py`](azure_ai_with_code_interpreter_file_download.py) | Shows how to download files generated by code interpreter using the OpenAI containers API. | +| [`azure_ai_with_content_filtering.py`](azure_ai_with_content_filtering.py) | Shows how to enable content filtering (RAI policy) on Azure AI agents using `RaiConfig`. Requires creating an RAI policy in Azure AI Foundry portal first. | +| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent name and version to the Azure AI client. Demonstrates agent reuse patterns for production scenarios. | +| [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Demonstrates how to use an existing conversation created on the service side with Azure AI agents. Shows two approaches: specifying conversation ID at the client level and using AgentSession with an existing conversation ID. | +| [`azure_ai_with_application_endpoint.py`](azure_ai_with_application_endpoint.py) | Demonstrates calling the Azure AI application-scoped endpoint. | +| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIClient` settings, including project endpoint, model deployment, and credentials rather than relying on environment variable defaults. | +| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use `AzureAIClient.get_file_search_tool()` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. | +| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent using `AzureAIClient.get_mcp_tool()`. | +| [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate local Model Context Protocol (MCP) tools with Azure AI agents. | +| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. | +| [`azure_ai_with_runtime_json_schema.py`](azure_ai_with_runtime_json_schema.py) | Shows how to use structured outputs (response format) with Azure AI agents using a JSON schema to enforce specific response schemas. | +| [`azure_ai_with_search_context_agentic.py`](../../context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py) | Shows how to use AzureAISearchContextProvider with agentic mode. Uses Knowledge Bases for multi-hop reasoning across documents with query planning. Recommended for most scenarios - slightly slower with more token consumption for query planning, but more accurate results. | +| [`azure_ai_with_search_context_semantic.py`](../../context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py) | Shows how to use AzureAISearchContextProvider with semantic mode. Fast hybrid search with vector + keyword search and semantic ranking for RAG. Best for simple queries where speed is critical. | +| [`azure_ai_with_sharepoint.py`](azure_ai_with_sharepoint.py) | Shows how to use SharePoint grounding with Azure AI agents to search through SharePoint content and answer user questions with proper citations. Requires a SharePoint connection configured in your Azure AI project. | +| [`azure_ai_with_session.py`](azure_ai_with_session.py) | Demonstrates session management with Azure AI agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | +| [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use `AzureAIClient.get_image_generation_tool()` with Azure AI agents to generate images based on text prompts. | +| [`azure_ai_with_memory_search.py`](azure_ai_with_memory_search.py) | Shows how to use memory search functionality with Azure AI agents for conversation persistence. Demonstrates creating memory stores and enabling agents to search through conversation history. | +| [`azure_ai_with_microsoft_fabric.py`](azure_ai_with_microsoft_fabric.py) | Shows how to use Microsoft Fabric with Azure AI agents to query Fabric data sources and provide responses based on data analysis. Requires a Microsoft Fabric connection configured in your Azure AI project. | +| [`azure_ai_with_openapi.py`](azure_ai_with_openapi.py) | Shows how to integrate OpenAPI specifications with Azure AI agents using dictionary-based tool configuration. Demonstrates using external REST APIs for dynamic data lookup. | +| [`azure_ai_with_reasoning.py`](azure_ai_with_reasoning.py) | Shows how to enable reasoning for a model that supports it. | +| [`azure_ai_with_web_search.py`](azure_ai_with_web_search.py) | Shows how to use `AzureAIClient.get_web_search_tool()` with Azure AI agents to perform web searches and retrieve up-to-date information from the internet. | + +## Environment Variables + +Before running the examples, you need to set up your environment variables. You can do this in one of two ways: + +### Option 1: Using a .env file (Recommended) + +1. Copy the `.env.example` file from the `python` directory to create a `.env` file: + + ```bash + cp ../../../../.env.example ../../../../.env + ``` + +2. Edit the `.env` file and add your values: + + ```env + AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" + AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" + ``` + +### Option 2: Using environment variables directly + +Set the environment variables in your shell: + +```bash +export AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" +export AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" +``` + +### Required Variables + +- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint (required for all examples) +- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (required for all examples) + +## Authentication + +All examples use `AzureCliCredential` for authentication by default. Before running the examples: + +1. Install the Azure CLI +2. Run `az login` to authenticate with your Azure account +3. Ensure you have appropriate permissions to the Azure AI project + +Alternatively, you can replace `AzureCliCredential` with other authentication options like `DefaultAzureCredential` or environment-based credentials. + +## Running the Examples + +Each example can be run independently. Navigate to this directory and run any example: + +```bash +python azure_ai_basic.py +python azure_ai_with_code_interpreter.py +# ... etc +``` + +The examples demonstrate various patterns for working with Azure AI agents, from basic usage to advanced scenarios like session management and structured outputs. +# Azure AI Agent Examples + +This folder contains examples demonstrating different ways to create and use agents with Azure AI using the `AzureAIAgentsProvider` from the `agent_framework.azure` package. These examples use the `azure-ai-agents` 1.x (V1) API surface. For updated V2 (`azure-ai-projects` 2.x) samples, see the [Azure AI V2 examples folder](../azure_ai/). + +## Provider Pattern + +All examples in this folder use the `AzureAIAgentsProvider` class which provides a high-level interface for agent operations: + +- **`create_agent()`** - Create a new agent on the Azure AI service +- **`get_agent()`** - Retrieve an existing agent by ID or from a pre-fetched Agent object +- **`as_agent()`** - Wrap an SDK Agent object as a Agent without HTTP calls + +```python +from agent_framework.azure import AzureAIAgentsProvider +from azure.identity.aio import AzureCliCredential + +async with ( + AzureCliCredential() as credential, + AzureAIAgentsProvider(credential=credential) as provider, +): + agent = await provider.create_agent( + name="MyAgent", + instructions="You are a helpful assistant.", + tools=my_function, + ) + result = await agent.run("Hello!") +``` + +## Examples + +| File | Description | +|------|-------------| +| [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive example demonstrating all `AzureAIAgentsProvider` methods: `create_agent()`, `get_agent()`, `as_agent()`, and managing multiple agents from a single provider. | +| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIAgentsProvider`. It automatically handles all configuration using environment variables. Shows both streaming and non-streaming responses. | +| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to find real-time information from the web using custom search configurations. Demonstrates how to use `AzureAIAgentClient.get_web_search_tool()` with custom search instances. | +| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to find real-time information from the web. Demonstrates `AzureAIAgentClient.get_web_search_tool()` with proper source citations and comprehensive error handling. | +| [`azure_ai_with_bing_grounding_citations.py`](azure_ai_with_bing_grounding_citations.py) | Demonstrates how to extract and display citations from Bing Grounding search responses. Shows how to collect citation annotations (title, URL, snippet) during streaming responses, enabling users to verify sources and access referenced content. | +| [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. | +| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use `AzureAIAgentClient.get_code_interpreter_tool()` with Azure AI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | +| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with an existing SDK Agent object using `provider.as_agent()`. This wraps the agent without making HTTP calls. | +| [`azure_ai_with_existing_session.py`](azure_ai_with_existing_session.py) | Shows how to work with a pre-existing session by providing the session ID. Demonstrates proper cleanup of manually created sessions. | +| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured provider settings, including project endpoint and model deployment name. | +| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Demonstrates how to use Azure AI Search with Azure AI agents. Shows how to create an agent with search tools using the SDK directly and wrap it with `provider.get_agent()`. | +| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Demonstrates how to use `AzureAIAgentClient.get_file_search_tool()` with Azure AI agents to search through uploaded documents. Shows file upload, vector store creation, and querying document content. | +| [`azure_ai_with_function_tools.py`](azure_ai_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | +| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to use `AzureAIAgentClient.get_mcp_tool()` with hosted Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates remote MCP server connections and tool discovery. | +| [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate Azure AI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates both agent-level and run-level tool configuration. | +| [`azure_ai_with_multiple_tools.py`](azure_ai_with_multiple_tools.py) | Demonstrates how to use multiple tools together with Azure AI agents, including web search, MCP servers, and function tools using client static methods. Shows coordinated multi-tool interactions and approval workflows. | +| [`azure_ai_with_openapi_tools.py`](azure_ai_with_openapi_tools.py) | Demonstrates how to use OpenAPI tools with Azure AI agents to integrate external REST APIs. Shows OpenAPI specification loading, anonymous authentication, session context management, and coordinated multi-API conversations. | +| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Demonstrates how to use structured outputs with Azure AI agents using Pydantic models. | +| [`azure_ai_with_session.py`](azure_ai_with_session.py) | Demonstrates session management with Azure AI agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | + +## Environment Variables + +Before running the examples, you need to set up your environment variables. You can do this in one of two ways: + +### Option 1: Using a .env file (Recommended) + +1. Copy the `.env.example` file from the `python` directory to create a `.env` file: + ```bash + cp ../../.env.example ../../.env + ``` + +2. Edit the `.env` file and add your values: + ``` + AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" + AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" + ``` + +3. For samples using Bing Grounding search (like `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`), you'll also need: + ``` + BING_CONNECTION_ID="your-bing-connection-id" + ``` + + To get your Bing connection details: + - Go to [Azure AI Foundry portal](https://ai.azure.com) + - Navigate to your project's "Connected resources" section + - Add a new connection for "Grounding with Bing Search" + - Copy the ID + +4. For samples using Bing Custom Search (like `azure_ai_with_bing_custom_search.py`), you'll also need: + ``` + BING_CUSTOM_CONNECTION_ID="your-bing-custom-connection-id" + BING_CUSTOM_INSTANCE_NAME="your-bing-custom-instance-name" + ``` + + To get your Bing Custom Search connection details: + - Go to [Azure AI Foundry portal](https://ai.azure.com) + - Navigate to your project's "Connected resources" section + - Add a new connection for "Grounding with Bing Custom Search" + - Copy the connection ID and instance name + +### Option 2: Using environment variables directly + +Set the environment variables in your shell: + +```bash +export AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" +export AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" +export BING_CONNECTION_ID="your-bing-connection-id" +export BING_CUSTOM_CONNECTION_ID="your-bing-custom-connection-id" +export BING_CUSTOM_INSTANCE_NAME="your-bing-custom-instance-name" +``` + +### Required Variables + +- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint (required for all examples) +- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (required for all examples) + +### Optional Variables + +- `BING_CONNECTION_ID`: Your Bing connection ID (required for `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`) +- `BING_CUSTOM_CONNECTION_ID`: Your Bing Custom Search connection ID (required for `azure_ai_with_bing_custom_search.py`) +- `BING_CUSTOM_INSTANCE_NAME`: Your Bing Custom Search instance name (required for `azure_ai_with_bing_custom_search.py`) diff --git a/python/samples/02-agents/providers/azure/foundry_agent_basic.py b/python/samples/02-agents/providers/azure/foundry_agent_basic.py new file mode 100644 index 0000000000..ffc57fa110 --- /dev/null +++ b/python/samples/02-agents/providers/azure/foundry_agent_basic.py @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework.azure import FoundryAgent +from azure.identity import AzureCliCredential + +""" +Foundry Agent — Connect to a pre-configured agent in Azure AI Foundry + +This sample shows the simplest way to connect to an existing PromptAgent +in Azure AI Foundry and run it. The agent's instructions, model, and hosted +tools are all configured on the service — you just connect and run. + +Environment variables: + FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint + FOUNDRY_AGENT_NAME — Name of the agent in Foundry + FOUNDRY_AGENT_VERSION — Version of the agent (for PromptAgents) +""" + + +async def main() -> None: + agent = FoundryAgent( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-prompt-agent", + agent_version="1.0", + credential=AzureCliCredential(), + ) + + result = await agent.run("What is the capital of France?") + print(f"Agent: {result}") + + # Streaming + print("Agent (streaming): ", end="", flush=True) + async for chunk in agent.run("Tell me a fun fact.", stream=True): + if chunk.text: + print(chunk.text, end="", flush=True) + print() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py b/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py new file mode 100644 index 0000000000..9b51c533b9 --- /dev/null +++ b/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py @@ -0,0 +1,62 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import Agent +from agent_framework.azure import RawFoundryAgentChatClient, RawRawFoundryAgentChatClient +from azure.identity import AzureCliCredential + +""" +Foundry Agent — Custom client configuration + +This sample demonstrates three ways to customize the FoundryAgent client layer: + +1. Default: FoundryAgent creates a RawFoundryAgentChatClient (full middleware) internally +2. client_type: Pass RawRawFoundryAgentChatClient for no client middleware +3. Composition: Use Agent(client=RawFoundryAgentChatClient(...)) directly + +Environment variables: + FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint + FOUNDRY_AGENT_NAME — Name of the agent in Foundry + FOUNDRY_AGENT_VERSION — Version of the agent +""" + + +async def main() -> None: + # Option 1: Default — full middleware on both agent and client + from agent_framework.azure import FoundryAgent + + agent = FoundryAgent( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-agent", + agent_version="1.0", + credential=AzureCliCredential(), + ) + result = await agent.run("Hello from the default setup!") + print(f"Default: {result}\n") + + # Option 2: Raw client — no client-level middleware (agent middleware still active) + agent_raw_client = FoundryAgent( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-agent", + agent_version="1.0", + credential=AzureCliCredential(), + client_type=RawRawFoundryAgentChatClient, + ) + result = await agent_raw_client.run("Hello from raw client!") + print(f"Raw client: {result}\n") + + # Option 3: Composition — use Agent(client=...) directly + client = RawFoundryAgentChatClient( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-agent", + agent_version="1.0", + credential=AzureCliCredential(), + ) + agent_composed = Agent(client=client) + result = await agent_composed.run("Hello from composed setup!") + print(f"Composed: {result}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure/foundry_agent_hosted.py b/python/samples/02-agents/providers/azure/foundry_agent_hosted.py new file mode 100644 index 0000000000..27dce9b3a6 --- /dev/null +++ b/python/samples/02-agents/providers/azure/foundry_agent_hosted.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework.azure import FoundryAgent +from azure.identity import AzureCliCredential + +""" +Foundry Agent — Connect to a HostedAgent (no version needed) + +HostedAgents in Azure AI Foundry are pre-deployed agents that don't require +a version number. You only need the agent name to connect. + +Environment variables: + FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint + FOUNDRY_AGENT_NAME — Name of the hosted agent +""" + + +async def main() -> None: + # HostedAgents don't need agent_version + agent = FoundryAgent( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-hosted-agent", + credential=AzureCliCredential(), + ) + + result = await agent.run("Summarize the latest news about AI.") + print(f"Agent: {result}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure/foundry_agent_with_env_vars.py b/python/samples/02-agents/providers/azure/foundry_agent_with_env_vars.py new file mode 100644 index 0000000000..cef005735f --- /dev/null +++ b/python/samples/02-agents/providers/azure/foundry_agent_with_env_vars.py @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os + +from agent_framework.azure import FoundryAgent +from azure.identity import AzureCliCredential + +""" +Foundry Agent with Environment Variables + +This sample shows the recommended pattern for advanced samples that use +environment variables for configuration. + +Environment variables: + FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint + FOUNDRY_AGENT_NAME — Name of the agent in Foundry + FOUNDRY_AGENT_VERSION — Version of the agent (optional, for PromptAgents) +""" + + +async def main() -> None: + agent = FoundryAgent( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + agent_name=os.environ["FOUNDRY_AGENT_NAME"], + agent_version=os.environ.get("FOUNDRY_AGENT_VERSION"), + credential=AzureCliCredential(), + ) + + session = agent.create_session() + + result = await agent.run("Hello! My name is Alice.", session=session) + print(f"Agent: {result}\n") + + result = await agent.run("What's my name?", session=session) + print(f"Agent: {result}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure/foundry_agent_with_function_tools.py b/python/samples/02-agents/providers/azure/foundry_agent_with_function_tools.py new file mode 100644 index 0000000000..324bf7a3f6 --- /dev/null +++ b/python/samples/02-agents/providers/azure/foundry_agent_with_function_tools.py @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from typing import Annotated + +from agent_framework import tool +from agent_framework.azure import FoundryAgent +from azure.identity import AzureCliCredential +from pydantic import Field + +""" +Foundry Agent with Local Function Tools + +This sample shows how to connect to a Foundry agent and provide local function +tools. The Foundry agent must already have these tools defined in its configuration +(as declaration-only tools). The local implementations are matched by name. + +Only FunctionTool objects are accepted — hosted tools (code interpreter, file search, +web search, etc.) must be configured on the agent definition in the service. + +Environment variables: + FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint + FOUNDRY_AGENT_NAME — Name of the agent in Foundry + FOUNDRY_AGENT_VERSION — Version of the agent +""" + + +@tool(approval_mode="never_require") +def get_weather( + location: Annotated[str, Field(description="The city to get weather for.")], +) -> str: + """Get the current weather for a location.""" + return f"The weather in {location} is sunny, 22°C." + + +async def main() -> None: + agent = FoundryAgent( + project_endpoint="https://your-project.services.ai.azure.com", + agent_name="my-weather-agent", + agent_version="1.0", + credential=AzureCliCredential(), + tools=[get_weather], + ) + + result = await agent.run("What's the weather in Paris?") + print(f"Agent: {result}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_foundry.py b/python/samples/02-agents/providers/azure/foundry_chat_client.py similarity index 78% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_with_foundry.py rename to python/samples/02-agents/providers/azure/foundry_chat_client.py index f7d78683f1..dc307e825c 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_foundry.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client.py @@ -5,8 +5,8 @@ from random import randint from typing import Annotated -from agent_framework import tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -17,15 +17,15 @@ """ Azure OpenAI Responses Client with Foundry Project Example -This sample demonstrates how to create an AzureOpenAIResponsesClient using an +This sample demonstrates how to create an FoundryChatClient using an Azure AI Foundry project endpoint. Instead of providing an Azure OpenAI endpoint directly, you provide a Foundry project endpoint and the client is created via the Azure AI Foundry project SDK. This requires: - The `azure-ai-projects` package to be installed. -- The `AZURE_AI_PROJECT_ENDPOINT` environment variable set to your Foundry project endpoint. -- The `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME` environment variable set to the model deployment name. +- The `FOUNDRY_PROJECT_ENDPOINT` environment variable set to your Foundry project endpoint. +- The `FOUNDRY_MODEL` environment variable set to the model deployment name. """ # Load environment variables from .env file if present @@ -43,15 +43,16 @@ async def non_streaming_example() -> None: """Example of non-streaming response (get the complete result at once).""" print("=== Non-streaming Response Example ===") - # 1. Create the AzureOpenAIResponsesClient using a Foundry project endpoint. + # 1. Create the FoundryChatClient using a Foundry project endpoint. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. credential = AzureCliCredential() - agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + _client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], credential=credential, - ).as_agent( + ) + agent = Agent(client=_client, instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -67,15 +68,16 @@ async def streaming_example() -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") - # 1. Create the AzureOpenAIResponsesClient using a Foundry project endpoint. + # 1. Create the FoundryChatClient using a Foundry project endpoint. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. credential = AzureCliCredential() - agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + _client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], credential=credential, - ).as_agent( + ) + agent = Agent(client=_client, instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_basic.py b/python/samples/02-agents/providers/azure/foundry_chat_client_basic.py similarity index 83% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_basic.py rename to python/samples/02-agents/providers/azure/foundry_chat_client_basic.py index 73820443f3..c86e3bc27f 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_basic.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_basic.py @@ -4,8 +4,8 @@ from random import randint from typing import Annotated -from agent_framework import tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -14,9 +14,9 @@ load_dotenv() """ -Azure OpenAI Responses Client Basic Example +Azure OpenAI Chat Client Basic Example -This sample demonstrates basic usage of AzureOpenAIResponsesClient for structured +This sample demonstrates basic usage of OpenAIChatClient for structured response generation, showing both streaming and non-streaming responses. """ @@ -39,7 +39,8 @@ async def non_streaming_example() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = AzureOpenAIResponsesClient(credential=AzureCliCredential()).as_agent( + agent = Agent( + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -56,7 +57,8 @@ async def streaming_example() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = AzureOpenAIResponsesClient(credential=AzureCliCredential()).as_agent( + agent = Agent( + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -71,7 +73,7 @@ async def streaming_example() -> None: async def main() -> None: - print("=== Basic Azure OpenAI Responses Client Agent Example ===") + print("=== Basic Azure OpenAI Chat Client Agent Example ===") await non_streaming_example() await streaming_example() diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_code_interpreter_files.py b/python/samples/02-agents/providers/azure/foundry_chat_client_code_interpreter_files.py similarity index 96% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_code_interpreter_files.py rename to python/samples/02-agents/providers/azure/foundry_chat_client_code_interpreter_files.py index eb81f941b8..a8cb8d5a1c 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_code_interpreter_files.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_code_interpreter_files.py @@ -5,7 +5,7 @@ import tempfile from agent_framework import Agent -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from openai import AsyncAzureOpenAI @@ -80,7 +80,7 @@ async def get_token(): temp_file_path, file_id = await create_sample_file_and_upload(openai_client) # Create agent using Azure OpenAI Responses client - client = AzureOpenAIResponsesClient(credential=credential) + client = FoundryChatClient(credential=credential) # Create code interpreter tool with file access code_interpreter_tool = client.get_code_interpreter_tool(file_ids=[file_id]) diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_image_analysis.py b/python/samples/02-agents/providers/azure/foundry_chat_client_image_analysis.py similarity index 81% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_image_analysis.py rename to python/samples/02-agents/providers/azure/foundry_chat_client_image_analysis.py index 5066c6c832..1876aa4aaf 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_image_analysis.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_image_analysis.py @@ -2,8 +2,8 @@ import asyncio -from agent_framework import Content -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, Content +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -11,7 +11,7 @@ load_dotenv() """ -Azure OpenAI Responses Client with Image Analysis Example +Azure OpenAI Chat Client with Image Analysis Example This sample demonstrates using Azure OpenAI Responses for image analysis and vision tasks, showing multi-modal messages combining text and image content. @@ -22,7 +22,8 @@ async def main(): print("=== Azure Responses Agent with Image Analysis ===") # 1. Create an Azure Responses agent with vision capabilities - agent = AzureOpenAIResponsesClient(credential=AzureCliCredential()).as_agent( + agent = Agent( + client=FoundryChatClient(credential=AzureCliCredential()), name="VisionAgent", instructions="You are a image analysist, you get a image and need to respond with what you see in the picture.", ) diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_code_interpreter.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_code_interpreter.py similarity index 93% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_with_code_interpreter.py rename to python/samples/02-agents/providers/azure/foundry_chat_client_with_code_interpreter.py index 8862f81741..a3d32c451a 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_code_interpreter.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_code_interpreter.py @@ -3,7 +3,7 @@ import asyncio from agent_framework import Agent, ChatResponse -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from openai.types.responses.response import Response as OpenAIResponse @@ -26,7 +26,7 @@ async def main() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) + client = FoundryChatClient(credential=AzureCliCredential()) # Create code interpreter tool using instance method code_interpreter_tool = client.get_code_interpreter_tool() diff --git a/python/samples/02-agents/providers/azure_openai/azure_chat_client_with_explicit_settings.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_explicit_settings.py similarity index 87% rename from python/samples/02-agents/providers/azure_openai/azure_chat_client_with_explicit_settings.py rename to python/samples/02-agents/providers/azure/foundry_chat_client_with_explicit_settings.py index ea00a80d6d..e4a11e546b 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_chat_client_with_explicit_settings.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_explicit_settings.py @@ -5,8 +5,8 @@ from random import randint from typing import Annotated -from agent_framework import tool -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -39,13 +39,15 @@ async def main() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = AzureOpenAIChatClient( - deployment_name=os.environ["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], + _client = FoundryChatClient( + model=os.environ["FOUNDRY_MODEL"], endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], credential=AzureCliCredential(), - ).as_agent( + ) + agent = Agent( + client=_client, instructions="You are a helpful weather agent.", - tools=get_weather, + tools=[get_weather], ) result = await agent.run("What's the weather like in New York?") diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_file_search.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_file_search.py similarity index 85% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_with_file_search.py rename to python/samples/02-agents/providers/azure/foundry_chat_client_with_file_search.py index f3fc3c852c..08781ffa44 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_file_search.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_file_search.py @@ -4,7 +4,7 @@ import contextlib from agent_framework import Agent -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -20,14 +20,14 @@ Prerequisites: - Set environment variables: - AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint URL - - AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: Your Responses API deployment name + - FOUNDRY_MODEL: Your Responses API deployment name - Authenticate via 'az login' for AzureCliCredential """ # Helper functions -async def create_vector_store(client: AzureOpenAIResponsesClient) -> tuple[str, str]: +async def create_vector_store(client: FoundryChatClient) -> tuple[str, str]: """Create a vector store with sample documents.""" file = await client.client.files.create( file=("todays_weather.txt", b"The weather today is sunny with a high of 75F."), purpose="assistants" @@ -43,7 +43,7 @@ async def create_vector_store(client: AzureOpenAIResponsesClient) -> tuple[str, return file.id, vector_store.id -async def delete_vector_store(client: AzureOpenAIResponsesClient, file_id: str, vector_store_id: str) -> None: +async def delete_vector_store(client: FoundryChatClient, file_id: str, vector_store_id: str) -> None: """Delete the vector store after using it.""" with contextlib.suppress(Exception): await client.client.vector_stores.delete(vector_store_id=vector_store_id) @@ -56,7 +56,7 @@ async def main() -> None: # Initialize Responses client # Make sure you're logged in via 'az login' before running this sample - client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) + client = FoundryChatClient(credential=AzureCliCredential()) file_id, vector_store_id = await create_vector_store(client) diff --git a/python/samples/02-agents/providers/azure_openai/azure_chat_client_with_function_tools.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_function_tools.py similarity index 94% rename from python/samples/02-agents/providers/azure_openai/azure_chat_client_with_function_tools.py rename to python/samples/02-agents/providers/azure/foundry_chat_client_with_function_tools.py index 7458aea2e6..514e3b4ebe 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_chat_client_with_function_tools.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_function_tools.py @@ -6,7 +6,7 @@ from typing import Annotated from agent_framework import Agent, tool -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -50,7 +50,7 @@ async def tools_on_agent_level() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) @@ -82,7 +82,7 @@ async def tools_on_run_level() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", # No tools defined here ) @@ -114,7 +114,7 @@ async def mixed_tools_example() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_hosted_mcp.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_hosted_mcp.py similarity index 97% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_with_hosted_mcp.py rename to python/samples/02-agents/providers/azure/foundry_chat_client_with_hosted_mcp.py index f81bfc83c7..b2a308e3db 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_hosted_mcp.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_hosted_mcp.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any from agent_framework import Agent -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -103,7 +103,7 @@ async def run_hosted_mcp_without_session_and_specific_approval() -> None: """Example showing Mcp Tools with approvals without using a session.""" print("=== Mcp with approvals and without session ===") credential = AzureCliCredential() - client = AzureOpenAIResponsesClient(credential=credential) + client = FoundryChatClient(credential=credential) # Create MCP tool with specific approval settings mcp_tool = client.get_mcp_tool( @@ -140,7 +140,7 @@ async def run_hosted_mcp_without_approval() -> None: """Example showing Mcp Tools without approvals.""" print("=== Mcp without approvals ===") credential = AzureCliCredential() - client = AzureOpenAIResponsesClient(credential=credential) + client = FoundryChatClient(credential=credential) # Create MCP tool without approval requirements mcp_tool = client.get_mcp_tool( @@ -178,7 +178,7 @@ async def run_hosted_mcp_with_session() -> None: """Example showing Mcp Tools with approvals using a session.""" print("=== Mcp with approvals and with session ===") credential = AzureCliCredential() - client = AzureOpenAIResponsesClient(credential=credential) + client = FoundryChatClient(credential=credential) # Create MCP tool with always require approval mcp_tool = client.get_mcp_tool( @@ -215,7 +215,7 @@ async def run_hosted_mcp_with_session_streaming() -> None: """Example showing Mcp Tools with approvals using a session.""" print("=== Mcp with approvals and with session ===") credential = AzureCliCredential() - client = AzureOpenAIResponsesClient(credential=credential) + client = FoundryChatClient(credential=credential) # Create MCP tool with always require approval mcp_tool = client.get_mcp_tool( diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_local_mcp.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py similarity index 90% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_with_local_mcp.py rename to python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py index e470d9bfe9..34ab6f19b2 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_local_mcp.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py @@ -4,7 +4,7 @@ import os from agent_framework import Agent, MCPStreamableHTTPTool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -26,7 +26,7 @@ # Environment variables for Azure OpenAI Responses authentication # AZURE_OPENAI_ENDPOINT="" -# AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME="" +# FOUNDRY_MODEL="" # AZURE_OPENAI_API_VERSION="" # e.g. "2025-03-01-preview" @@ -37,11 +37,11 @@ async def main(): # Build an agent backed by Azure OpenAI Responses # (endpoint/deployment/api_version can also come from env vars above) - responses_client = AzureOpenAIResponsesClient( + responses_client = FoundryChatClient( credential=credential, ) - agent: Agent = responses_client.as_agent( + agent: Agent = Agent(client=responses_client, name="DocsAgent", instructions=("You are a helpful assistant that can help with Microsoft documentation questions."), ) diff --git a/python/samples/02-agents/providers/azure_openai/azure_chat_client_with_session.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_session.py similarity index 94% rename from python/samples/02-agents/providers/azure_openai/azure_chat_client_with_session.py rename to python/samples/02-agents/providers/azure/foundry_chat_client_with_session.py index 0592aba2c7..e8d926fcfd 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_chat_client_with_session.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_session.py @@ -5,7 +5,7 @@ from typing import Annotated from agent_framework import Agent, AgentSession, InMemoryHistoryProvider, tool -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -40,7 +40,7 @@ async def example_with_automatic_session_creation() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -67,7 +67,7 @@ async def example_with_session_persistence() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -102,7 +102,7 @@ async def example_with_existing_session_messages() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -125,7 +125,7 @@ async def example_with_existing_session_messages() -> None: # Create a new agent instance but use the existing session with its message history new_agent = Agent( - client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/02-agents/providers/azure_openai/azure_chat_client_basic.py b/python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_basic.py similarity index 86% rename from python/samples/02-agents/providers/azure_openai/azure_chat_client_basic.py rename to python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_basic.py index 6d9a4d8e01..db5740cfc3 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_chat_client_basic.py +++ b/python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_basic.py @@ -4,8 +4,8 @@ from random import randint from typing import Annotated -from agent_framework import tool -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework import Agent, tool +from agent_framework.openai import OpenAIChatCompletionClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -16,7 +16,7 @@ """ Azure OpenAI Chat Client Basic Example -This sample demonstrates basic usage of AzureOpenAIChatClient for direct chat-based +This sample demonstrates basic usage of OpenAIChatCompletionClient for direct chat-based interactions, showing both streaming and non-streaming responses. """ @@ -40,7 +40,8 @@ async def non_streaming_example() -> None: # Create agent with Azure Chat Client # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + agent = Agent( + client=OpenAIChatCompletionClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -58,7 +59,8 @@ async def streaming_example() -> None: # Create agent with Azure Chat Client # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + agent = Agent( + client=OpenAIChatCompletionClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_explicit_settings.py b/python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_with_explicit_settings.py similarity index 75% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_with_explicit_settings.py rename to python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_with_explicit_settings.py index f31efaa611..e4a11e546b 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_explicit_settings.py +++ b/python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_with_explicit_settings.py @@ -5,8 +5,8 @@ from random import randint from typing import Annotated -from agent_framework import tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -15,9 +15,9 @@ load_dotenv() """ -Azure OpenAI Responses Client with Explicit Settings Example +Azure OpenAI Chat Client with Explicit Settings Example -This sample demonstrates creating Azure OpenAI Responses Client with explicit configuration +This sample demonstrates creating Azure OpenAI Chat Client with explicit configuration settings rather than relying on environment variable defaults. """ @@ -35,17 +35,19 @@ def get_weather( async def main() -> None: - print("=== Azure Responses Client with Explicit Settings ===") + print("=== Azure Chat Client with Explicit Settings ===") # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = AzureOpenAIResponsesClient( - deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + _client = FoundryChatClient( + model=os.environ["FOUNDRY_MODEL"], endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], credential=AzureCliCredential(), - ).as_agent( + ) + agent = Agent( + client=_client, instructions="You are a helpful weather agent.", - tools=get_weather, + tools=[get_weather], ) result = await agent.run("What's the weather like in New York?") diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_function_tools.py b/python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_with_function_tools.py similarity index 91% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_with_function_tools.py rename to python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_with_function_tools.py index 1ebda9ce37..514e3b4ebe 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_function_tools.py +++ b/python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_with_function_tools.py @@ -6,7 +6,7 @@ from typing import Annotated from agent_framework import Agent, tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -15,9 +15,9 @@ load_dotenv() """ -Azure OpenAI Responses Client with Function Tools Example +Azure OpenAI Chat Client with Function Tools Example -This sample demonstrates function tool integration with Azure OpenAI Responses Client, +This sample demonstrates function tool integration with Azure OpenAI Chat Client, showing both agent-level and query-level tool configuration patterns. """ @@ -50,7 +50,7 @@ async def tools_on_agent_level() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) @@ -82,7 +82,7 @@ async def tools_on_run_level() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", # No tools defined here ) @@ -114,7 +114,7 @@ async def mixed_tools_example() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) @@ -132,7 +132,7 @@ async def mixed_tools_example() -> None: async def main() -> None: - print("=== Azure OpenAI Responses Client Agent with Function Tools Examples ===\n") + print("=== Azure Chat Client Agent with Function Tools Examples ===\n") await tools_on_agent_level() await tools_on_run_level() diff --git a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_session.py b/python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_with_session.py similarity index 56% rename from python/samples/02-agents/providers/azure_openai/azure_responses_client_with_session.py rename to python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_with_session.py index b7ed1d4011..e8d926fcfd 100644 --- a/python/samples/02-agents/providers/azure_openai/azure_responses_client_with_session.py +++ b/python/samples/02-agents/providers/azure/openai_chat_completion_client_azure_with_session.py @@ -4,8 +4,8 @@ from random import randint from typing import Annotated -from agent_framework import Agent, AgentSession, tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, AgentSession, InMemoryHistoryProvider, tool +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -14,9 +14,9 @@ load_dotenv() """ -Azure OpenAI Responses Client with Session Management Example +Azure OpenAI Chat Client with Session Management Example -This sample demonstrates session management with Azure OpenAI Responses Client, comparing +This sample demonstrates session management with Azure OpenAI Chat Client, comparing automatic session creation with explicit session management for persistent context. """ @@ -34,13 +34,13 @@ def get_weather( async def example_with_automatic_session_creation() -> None: - """Example showing automatic session creation.""" + """Example showing automatic session creation (service-managed session).""" print("=== Automatic Session Creation Example ===") # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -59,17 +59,15 @@ async def example_with_automatic_session_creation() -> None: print("Note: Each call creates a separate session, so the agent doesn't remember previous context.\n") -async def example_with_session_persistence_in_memory() -> None: - """ - Example showing session persistence across multiple conversations. - In this example, messages are stored in-memory. - """ - print("=== Session Persistence Example (In-Memory) ===") +async def example_with_session_persistence() -> None: + """Example showing session persistence across multiple conversations.""" + print("=== Session Persistence Example ===") + print("Using the same session across multiple conversations to maintain context.\n") # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -97,62 +95,66 @@ async def example_with_session_persistence_in_memory() -> None: print("Note: The agent remembers context from previous messages in the same session.\n") -async def example_with_existing_session_id() -> None: - """ - Example showing how to work with an existing session ID from the service. - In this example, messages are stored on the server using Azure OpenAI conversation state. - """ - print("=== Existing Session ID Example ===") - - # First, create a conversation and capture the session ID - existing_session_id = None +async def example_with_existing_session_messages() -> None: + """Example showing how to work with existing session messages for Azure.""" + print("=== Existing Session Messages Example ===") # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. agent = Agent( - client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) - # Start a conversation and get the session ID + # Start a conversation and build up message history session = agent.create_session() query1 = "What's the weather in Paris?" print(f"User: {query1}") - # Enable Azure OpenAI conversation state by setting `store` parameter to True - result1 = await agent.run(query1, session=session, store=True) + result1 = await agent.run(query1, session=session) print(f"Agent: {result1.text}") - # The session ID is set after the first response - existing_session_id = session.service_session_id - print(f"Session ID: {existing_session_id}") + # The session now contains the conversation history in state + memory_state = session.state.get(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {}) + messages = memory_state.get("messages", []) + if messages: + print(f"Session contains {len(messages)} messages") + + print("\n--- Continuing with the same session in a new agent instance ---") + + # Create a new agent instance but use the existing session with its message history + new_agent = Agent( + client=FoundryChatClient(credential=AzureCliCredential()), + instructions="You are a helpful weather agent.", + tools=get_weather, + ) - if existing_session_id: - print("\n--- Continuing with the same session ID in a new agent instance ---") + # Use the same session object which contains the conversation history + query2 = "What was the last city I asked about?" + print(f"User: {query2}") + result2 = await new_agent.run(query2, session=session) + print(f"Agent: {result2.text}") + print("Note: The agent continues the conversation using the local message history.\n") - agent = Agent( - client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), - instructions="You are a helpful weather agent.", - tools=get_weather, - ) + print("\n--- Alternative: Creating a new session from existing messages ---") - # Create a session with the existing ID - session = AgentSession(service_session_id=existing_session_id) + # You can also create a new session from existing messages + new_session = AgentSession() - query2 = "What was the last city I asked about?" - print(f"User: {query2}") - result2 = await agent.run(query2, session=session, store=True) - print(f"Agent: {result2.text}") - print("Note: The agent continues the conversation from the previous session by using session ID.\n") + query3 = "How does the Paris weather compare to London?" + print(f"User: {query3}") + result3 = await new_agent.run(query3, session=new_session) + print(f"Agent: {result3.text}") + print("Note: This creates a new session with the same conversation history.\n") async def main() -> None: - print("=== Azure OpenAI Response Client Agent Session Management Examples ===\n") + print("=== Azure Chat Client Agent Session Management Examples ===\n") await example_with_automatic_session_creation() - await example_with_session_persistence_in_memory() - await example_with_existing_session_id() + await example_with_session_persistence() + await example_with_existing_session_messages() if __name__ == "__main__": diff --git a/python/samples/02-agents/providers/azure_ai/README.md b/python/samples/02-agents/providers/azure_ai/README.md deleted file mode 100644 index 3a73350f24..0000000000 --- a/python/samples/02-agents/providers/azure_ai/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# Azure AI Agent Examples - -This folder contains examples demonstrating different ways to create and use agents with the Azure AI client from the `agent_framework.azure` package. These examples use the `AzureAIClient` with the `azure-ai-projects` 2.x (V2) API surface (see [changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-projects/CHANGELOG.md#200b1-2025-11-11)). For V1 (`azure-ai-agents` 1.x) samples using `AzureAIAgentClient`, see the [Azure AI V1 examples folder](../azure_ai_agent/). When using preview-only agent creation features on GA SDK versions, create `AIProjectClient` with `allow_preview=True`. - -## Examples - -| File | Description | -|------|-------------| -| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIProjectAgentProvider`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. | -| [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive guide to `AzureAIProjectAgentProvider` methods: `create_agent()` for creating new agents, `get_agent()` for retrieving existing agents (by name, reference, or details), and `as_agent()` for wrapping SDK objects without HTTP calls. | -| [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation by using `provider.get_agent()` to retrieve the latest version. | -| [`azure_ai_with_agent_as_tool.py`](azure_ai_with_agent_as_tool.py) | Shows how to use the agent-as-tool pattern with Azure AI agents, where one agent delegates work to specialized sub-agents wrapped as tools using `as_tool()`. Demonstrates hierarchical agent architectures. | -| [`azure_ai_with_agent_to_agent.py`](azure_ai_with_agent_to_agent.py) | Shows how to use Agent-to-Agent (A2A) capabilities with Azure AI agents to enable communication with other agents using the A2A protocol. Requires an A2A connection configured in your Azure AI project. | -| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Shows how to use Azure AI Search with Azure AI agents to search through indexed data and answer user questions with proper citations. Requires an Azure AI Search connection and index configured in your Azure AI project. | -| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to search the web for current information and provide grounded responses with citations. Requires a Bing connection configured in your Azure AI project. | -| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to search custom search instances and provide responses with relevant results. Requires a Bing Custom Search connection and instance configured in your Azure AI project. | -| [`azure_ai_with_browser_automation.py`](azure_ai_with_browser_automation.py) | Shows how to use Browser Automation with Azure AI agents to perform automated web browsing tasks and provide responses based on web interactions. Requires a Browser Automation connection configured in your Azure AI project. | -| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use `AzureAIClient.get_code_interpreter_tool()` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. | -| [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. | -| [`azure_ai_with_code_interpreter_file_download.py`](azure_ai_with_code_interpreter_file_download.py) | Shows how to download files generated by code interpreter using the OpenAI containers API. | -| [`azure_ai_with_content_filtering.py`](azure_ai_with_content_filtering.py) | Shows how to enable content filtering (RAI policy) on Azure AI agents using `RaiConfig`. Requires creating an RAI policy in Azure AI Foundry portal first. | -| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent name and version to the Azure AI client. Demonstrates agent reuse patterns for production scenarios. | -| [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Demonstrates how to use an existing conversation created on the service side with Azure AI agents. Shows two approaches: specifying conversation ID at the client level and using AgentSession with an existing conversation ID. | -| [`azure_ai_with_application_endpoint.py`](azure_ai_with_application_endpoint.py) | Demonstrates calling the Azure AI application-scoped endpoint. | -| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIClient` settings, including project endpoint, model deployment, and credentials rather than relying on environment variable defaults. | -| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use `AzureAIClient.get_file_search_tool()` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. | -| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent using `AzureAIClient.get_mcp_tool()`. | -| [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate local Model Context Protocol (MCP) tools with Azure AI agents. | -| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. | -| [`azure_ai_with_runtime_json_schema.py`](azure_ai_with_runtime_json_schema.py) | Shows how to use structured outputs (response format) with Azure AI agents using a JSON schema to enforce specific response schemas. | -| [`azure_ai_with_search_context_agentic.py`](../../context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py) | Shows how to use AzureAISearchContextProvider with agentic mode. Uses Knowledge Bases for multi-hop reasoning across documents with query planning. Recommended for most scenarios - slightly slower with more token consumption for query planning, but more accurate results. | -| [`azure_ai_with_search_context_semantic.py`](../../context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py) | Shows how to use AzureAISearchContextProvider with semantic mode. Fast hybrid search with vector + keyword search and semantic ranking for RAG. Best for simple queries where speed is critical. | -| [`azure_ai_with_sharepoint.py`](azure_ai_with_sharepoint.py) | Shows how to use SharePoint grounding with Azure AI agents to search through SharePoint content and answer user questions with proper citations. Requires a SharePoint connection configured in your Azure AI project. | -| [`azure_ai_with_session.py`](azure_ai_with_session.py) | Demonstrates session management with Azure AI agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | -| [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use `AzureAIClient.get_image_generation_tool()` with Azure AI agents to generate images based on text prompts. | -| [`azure_ai_with_memory_search.py`](azure_ai_with_memory_search.py) | Shows how to use memory search functionality with Azure AI agents for conversation persistence. Demonstrates creating memory stores and enabling agents to search through conversation history. | -| [`azure_ai_with_microsoft_fabric.py`](azure_ai_with_microsoft_fabric.py) | Shows how to use Microsoft Fabric with Azure AI agents to query Fabric data sources and provide responses based on data analysis. Requires a Microsoft Fabric connection configured in your Azure AI project. | -| [`azure_ai_with_openapi.py`](azure_ai_with_openapi.py) | Shows how to integrate OpenAPI specifications with Azure AI agents using dictionary-based tool configuration. Demonstrates using external REST APIs for dynamic data lookup. | -| [`azure_ai_with_reasoning.py`](azure_ai_with_reasoning.py) | Shows how to enable reasoning for a model that supports it. | -| [`azure_ai_with_web_search.py`](azure_ai_with_web_search.py) | Shows how to use `AzureAIClient.get_web_search_tool()` with Azure AI agents to perform web searches and retrieve up-to-date information from the internet. | - -## Environment Variables - -Before running the examples, you need to set up your environment variables. You can do this in one of two ways: - -### Option 1: Using a .env file (Recommended) - -1. Copy the `.env.example` file from the `python` directory to create a `.env` file: - - ```bash - cp ../../../../.env.example ../../../../.env - ``` - -2. Edit the `.env` file and add your values: - - ```env - AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" - AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" - ``` - -### Option 2: Using environment variables directly - -Set the environment variables in your shell: - -```bash -export AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" -export AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" -``` - -### Required Variables - -- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint (required for all examples) -- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (required for all examples) - -## Authentication - -All examples use `AzureCliCredential` for authentication by default. Before running the examples: - -1. Install the Azure CLI -2. Run `az login` to authenticate with your Azure account -3. Ensure you have appropriate permissions to the Azure AI project - -Alternatively, you can replace `AzureCliCredential` with other authentication options like `DefaultAzureCredential` or environment-based credentials. - -## Running the Examples - -Each example can be run independently. Navigate to this directory and run any example: - -```bash -python azure_ai_basic.py -python azure_ai_with_code_interpreter.py -# ... etc -``` - -The examples demonstrate various patterns for working with Azure AI agents, from basic usage to advanced scenarios like session management and structured outputs. diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_basic.py b/python/samples/02-agents/providers/azure_ai/azure_ai_basic.py deleted file mode 100644 index 2f4df77f17..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_basic.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent Basic Example - -This sample demonstrates basic usage of AzureAIProjectAgentProvider. -Shows both streaming and non-streaming responses with function tools. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def non_streaming_example() -> None: - """Example of non-streaming response (get the complete result at once).""" - print("=== Non-streaming Response Example ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="BasicWeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - query = "What's the weather like in Seattle?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - -async def streaming_example() -> None: - """Example of streaming response (get results as they are generated).""" - print("=== Streaming Response Example ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="BasicWeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - query = "What's the weather like in Tokyo?" - print(f"User: {query}") - print("Agent: ", end="", flush=True) - async for chunk in agent.run(query, stream=True): - if chunk.text: - print(chunk.text, end="", flush=True) - print("\n") - - -async def main() -> None: - print("=== Basic Azure AI Chat Client Agent Example ===") - - await non_streaming_example() - await streaming_example() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_provider_methods.py b/python/samples/02-agents/providers/azure_ai/azure_ai_provider_methods.py deleted file mode 100644 index 9efa5592c7..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_provider_methods.py +++ /dev/null @@ -1,258 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.ai.projects.aio import AIProjectClient -from azure.ai.projects.models import PromptAgentDefinition -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Project Agent Provider Methods Example - -This sample demonstrates the three main methods of AzureAIProjectAgentProvider: -1. create_agent() - Create a new agent on the Azure AI service -2. get_agent() - Retrieve an existing agent from the service -3. as_agent() - Wrap an SDK agent version object without making HTTP calls - -It also shows how to use a single provider instance to spawn multiple agents -with different configurations, which is efficient for multi-agent scenarios. - -Each method returns a Agent that can be used for conversations. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}C." - - -async def create_agent_example() -> None: - """Example of using provider.create_agent() to create a new agent. - - This method creates a new agent version on the Azure AI service and returns - a Agent. Use this when you want to create a fresh agent with - specific configuration. - """ - print("=== provider.create_agent() Example ===") - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a new agent with custom configuration - agent = await provider.create_agent( - name="WeatherAssistant", - instructions="You are a helpful weather assistant. Always be concise.", - description="An agent that provides weather information.", - tools=get_weather, - ) - - print(f"Created agent: {agent.name}") - print(f"Agent ID: {agent.id}") - - query = "What's the weather in Paris?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - -async def get_agent_by_name_example() -> None: - """Example of using provider.get_agent(name=...) to retrieve an agent by name. - - This method fetches the latest version of an existing agent from the service. - Use this when you know the agent name and want to use the most recent version. - """ - print("=== provider.get_agent(name=...) Example ===") - - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - ): - # First, create an agent using the SDK directly - created_agent = await project_client.agents.create_version( - agent_name="TestAgentByName", - description="Test agent for get_agent by name example.", - definition=PromptAgentDefinition( - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - instructions="You are a helpful assistant. End each response with '- Your Assistant'.", - ), - ) - - try: - # Get the agent using the provider by name (fetches latest version) - provider = AzureAIProjectAgentProvider(project_client=project_client) - agent = await provider.get_agent(name=created_agent.name) - - print(f"Retrieved agent: {agent.name}") - - query = "Hello!" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - finally: - # Clean up the agent - await project_client.agents.delete_version( - agent_name=created_agent.name, agent_version=created_agent.version - ) - - -async def get_agent_by_reference_example() -> None: - """Example of using provider.get_agent(reference=...) to retrieve a specific agent version. - - This method fetches a specific version of an agent using a reference mapping. - Use this when you need to use a particular version of an agent. - """ - print("=== provider.get_agent(reference=...) Example ===") - - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - ): - # First, create an agent using the SDK directly - created_agent = await project_client.agents.create_version( - agent_name="TestAgentByReference", - description="Test agent for get_agent by reference example.", - definition=PromptAgentDefinition( - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - instructions="You are a helpful assistant. Always respond in uppercase.", - ), - ) - - try: - # Get the agent using a reference mapping with specific version - provider = AzureAIProjectAgentProvider(project_client=project_client) - reference = {"name": created_agent.name, "version": created_agent.version} - agent = await provider.get_agent(reference=reference) - - print(f"Retrieved agent: {agent.name} (version via reference)") - - query = "Say hello" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - finally: - # Clean up the agent - await project_client.agents.delete_version( - agent_name=created_agent.name, agent_version=created_agent.version - ) - - -async def multiple_agents_example() -> None: - """Example of using a single provider to spawn multiple agents. - - A single provider instance can create multiple agents with different - configurations. - """ - print("=== Multiple Agents from Single Provider Example ===") - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create multiple specialized agents from the same provider - weather_agent = await provider.create_agent( - name="WeatherExpert", - instructions="You are a weather expert. Provide brief weather information.", - tools=get_weather, - ) - - translator_agent = await provider.create_agent( - name="Translator", - instructions="You are a translator. Translate any text to French. Only output the translation.", - ) - - poet_agent = await provider.create_agent( - name="Poet", - instructions="You are a poet. Respond to everything with a short haiku.", - ) - - print(f"Created agents: {weather_agent.name}, {translator_agent.name}, {poet_agent.name}\n") - - # Use each agent for its specialty - weather_query = "What's the weather in London?" - print(f"User to WeatherExpert: {weather_query}") - weather_result = await weather_agent.run(weather_query) - print(f"WeatherExpert: {weather_result}\n") - - translate_query = "Hello, how are you today?" - print(f"User to Translator: {translate_query}") - translate_result = await translator_agent.run(translate_query) - print(f"Translator: {translate_result}\n") - - poet_query = "Tell me about the morning sun" - print(f"User to Poet: {poet_query}") - poet_result = await poet_agent.run(poet_query) - print(f"Poet: {poet_result}\n") - - -async def as_agent_example() -> None: - """Example of using provider.as_agent() to wrap an SDK object without HTTP calls. - - This method wraps an existing AgentVersionDetails into a Agent without - making additional HTTP calls. Use this when you already have the full - AgentVersionDetails from a previous SDK operation. - """ - print("=== provider.as_agent() Example ===") - - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - ): - # Create an agent using the SDK directly - this returns AgentVersionDetails - agent_version_details = await project_client.agents.create_version( - agent_name="TestAgentAsAgent", - description="Test agent for as_agent example.", - definition=PromptAgentDefinition( - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - instructions="You are a helpful assistant. Keep responses under 20 words.", - ), - ) - - try: - # Wrap the SDK object directly without any HTTP calls - provider = AzureAIProjectAgentProvider(project_client=project_client) - agent = provider.as_agent(agent_version_details) - - print(f"Wrapped agent: {agent.name} (no HTTP call needed)") - print(f"Agent version: {agent_version_details.version}") - - query = "What can you do?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - finally: - # Clean up the agent - await project_client.agents.delete_version( - agent_name=agent_version_details.name, agent_version=agent_version_details.version - ) - - -async def main() -> None: - print("=== Azure AI Project Agent Provider Methods Example ===\n") - - await create_agent_example() - await get_agent_by_name_example() - await get_agent_by_reference_example() - await as_agent_example() - await multiple_agents_example() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_use_latest_version.py b/python/samples/02-agents/providers/azure_ai/azure_ai_use_latest_version.py deleted file mode 100644 index 2647742d38..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_use_latest_version.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent Latest Version Example - -This sample demonstrates how to reuse the latest version of an existing agent -instead of creating a new agent version on each instantiation. The first call creates a new agent, -while subsequent calls with `get_agent()` reuse the latest agent version. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def main() -> None: - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # First call creates a new agent - agent = await provider.create_agent( - name="MyWeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - query = "What's the weather like in Seattle?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - # Second call retrieves the existing agent (latest version) instead of creating a new one - # This is useful when you want to reuse an agent that was created earlier - agent2 = await provider.get_agent( - name="MyWeatherAgent", - tools=get_weather, # Tools must be provided for function tools - ) - - query = "What's the weather like in Tokyo?" - print(f"User: {query}") - result = await agent2.run(query) - print(f"Agent: {result}\n") - - print(f"First agent ID with version: {agent.id}") - print(f"Second agent ID with version: {agent2.id}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_agent_as_tool.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_agent_as_tool.py deleted file mode 100644 index c217242df7..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_agent_as_tool.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from collections.abc import Awaitable, Callable - -from agent_framework import FunctionInvocationContext -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent-as-Tool Example - -Demonstrates hierarchical agent architectures where one agent delegates -work to specialized sub-agents wrapped as tools using as_tool(). - -This pattern is useful when you want a coordinator agent to orchestrate -multiple specialized agents, each focusing on specific tasks. -""" - - -async def logging_middleware( - context: FunctionInvocationContext, - call_next: Callable[[], Awaitable[None]], -) -> None: - """MiddlewareTypes that logs tool invocations to show the delegation flow.""" - print(f"[Calling tool: {context.function.name}]") - print(f"[Request: {context.arguments}]") - - await call_next() - - print(f"[Response: {context.result}]") - - -async def main() -> None: - print("=== Azure AI Agent-as-Tool Pattern ===") - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a specialized writer agent - writer = await provider.create_agent( - name="WriterAgent", - instructions="You are a creative writer. Write short, engaging content.", - ) - - # Convert writer agent to a tool using as_tool() - writer_tool = writer.as_tool( - name="creative_writer", - description="Generate creative content like taglines, slogans, or short copy", - arg_name="request", - arg_description="What to write", - ) - - # Create coordinator agent with writer as a tool - coordinator = await provider.create_agent( - name="CoordinatorAgent", - instructions="You coordinate with specialized agents. Delegate writing tasks to the creative_writer tool.", - tools=[writer_tool], - middleware=[logging_middleware], - ) - - query = "Create a tagline for a coffee shop" - print(f"User: {query}") - result = await coordinator.run(query) - print(f"Coordinator: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_agent_to_agent.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_agent_to_agent.py deleted file mode 100644 index 881b4a38f1..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_agent_to_agent.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import os - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Agent-to-Agent (A2A) Example - -This sample demonstrates usage of AzureAIProjectAgentProvider with Agent-to-Agent (A2A) capabilities -to enable communication with other agents using the A2A protocol. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. -2. Ensure you have an A2A connection configured in your Azure AI project - and set A2A_PROJECT_CONNECTION_ID environment variable. -3. (Optional) A2A_ENDPOINT - If the connection is missing target (e.g., "Custom keys" type), - set the A2A endpoint URL directly. -""" - - -async def main() -> None: - # Configure A2A tool with connection ID - a2a_tool = { - "type": "a2a_preview", - "project_connection_id": os.environ["A2A_PROJECT_CONNECTION_ID"], - } - - # If the connection is missing a target, we need to set the A2A endpoint URL - if os.environ.get("A2A_ENDPOINT"): - a2a_tool["base_url"] = os.environ["A2A_ENDPOINT"] - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="MyA2AAgent", - instructions="""You are a helpful assistant that can communicate with other agents. - Use the A2A tool when you need to interact with other agents to complete tasks - or gather information from specialized agents.""", - tools=a2a_tool, - ) - - query = "What can the secondary agent do?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Result: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_application_endpoint.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_application_endpoint.py deleted file mode 100644 index 8183d2850a..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_application_endpoint.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os - -from agent_framework import Agent -from agent_framework.azure import AzureAIClient -from azure.ai.projects.aio import AIProjectClient -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Application Endpoint Example - -This sample demonstrates working with pre-existing Azure AI Agents by providing -application endpoint instead of project endpoint. -""" - - -async def main() -> None: - # Create the client - async with ( - AzureCliCredential() as credential, - # Endpoint here should be application endpoint with format: - # /api/projects//applications//protocols - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - Agent( - name="ApplicationAgent", - client=AzureAIClient( - project_client=project_client, - ), - ) as agent, - ): - query = "How are you?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_azure_ai_search.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_azure_ai_search.py deleted file mode 100644 index 3e7ce71096..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_azure_ai_search.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import os - -from agent_framework import Annotation -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Azure AI Search Example - -This sample demonstrates usage of AzureAIProjectAgentProvider with Azure AI Search -to search through indexed data and answer user questions about it. - -Citations from Azure AI Search are automatically enriched with document-specific -URLs (get_url) that can be used to retrieve the original documents. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. -2. Ensure you have an Azure AI Search connection configured in your Azure AI project - and set AI_SEARCH_PROJECT_CONNECTION_ID and AI_SEARCH_INDEX_NAME environment variable. -""" - - -async def main() -> None: - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="MySearchAgent", - instructions=( - "You are a helpful agent that searches hotel information using Azure AI Search. " - "Always use the search tool and index to find hotel data and provide accurate information." - ), - tools={ - "type": "azure_ai_search", - "azure_ai_search": { - "indexes": [ - { - "project_connection_id": os.environ["AI_SEARCH_PROJECT_CONNECTION_ID"], - "index_name": os.environ["AI_SEARCH_INDEX_NAME"], - # For query_type=vector, ensure your index has a field with vectorized data. - "query_type": "simple", - } - ] - }, - }, - ) - - query = ( - "Use Azure AI search knowledge tool to find detailed information about a winter hotel." - " Use the search tool and index." # You can modify prompt to force tool usage - ) - print(f"User: {query}") - - # Non-streaming: get response with enriched citations - result = await agent.run(query) - print(f"Result: {result}\n") - - # Display citations with document-specific URLs - if result.messages: - citations: list[Annotation] = [] - for msg in result.messages: - for content in msg.contents: - if hasattr(content, "annotations") and content.annotations: - citations.extend(content.annotations) - - if citations: - print("Citations:") - for i, citation in enumerate(citations, 1): - url = citation.get("url", "N/A") - # get_url contains the document-specific REST API URL from Azure AI Search - get_url = (citation.get("additional_properties") or {}).get("get_url") - print(f" [{i}] {citation.get('title', 'N/A')}") - print(f" URL: {url}") - if get_url: - print(f" Document URL: {get_url}") - - # Streaming: collect citations from streamed response - print("\n--- Streaming ---") - print(f"User: {query}") - print("Agent: ", end="", flush=True) - streaming_citations: list[Annotation] = [] - async for chunk in agent.run(query, stream=True): - if chunk.text: - print(chunk.text, end="", flush=True) - for content in getattr(chunk, "contents", []): - annotations = getattr(content, "annotations", []) - if annotations: - streaming_citations.extend(annotations) - - print() - if streaming_citations: - print("\nStreaming Citations:") - for i, citation in enumerate(streaming_citations, 1): - url = citation.get("url", "N/A") - get_url = (citation.get("additional_properties") or {}).get("get_url") - print(f" [{i}] {citation.get('title', 'N/A')}") - print(f" URL: {url}") - if get_url: - print(f" Document URL: {get_url}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_bing_custom_search.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_bing_custom_search.py deleted file mode 100644 index 123ee82431..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_bing_custom_search.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import os - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Bing Custom Search Example - -This sample demonstrates usage of AzureAIProjectAgentProvider with Bing Custom Search -to search custom search instances and provide responses with relevant results. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. -2. Ensure you have a Bing Custom Search connection configured in your Azure AI project - and set BING_CUSTOM_SEARCH_PROJECT_CONNECTION_ID and BING_CUSTOM_SEARCH_INSTANCE_NAME environment variables. -""" - - -async def main() -> None: - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="MyCustomSearchAgent", - instructions="""You are a helpful agent that can use Bing Custom Search tools to assist users. - Use the available Bing Custom Search tools to answer questions and perform tasks.""", - tools={ - "type": "bing_custom_search_preview", - "bing_custom_search_preview": { - "search_configurations": [ - { - "project_connection_id": os.environ["BING_CUSTOM_SEARCH_PROJECT_CONNECTION_ID"], - "instance_name": os.environ["BING_CUSTOM_SEARCH_INSTANCE_NAME"], - } - ] - }, - }, - ) - - query = "Tell me more about foundry agent service" - print(f"User: {query}") - result = await agent.run(query) - print(f"Result: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_bing_grounding.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_bing_grounding.py deleted file mode 100644 index e3a3e8330c..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_bing_grounding.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import os - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Bing Grounding Example - -This sample demonstrates usage of AzureAIProjectAgentProvider with Bing Grounding -to search the web for current information and provide grounded responses. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. -2. Ensure you have a Bing connection configured in your Azure AI project - and set BING_PROJECT_CONNECTION_ID environment variable. - -To get your Bing connection ID: -- Go to Azure AI Foundry portal (https://ai.azure.com) -- Navigate to your project's "Connected resources" section -- Add a new connection for "Grounding with Bing Search" -- Copy the connection ID and set it as the BING_PROJECT_CONNECTION_ID environment variable -""" - - -async def main() -> None: - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="MyBingGroundingAgent", - instructions="""You are a helpful assistant that can search the web for current information. - Use the Bing search tool to find up-to-date information and provide accurate, well-sourced answers. - Always cite your sources when possible.""", - tools={ - "type": "bing_grounding", - "bing_grounding": { - "search_configurations": [ - { - "project_connection_id": os.environ["BING_PROJECT_CONNECTION_ID"], - } - ] - }, - }, - ) - - query = "What is today's date and weather in Seattle?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Result: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_browser_automation.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_browser_automation.py deleted file mode 100644 index 33cd302485..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_browser_automation.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import os - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Browser Automation Example - -This sample demonstrates usage of AzureAIProjectAgentProvider with Browser Automation -to perform automated web browsing tasks and provide responses based on web interactions. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. -2. Ensure you have a Browser Automation connection configured in your Azure AI project - and set BROWSER_AUTOMATION_PROJECT_CONNECTION_ID environment variable. -""" - - -async def main() -> None: - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="MyBrowserAutomationAgent", - instructions="""You are an Agent helping with browser automation tasks. - You can answer questions, provide information, and assist with various tasks - related to web browsing using the Browser Automation tool available to you.""", - tools={ - "type": "browser_automation_preview", - "browser_automation_preview": { - "connection": { - "project_connection_id": os.environ["BROWSER_AUTOMATION_PROJECT_CONNECTION_ID"], - } - }, - }, - ) - - query = """Your goal is to report the percent of Microsoft year-to-date stock price change. - To do that, go to the website finance.yahoo.com. - At the top of the page, you will find a search bar. - Enter the value 'MSFT', to get information about the Microsoft stock price. - At the top of the resulting page you will see a default chart of Microsoft stock price. - Click on 'YTD' at the top of that chart, and report the percent value that shows up just below it.""" - - print(f"User: {query}") - result = await agent.run(query) - print(f"Result: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter.py deleted file mode 100644 index d196e9dd73..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework import ChatResponse -from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from openai.types.responses.response import Response as OpenAIResponse -from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent Code Interpreter Example - -This sample demonstrates using get_code_interpreter_tool() with AzureAIProjectAgentProvider -for Python code execution and mathematical problem solving. -""" - - -async def main() -> None: - """Example showing how to use the code interpreter tool with AzureAIProjectAgentProvider.""" - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIClient(credential=credential) - code_interpreter_tool = client.get_code_interpreter_tool() - - agent = await provider.create_agent( - name="MyCodeInterpreterAgent", - instructions="You are a helpful assistant that can write and execute Python code to solve problems.", - tools=[code_interpreter_tool], - ) - - query = "Use code to get the factorial of 100?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Result: {result}\n") - - if ( - isinstance(result.raw_representation, ChatResponse) - and isinstance(result.raw_representation.raw_representation, OpenAIResponse) - and len(result.raw_representation.raw_representation.output) > 0 - ): - # Find the first ResponseCodeInterpreterToolCall item - code_interpreter_item = next( - ( - item - for item in result.raw_representation.raw_representation.output - if isinstance(item, ResponseCodeInterpreterToolCall) - ), - None, - ) - - if code_interpreter_item is not None: - generated_code = code_interpreter_item.code - print(f"Generated code:\n{generated_code}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter_file_download.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter_file_download.py deleted file mode 100644 index 686c395f74..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter_file_download.py +++ /dev/null @@ -1,236 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import tempfile -from pathlib import Path - -from agent_framework import ( - Agent, - AgentResponseUpdate, - Annotation, - Content, -) -from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI V2 Code Interpreter File Download Sample - -This sample demonstrates how the AzureAIProjectAgentProvider handles file annotations -when code interpreter generates text files. It shows: -1. How to extract file IDs and container IDs from annotations -2. How to download container files using the OpenAI containers API -3. How to save downloaded files locally - -Note: Code interpreter generates files in containers, which require both -file_id and container_id to download via client.containers.files.content.retrieve(). -""" - -QUERY = ( - "Write a simple Python script that creates a text file called 'sample.txt' containing " - "'Hello from the code interpreter!' and save it to disk." -) - - -async def download_container_files(file_contents: list[Annotation | Content], agent: Agent) -> list[Path]: - """Download container files using the OpenAI containers API. - - Code interpreter generates files in containers, which require both file_id - and container_id to download. The container_id is stored in additional_properties. - - This function works for both streaming (Content with type="hosted_file") and non-streaming - (Annotation) responses. - - Args: - file_contents: List of Annotation or Content objects - containing file_id and container_id. - agent: The Agent instance with access to the AzureAIClient. - - Returns: - List of Path objects for successfully downloaded files. - """ - if not file_contents: - return [] - - # Create output directory in system temp folder - temp_dir = Path(tempfile.gettempdir()) - output_dir = temp_dir / "agent_framework_downloads" - output_dir.mkdir(exist_ok=True) - - print(f"\nDownloading {len(file_contents)} container file(s) to {output_dir.absolute()}...") - - # Access the OpenAI client from AzureAIClient - openai_client = agent.client.client # type: ignore[attr-defined] - - downloaded_files: list[Path] = [] - - for content in file_contents: - # Handle both Annotation (TypedDict) and Content objects - if isinstance(content, dict): # Annotation TypedDict - file_id = content.get("file_id") - additional_props = content.get("additional_properties", {}) - url = content.get("url") - else: # Content object - file_id = content.file_id - additional_props = content.additional_properties or {} - url = content.uri - - # Extract container_id from additional_properties - if not additional_props or "container_id" not in additional_props: - print(f" File {file_id}: ✗ Missing container_id") - continue - - container_id = additional_props["container_id"] - - # Extract filename based on content type - if isinstance(content, dict): # Annotation TypedDict - filename = url or f"{file_id}.txt" - # Extract filename from sandbox URL if present (e.g., sandbox:/mnt/data/sample.txt) - if filename.startswith("sandbox:"): - filename = filename.split("/")[-1] - else: # Content - filename = additional_props.get("filename") or f"{file_id}.txt" - - output_path = output_dir / filename - - try: - # Download using containers API - print(f" Downloading {filename}...", end="", flush=True) - file_content = await openai_client.containers.files.content.retrieve( - file_id=file_id, - container_id=container_id, - ) - - # file_content is HttpxBinaryResponseContent, read it - content_bytes = file_content.read() - - # Save to disk - output_path.write_bytes(content_bytes) - file_size = output_path.stat().st_size - print(f"({file_size} bytes)") - - downloaded_files.append(output_path) - - except Exception as e: - print(f"Failed: {e}") - - return downloaded_files - - -async def non_streaming_example() -> None: - """Example of downloading files from non-streaming response using Annotation.""" - print("=== Non-Streaming Response Example ===") - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIClient(credential=credential) - code_interpreter_tool = client.get_code_interpreter_tool() - - agent = await provider.create_agent( - name="V2CodeInterpreterFileAgent", - instructions="You are a helpful assistant that can write and execute Python code to create files.", - tools=[code_interpreter_tool], - ) - - print(f"User: {QUERY}\n") - - result = await agent.run(QUERY) - print(f"Agent: {result.text}\n") - - # Check for annotations in the response - annotations_found: list[Annotation] = [] - # AgentResponse has messages property, which contains Message objects - for message in result.messages: - for content in message.contents: - if content.type == "text" and content.annotations: - for annotation in content.annotations: - if annotation.get("file_id"): - annotations_found.append(annotation) - print(f"Found file annotation: file_id={annotation['file_id']}") - additional_props = annotation.get("additional_properties", {}) - if additional_props and "container_id" in additional_props: - print(f" container_id={additional_props['container_id']}") - - if annotations_found: - print(f"SUCCESS: Found {len(annotations_found)} file annotation(s)") - - # Download the container files (cast to Sequence for type compatibility) - downloaded_paths = await download_container_files(list(annotations_found), agent) - - if downloaded_paths: - print("\nDownloaded files available at:") - for path in downloaded_paths: - print(f" - {path.absolute()}") - else: - print("WARNING: No file annotations found in non-streaming response") - - -async def streaming_example() -> None: - """Example of downloading files from streaming response using Content with type='hosted_file'.""" - print("\n=== Streaming Response Example ===") - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIClient(credential=credential) - code_interpreter_tool = client.get_code_interpreter_tool() - - agent = await provider.create_agent( - name="V2CodeInterpreterFileAgentStreaming", - instructions="You are a helpful assistant that can write and execute Python code to create files.", - tools=[code_interpreter_tool], - ) - - print(f"User: {QUERY}\n") - file_contents_found: list[Content] = [] - text_chunks: list[str] = [] - - async for update in agent.run(QUERY, stream=True): - if isinstance(update, AgentResponseUpdate): - for content in update.contents: - if content.type == "text": - if content.text: - text_chunks.append(content.text) - if content.annotations: - for annotation in content.annotations: - if annotation.get("file_id"): - print(f"Found streaming annotation: file_id={annotation['file_id']}") - elif content.type == "hosted_file": - file_contents_found.append(content) - print(f"Found streaming hosted_file: file_id={content.file_id}") - if content.additional_properties and "container_id" in content.additional_properties: - print(f" container_id={content.additional_properties['container_id']}") - - print(f"\nAgent response: {''.join(text_chunks)[:200]}...") - - if file_contents_found: - print(f"SUCCESS: Found {len(file_contents_found)} file reference(s) in streaming") - - # Download the container files - downloaded_paths = await download_container_files(file_contents_found, agent) - - if downloaded_paths: - print("\n✓ Downloaded files available at:") - for path in downloaded_paths: - print(f" - {path.absolute()}") - else: - print("WARNING: No file annotations found in streaming response") - - -async def main() -> None: - print("AzureAIClient Code Interpreter File Download Sample\n") - await non_streaming_example() - await streaming_example() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter_file_generation.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter_file_generation.py deleted file mode 100644 index 38e3e7eada..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_code_interpreter_file_generation.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework import ( - AgentResponseUpdate, -) -from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI V2 Code Interpreter File Generation Sample - -This sample demonstrates how the AzureAIProjectAgentProvider handles file annotations -when code interpreter generates text files. It shows both non-streaming -and streaming approaches to verify file ID extraction. -""" - -QUERY = ( - "Write a simple Python script that creates a text file called 'sample.txt' containing " - "'Hello from the code interpreter!' and save it to disk." -) - - -async def non_streaming_example() -> None: - """Example of extracting file annotations from non-streaming response.""" - print("=== Non-Streaming Response Example ===") - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIClient(credential=credential) - code_interpreter_tool = client.get_code_interpreter_tool() - - agent = await provider.create_agent( - name="CodeInterpreterFileAgent", - instructions="You are a helpful assistant that can write and execute Python code to create files.", - tools=[code_interpreter_tool], - ) - - print(f"User: {QUERY}\n") - - result = await agent.run(QUERY) - print(f"Agent: {result.text}\n") - - # Check for annotations in the response - annotations_found: list[str] = [] - # AgentResponse has messages property, which contains Message objects - for message in result.messages: - for content in message.contents: - if content.type == "text" and content.annotations: - for annotation in content.annotations: - if annotation.get("file_id"): - annotations_found.append(annotation["file_id"]) - print(f"Found file annotation: file_id={annotation['file_id']}") - - if annotations_found: - print(f"SUCCESS: Found {len(annotations_found)} file annotation(s)") - else: - print("WARNING: No file annotations found in non-streaming response") - - -async def streaming_example() -> None: - """Example of extracting file annotations from streaming response.""" - print("\n=== Streaming Response Example ===") - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIClient(credential=credential) - code_interpreter_tool = client.get_code_interpreter_tool() - - agent = await provider.create_agent( - name="V2CodeInterpreterFileAgentStreaming", - instructions="You are a helpful assistant that can write and execute Python code to create files.", - tools=[code_interpreter_tool], - ) - - print(f"User: {QUERY}\n") - annotations_found: list[str] = [] - text_chunks: list[str] = [] - file_ids_found: list[str] = [] - - async for update in agent.run(QUERY, stream=True): - if isinstance(update, AgentResponseUpdate): - for content in update.contents: - if content.type == "text": - if content.text: - text_chunks.append(content.text) - if content.annotations: - for annotation in content.annotations: - if annotation.get("file_id"): - annotations_found.append(annotation["file_id"]) - print(f"Found streaming annotation: file_id={annotation['file_id']}") - elif content.type == "hosted_file": - file_ids_found.append(content.file_id) - print(f"Found streaming HostedFileContent: file_id={content.file_id}") - - print(f"\nAgent response: {''.join(text_chunks)[:200]}...") - - if annotations_found or file_ids_found: - total = len(annotations_found) + len(file_ids_found) - print(f"SUCCESS: Found {total} file reference(s) in streaming") - else: - print("WARNING: No file annotations found in streaming response") - - -async def main() -> None: - print("AzureAIClient Code Interpreter File Generation Sample\n") - await non_streaming_example() - await streaming_example() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_content_filtering.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_content_filtering.py deleted file mode 100644 index 3af4ef4854..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_content_filtering.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.ai.projects.models import RaiConfig -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Content Filtering (RAI Policy) Example - -This sample demonstrates how to enable content filtering on Azure AI agents using RaiConfig. - -Prerequisites: -1. Create an RAI Policy in Azure AI Foundry portal: - - Go to Azure AI Foundry > Your Project > Guardrails + Controls > Content Filters - - Create a new content filter or use an existing one - - Note the policy name - -2. Set environment variables: - - AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint - - AZURE_AI_MODEL_DEPLOYMENT_NAME: Your model deployment name - -3. Run `az login` to authenticate -""" - - -async def main() -> None: - print("=== Azure AI Agent with Content Filtering ===\n") - - # Replace with your RAI policy from Azure AI Foundry portal - rai_policy_name = ( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/" - "Microsoft.CognitiveServices/accounts/{accountName}/raiPolicies/{policyName}" - ) - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create agent with content filtering enabled via default_options - agent = await provider.create_agent( - name="ContentFilteredAgent", - instructions="You are a helpful assistant.", - default_options={"rai_config": RaiConfig(rai_policy_name=rai_policy_name)}, - ) - - # Test with a normal query - query = "What is the capital of France?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - # Test with a query that might trigger content filtering - # (depending on your RAI policy configuration) - query2 = "Tell me something inappropriate." - print(f"User: {query2}") - try: - result2 = await agent.run(query2) - print(f"Agent: {result2}\n") - except Exception as e: - print(f"Content filter triggered: {e}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_existing_agent.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_existing_agent.py deleted file mode 100644 index 84c6cc54de..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_existing_agent.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.ai.projects.aio import AIProjectClient -from azure.ai.projects.models import PromptAgentDefinition -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Existing Agent Example - -This sample demonstrates working with pre-existing Azure AI Agents by using provider.get_agent() method, -showing agent reuse patterns for production scenarios. -""" - - -async def using_provider_get_agent() -> None: - print("=== Get existing Azure AI agent with provider.get_agent() ===") - - # Create the client - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - ): - # Create remote agent using SDK directly - azure_ai_agent = await project_client.agents.create_version( - agent_name="MyNewTestAgent", - description="Agent for testing purposes.", - definition=PromptAgentDefinition( - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - # Setting specific requirements to verify that this agent is used. - instructions="End each response with [END].", - ), - ) - - try: - # Get newly created agent as Agent by using provider.get_agent() - provider = AzureAIProjectAgentProvider(project_client=project_client) - agent = await provider.get_agent(name=azure_ai_agent.name) - - # Verify agent properties - print(f"Agent ID: {agent.id}") - print(f"Agent name: {agent.name}") - print(f"Agent description: {agent.description}") - - query = "How are you?" - print(f"User: {query}") - result = await agent.run(query) - # Response that indicates that previously created agent was used: - # "I'm here and ready to help you! How can I assist you today? [END]" - print(f"Agent: {result}\n") - finally: - # Clean up the agent manually - await project_client.agents.delete_version( - agent_name=azure_ai_agent.name, agent_version=azure_ai_agent.version - ) - - -async def main() -> None: - await using_provider_get_agent() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_existing_conversation.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_existing_conversation.py deleted file mode 100644 index d19997b98a..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_existing_conversation.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import os -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.ai.projects.aio import AIProjectClient -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent Existing Conversation Example - -This sample demonstrates usage of AzureAIProjectAgentProvider with existing conversation created on service side. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def example_with_conversation_id() -> None: - """Example shows how to use existing conversation ID with the provider.""" - print("=== Azure AI Agent With Existing Conversation ===") - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - ): - # Create a conversation using OpenAI client - openai_client = project_client.get_openai_client() - conversation = await openai_client.conversations.create() - conversation_id = conversation.id - print(f"Conversation ID: {conversation_id}") - - provider = AzureAIProjectAgentProvider(project_client=project_client) - agent = await provider.create_agent( - name="BasicAgent", - instructions="You are a helpful agent.", - tools=get_weather, - ) - - # Pass conversation_id at run level - query = "What's the weather like in Seattle?" - print(f"User: {query}") - result = await agent.run(query, conversation_id=conversation_id) - print(f"Agent: {result.text}\n") - - query = "What was my last question?" - print(f"User: {query}") - result = await agent.run(query, conversation_id=conversation_id) - print(f"Agent: {result.text}\n") - - -async def example_with_session() -> None: - """This example shows how to specify existing conversation ID with AgentSession.""" - print("=== Azure AI Agent With Existing Conversation and Session ===") - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - ): - provider = AzureAIProjectAgentProvider(project_client=project_client) - agent = await provider.create_agent( - name="BasicAgent", - instructions="You are a helpful agent.", - tools=get_weather, - ) - - # Create a conversation using OpenAI client - openai_client = project_client.get_openai_client() - conversation = await openai_client.conversations.create() - conversation_id = conversation.id - print(f"Conversation ID: {conversation_id}") - - # Create a session with the existing ID - session = agent.create_session(service_session_id=conversation_id) - - query = "What's the weather like in Seattle?" - print(f"User: {query}") - result = await agent.run(query, session=session) - print(f"Agent: {result.text}\n") - - query = "What was my last question?" - print(f"User: {query}") - result = await agent.run(query, session=session) - print(f"Agent: {result.text}\n") - - -async def main() -> None: - await example_with_conversation_id() - await example_with_session() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_explicit_settings.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_explicit_settings.py deleted file mode 100644 index 9ee83478a3..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_explicit_settings.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Explicit Settings Example - -This sample demonstrates creating Azure AI Agents with explicit configuration -settings rather than relying on environment variable defaults. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def main() -> None: - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=credential, - ) as provider, - ): - agent = await provider.create_agent( - name="WeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - query = "What's the weather like in New York?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_file_search.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_file_search.py deleted file mode 100644 index c15edd95dd..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_file_search.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import contextlib -import os -from pathlib import Path - -from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider -from azure.ai.projects.aio import AIProjectClient -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -The following sample demonstrates how to create a simple, Azure AI agent that -uses a file search tool to answer user questions. -""" - - -# Simulate a conversation with the agent -USER_INPUTS = [ - "Who is the youngest employee?", - "Who works in sales?", - "I have a customer request, who can help me?", -] - - -async def main() -> None: - """Main function demonstrating Azure AI agent with file search capabilities.""" - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - AzureAIProjectAgentProvider(project_client=project_client) as provider, - ): - openai_client = project_client.get_openai_client() - - try: - # 1. Upload file and create vector store via OpenAI client - pdf_file_path = Path(__file__).parents[3] / "shared" / "resources" / "employees.pdf" - print(f"Uploading file from: {pdf_file_path}") - - vector_store = await openai_client.vector_stores.create(name="my_vectorstore") - print(f"Created vector store, vector store ID: {vector_store.id}") - - with open(pdf_file_path, "rb") as f: - file = await openai_client.vector_stores.files.upload_and_poll( - vector_store_id=vector_store.id, - file=f, - ) - print(f"Uploaded file, file ID: {file.id}") - - # 2. Create a file search tool - client = AzureAIClient(project_client=project_client) - file_search_tool = client.get_file_search_tool(vector_store_ids=[vector_store.id]) - - # 3. Create an agent with file search capabilities using the provider - agent = await provider.create_agent( - name="EmployeeSearchAgent", - instructions=( - "You are a helpful assistant that can search through uploaded employee files " - "to answer questions about employees." - ), - tools=[file_search_tool], - ) - - # 4. Simulate conversation with the agent - for user_input in USER_INPUTS: - print(f"# User: '{user_input}'") - response = await agent.run(user_input) - print(f"# Agent: {response.text}") - finally: - # 5. Cleanup: Delete the vector store (also deletes associated files) - with contextlib.suppress(Exception): - await openai_client.vector_stores.delete(vector_store.id) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_hosted_mcp.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_hosted_mcp.py deleted file mode 100644 index d892834afc..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_hosted_mcp.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from typing import Any - -from agent_framework import AgentResponse, AgentSession, Message, SupportsAgentRun -from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Hosted MCP Example - -This sample demonstrates integrating hosted Model Context Protocol (MCP) tools with Azure AI Agent. -""" - - -async def handle_approvals_without_session(query: str, agent: "SupportsAgentRun") -> AgentResponse: - """When we don't have a session, we need to ensure we return with the input, approval request and approval.""" - - result = await agent.run(query, store=False) - while len(result.user_input_requests) > 0: - new_inputs: list[Any] = [query] - for user_input_needed in result.user_input_requests: - print( - f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}" - f" with arguments: {user_input_needed.function_call.arguments}" - ) - new_inputs.append(Message("assistant", [user_input_needed])) - user_approval = input("Approve function call? (y/n): ") - new_inputs.append( - Message("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) - ) - - result = await agent.run(new_inputs, store=False) - return result - - -async def handle_approvals_with_session( - query: str, agent: "SupportsAgentRun", session: "AgentSession" -) -> AgentResponse: - """Here we let the session deal with the previous responses, and we just rerun with the approval.""" - - result = await agent.run(query, session=session) - while len(result.user_input_requests) > 0: - new_input: list[Any] = [] - for user_input_needed in result.user_input_requests: - print( - f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}" - f" with arguments: {user_input_needed.function_call.arguments}" - ) - user_approval = input("Approve function call? (y/n): ") - new_input.append( - Message( - role="user", - contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], - ) - ) - result = await agent.run(new_input, session=session) - return result - - -async def run_hosted_mcp_without_approval() -> None: - """Example showing MCP Tools without approval.""" - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIClient(credential=credential) - # Create MCP tool using instance method - mcp_tool = client.get_mcp_tool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - approval_mode="never_require", - ) - - agent = await provider.create_agent( - name="MyLearnDocsAgent", - instructions="You are a helpful assistant that can help with Microsoft documentation questions.", - tools=[mcp_tool], - ) - - query = "How to create an Azure storage account using az cli?" - print(f"User: {query}") - result = await handle_approvals_without_session(query, agent) - print(f"{agent.name}: {result}\n") - - -async def run_hosted_mcp_with_approval_and_session() -> None: - """Example showing MCP Tools with approvals using a session.""" - print("=== MCP with approvals and with session ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIClient(credential=credential) - # Create MCP tool using instance method - mcp_tool = client.get_mcp_tool( - name="api-specs", - url="https://gitmcp.io/Azure/azure-rest-api-specs", - approval_mode="always_require", - ) - - agent = await provider.create_agent( - name="MyApiSpecsAgent", - instructions="You are a helpful agent that can use MCP tools to assist users.", - tools=[mcp_tool], - ) - - session = agent.create_session() - query = "Please summarize the Azure REST API specifications Readme" - print(f"User: {query}") - result = await handle_approvals_with_session(query, agent, session) - print(f"{agent.name}: {result}\n") - - -async def main() -> None: - print("=== Azure AI Agent with Hosted MCP Tools Example ===\n") - - await run_hosted_mcp_without_approval() - await run_hosted_mcp_with_approval_and_session() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_image_generation.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_image_generation.py deleted file mode 100644 index e9d783d080..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_image_generation.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import base64 -import tempfile -from pathlib import Path -from urllib import request as urllib_request - -from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Image Generation Example - -This sample demonstrates basic usage of AzureAIProjectAgentProvider to create an agent -that can generate images based on user requirements. - -Pre-requisites: -- Make sure to set up the AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME - environment variables before running this sample. -""" - - -async def main() -> None: - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIClient(credential=credential) - # Create image generation tool using instance method - image_gen_tool = client.get_image_generation_tool( - model="gpt-image-1", - size="1024x1024", - output_format="png", - quality="low", - background="opaque", - ) - - agent = await provider.create_agent( - name="ImageGenAgent", - instructions="Generate images based on user requirements.", - tools=[image_gen_tool], - ) - - query = "Generate an image of Microsoft logo." - print(f"User: {query}") - result = await agent.run( - query, - # These additional options are required for image generation - options={ - "extra_headers": {"x-ms-oai-image-generation-deployment": "gpt-image-1-mini"}, - }, - ) - print(f"Agent: {result}\n") - - # Save the image to a file - print("Downloading generated image...") - image_data = [ - content.outputs - for content in result.messages[0].contents - if content.type == "image_generation_tool_result" and content.outputs is not None - ] - if image_data and image_data[0]: - # Save to the OS temporary directory - filename = "microsoft.png" - file_path = Path(tempfile.gettempdir()) / filename - # outputs can be a list of Content items (data/uri) or a single item - out = image_data[0][0] if isinstance(image_data[0], list) else image_data[0] - data_bytes: bytes | None = None - uri = getattr(out, "uri", None) - if isinstance(uri, str): - if ";base64," in uri: - try: - b64 = uri.split(";base64,", 1)[1] - data_bytes = base64.b64decode(b64) - except Exception: - data_bytes = None - else: - try: - data_bytes = await asyncio.to_thread(lambda: urllib_request.urlopen(uri).read()) - except Exception: - data_bytes = None - - if data_bytes is None: - raise RuntimeError("Image output present but could not retrieve bytes.") - - with open(file_path, "wb") as f: - f.write(data_bytes) - - print(f"Image downloaded and saved to: {file_path}") - else: - print("No image data found in the agent response.") - - """ - Sample output: - User: Generate an image of Microsoft logo. - Agent: Here is the Microsoft logo image featuring its iconic four quadrants. - - Downloading generated image... - Image downloaded and saved to: .../microsoft.png - """ - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_local_mcp.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_local_mcp.py deleted file mode 100644 index 77dd0cdad3..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_local_mcp.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework import MCPStreamableHTTPTool -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Local MCP Example - -This sample demonstrates integration of Azure AI Agents with local Model Context Protocol (MCP) -servers. - -Pre-requisites: -- Make sure to set up the AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME - environment variables before running this sample. -""" - - -async def main() -> None: - """Example showing use of Local MCP Tool with AzureAIProjectAgentProvider.""" - print("=== Azure AI Agent with Local MCP Tools Example ===\n") - - mcp_tool = MCPStreamableHTTPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ) - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="DocsAgent", - instructions="You are a helpful assistant that can help with Microsoft documentation questions.", - tools=mcp_tool, - ) - - # Use agent as context manager to ensure proper cleanup - async with agent: - # First query - first_query = "How to create an Azure storage account using az cli?" - print(f"User: {first_query}") - first_result = await agent.run(first_query) - print(f"Agent: {first_result}") - print("\n=======================================\n") - # Second query - second_query = "What is Microsoft Agent Framework?" - print(f"User: {second_query}") - second_result = await agent.run(second_query) - print(f"Agent: {second_result}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_memory_search.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_memory_search.py deleted file mode 100644 index 9377a78214..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_memory_search.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import os -import uuid - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.ai.projects.aio import AIProjectClient -from azure.ai.projects.models import MemoryStoreDefaultDefinition, MemoryStoreDefaultOptions -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Memory Search Example - -This sample demonstrates usage of AzureAIProjectAgentProvider with memory search capabilities -to retrieve relevant past user messages and maintain conversation context across sessions. -It shows explicit memory store creation using Azure AI Projects client and agent creation -using the Agent Framework. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. -2. Set AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME for the memory chat model. -3. Set AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME for the memory embedding model. -4. Deploy both a chat model (e.g. gpt-4.1) and an embedding model (e.g. text-embedding-3-small). -""" - - -async def main() -> None: - endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] - # Generate a unique memory store name to avoid conflicts - memory_store_name = f"agent_framework_memory_store_{uuid.uuid4().hex[:8]}" - - async with AzureCliCredential() as credential: - # Create the memory store using Azure AI Projects client - async with AIProjectClient(endpoint=endpoint, credential=credential) as project_client: - # Create a memory store using proper model classes - memory_store_definition = MemoryStoreDefaultDefinition( - chat_model=os.environ["AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME"], - embedding_model=os.environ["AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME"], - options=MemoryStoreDefaultOptions(user_profile_enabled=True, chat_summary_enabled=True), - ) - - memory_store = await project_client.beta.memory_stores.create( - name=memory_store_name, - description="Memory store for Agent Framework conversations", - definition=memory_store_definition, - ) - print(f"Created memory store: {memory_store.name} ({memory_store.id}): {memory_store.description}") - - # Then, create the agent using Agent Framework provider - async with AzureAIProjectAgentProvider(credential=credential) as provider: - agent = await provider.create_agent( - name="MyMemoryAgent", - instructions="""You are a helpful assistant that remembers past conversations. - Use the memory search tool to recall relevant information from previous interactions.""", - tools={ - "type": "memory_search_preview", - "memory_store_name": memory_store.name, - "scope": "user_123", - "update_delay": 1, # Wait 1 second before updating memories (use higher value in production) - }, - ) - - # First interaction - establish some preferences - print("=== First conversation ===") - query1 = "I prefer dark roast coffee" - print(f"User: {query1}") - result1 = await agent.run(query1) - print(f"Agent: {result1}\n") - - # Wait for memories to be processed - print("Waiting for memories to be stored...") - await asyncio.sleep(5) # Reduced wait time for demo purposes - - # Second interaction - test memory recall - print("=== Second conversation ===") - query2 = "Please order my usual coffee" - print(f"User: {query2}") - result2 = await agent.run(query2) - print(f"Agent: {result2}\n") - - # Clean up - delete the memory store - async with AIProjectClient(endpoint=endpoint, credential=credential) as project_client: - await project_client.beta.memory_stores.delete(memory_store_name) - print("Memory store deleted") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_microsoft_fabric.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_microsoft_fabric.py deleted file mode 100644 index 531a18cb69..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_microsoft_fabric.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import os - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Microsoft Fabric Example - -This sample demonstrates usage of AzureAIProjectAgentProvider with Microsoft Fabric -to query Fabric data sources and provide responses based on data analysis. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. -2. Ensure you have a Microsoft Fabric connection configured in your Azure AI project - and set FABRIC_PROJECT_CONNECTION_ID environment variable. -""" - - -async def main() -> None: - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="MyFabricAgent", - instructions="You are a helpful assistant.", - tools={ - "type": "fabric_dataagent_preview", - "fabric_dataagent_preview": { - "project_connections": [ - { - "project_connection_id": os.environ["FABRIC_PROJECT_CONNECTION_ID"], - } - ] - }, - }, - ) - - query = "Tell me about sales records" - print(f"User: {query}") - result = await agent.run(query) - print(f"Result: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_openapi.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_openapi.py deleted file mode 100644 index 2565c6ea23..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_openapi.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import json -from pathlib import Path - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with OpenAPI Tool Example - -This sample demonstrates usage of AzureAIProjectAgentProvider with OpenAPI tools -to call external APIs defined by OpenAPI specifications. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. -2. The countries.json OpenAPI specification is included in the resources folder. -""" - - -async def main() -> None: - # Load the OpenAPI specification - resources_path = Path(__file__).parents[3] / "shared" / "resources" / "countries.json" - - with open(resources_path) as f: - openapi_countries = json.load(f) - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="MyOpenAPIAgent", - instructions="""You are a helpful assistant that can use country APIs to provide information. - Use the available OpenAPI tools to answer questions about countries, currencies, and demographics.""", - tools={ - "type": "openapi", - "openapi": { - "name": "get_countries", - "spec": openapi_countries, - "description": "Retrieve information about countries by currency code", - "auth": {"type": "anonymous"}, - }, - }, - ) - - query = "What is the name and population of the country that uses currency with abbreviation THB?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_reasoning.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_reasoning.py deleted file mode 100644 index 60e6635fdd..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_reasoning.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.ai.projects.models import Reasoning -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Reasoning Example - -Demonstrates how to enable reasoning capabilities using the Reasoning option. -Shows both non-streaming and streaming approaches, including how to access -reasoning content (type="text_reasoning") separately from answer content. - -Requires a reasoning-capable model (e.g., gpt-5.2) deployed in your Azure AI Project configured -as `AZURE_AI_MODEL_DEPLOYMENT_NAME` in your environment. -""" - - -async def non_streaming_example() -> None: - """Example of non-streaming response (get the complete result at once).""" - print("=== Non-streaming Response Example ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="ReasoningWeatherAgent", - instructions="You are a helpful weather agent who likes to understand the underlying physics.", - default_options={"reasoning": Reasoning(effort="medium", summary="concise")}, - ) - - query = "How does the Bernoulli effect work?" - print(f"User: {query}") - result = await agent.run(query) - - for msg in result.messages: - for content in msg.contents: - if content.type == "text_reasoning": - print(f"[Reasoning]: {content.text}") - elif content.type == "text": - print(f"[Answer]: {content.text}") - print() - - -async def streaming_example() -> None: - """Example of streaming response (get results as they are generated).""" - print("=== Streaming Response Example ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="ReasoningWeatherAgent", - instructions="You are a helpful weather agent who likes to understand the underlying physics.", - default_options={"reasoning": Reasoning(effort="medium", summary="concise")}, - ) - - query = "Help explain how air updrafts work?" - print(f"User: {query}") - - shown_reasoning_label = False - shown_text_label = False - async for chunk in agent.run(query, stream=True): - for content in chunk.contents: - if content.type == "text_reasoning": - if not shown_reasoning_label: - print("[Reasoning]: ", end="", flush=True) - shown_reasoning_label = True - print(content.text, end="", flush=True) - elif content.type == "text": - if not shown_text_label: - print("\n\n[Answer]: ", end="", flush=True) - shown_text_label = True - print(content.text, end="", flush=True) - print("\n") - - -async def main() -> None: - print("=== Azure AI Agent with Reasoning Example ===") - - # await non_streaming_example() - await streaming_example() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_response_format.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_response_format.py deleted file mode 100644 index e1285b1d17..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_response_format.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import BaseModel, ConfigDict - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent Response Format Example - -This sample demonstrates basic usage of AzureAIProjectAgentProvider with response format, -also known as structured outputs. -""" - - -class ReleaseBrief(BaseModel): - feature: str - benefit: str - launch_date: str - model_config = ConfigDict(extra="forbid") - - -async def main() -> None: - """Example of using response_format property.""" - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="ProductMarketerAgent", - instructions="Return launch briefs as structured JSON.", - # Specify Pydantic model for structured output via default_options - default_options={"response_format": ReleaseBrief}, - ) - - query = "Draft a launch brief for the Contoso Note app." - print(f"User: {query}") - result = await agent.run(query) - - try: - release_brief = result.value - print("Agent:") - print(f"Feature: {release_brief.feature}") - print(f"Benefit: {release_brief.benefit}") - print(f"Launch date: {release_brief.launch_date}") - except Exception: - print(f"Failed to parse response: {result.text}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_runtime_json_schema.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_runtime_json_schema.py deleted file mode 100644 index ae74566023..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_runtime_json_schema.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent Response Format Example with Runtime JSON Schema - -This sample demonstrates basic usage of AzureAIProjectAgentProvider with response format, -also known as structured outputs. -""" - - -runtime_schema = { - "title": "WeatherDigest", - "type": "object", - "properties": { - "location": {"type": "string"}, - "conditions": {"type": "string"}, - "temperature_c": {"type": "number"}, - "advisory": {"type": "string"}, - }, - # OpenAI strict mode requires every property to appear in required. - "required": ["location", "conditions", "temperature_c", "advisory"], - "additionalProperties": False, -} - - -async def main() -> None: - """Example of using response_format property with a runtime JSON schema.""" - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Pass response_format via default_options using dict schema format - agent = await provider.create_agent( - name="WeatherDigestAgent", - instructions="Return sample weather digest as structured JSON.", - default_options={ - "response_format": { - "type": "json_schema", - "json_schema": { - "name": runtime_schema["title"], - "strict": True, - "schema": runtime_schema, - }, - } - }, - ) - - query = "Draft a sample weather digest." - print(f"User: {query}") - result = await agent.run(query) - - print(result.text) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_session.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_session.py deleted file mode 100644 index 209c6e2695..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_session.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Session Management Example - -This sample demonstrates session management with Azure AI Agent, showing -persistent conversation capabilities using service-managed sessions as well as storing messages in-memory. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production -# See: -# samples/02-agents/tools/function_tool_with_approval.py -# samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def example_with_automatic_session_creation() -> None: - """Example showing automatic session creation.""" - print("=== Automatic Session Creation Example ===") - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="BasicWeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - # First conversation - no session provided, will be created automatically - query1 = "What's the weather like in Seattle?" - print(f"User: {query1}") - result1 = await agent.run(query1) - print(f"Agent: {result1.text}") - - # Second conversation - still no session provided, will create another new session - query2 = "What was the last city I asked about?" - print(f"\nUser: {query2}") - result2 = await agent.run(query2) - print(f"Agent: {result2.text}") - print("Note: Each call creates a separate session, so the agent doesn't remember previous context.\n") - - -async def example_with_session_persistence_in_memory() -> None: - """ - Example showing session persistence across multiple conversations. - In this example, messages are stored in-memory. - """ - print("=== Session Persistence Example (In-Memory) ===") - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="BasicWeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - # Create a new session that will be reused - session = agent.create_session() - - # First conversation - first_query = "What's the weather like in Tokyo?" - print(f"User: {first_query}") - first_result = await agent.run(first_query, session=session, options={"store": False}) - print(f"Agent: {first_result.text}") - - # Second conversation using the same session - maintains context - second_query = "How about London?" - print(f"\nUser: {second_query}") - second_result = await agent.run(second_query, session=session, options={"store": False}) - print(f"Agent: {second_result.text}") - - # Third conversation - agent should remember both previous cities - third_query = "Which of the cities I asked about has better weather?" - print(f"\nUser: {third_query}") - third_result = await agent.run(third_query, session=session, options={"store": False}) - print(f"Agent: {third_result.text}") - print("Note: The agent remembers context from previous messages in the same session.\n") - - -async def example_with_existing_session_id() -> None: - """ - Example showing how to work with an existing session ID from the service. - In this example, messages are stored on the server. - """ - print("=== Existing Session ID Example ===") - - # First, create a conversation and capture the session ID - existing_session_id = None - - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="BasicWeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - # Start a conversation and get the session ID - session = agent.create_session() - - first_query = "What's the weather in Paris?" - print(f"User: {first_query}") - first_result = await agent.run(first_query, session=session) - print(f"Agent: {first_result.text}") - - # The session ID is set after the first response - existing_session_id = session.service_session_id - print(f"Session ID: {existing_session_id}") - - if existing_session_id: - print("\n--- Continuing with the same session ID in a new agent instance ---") - - # Retrieve the same agent (reuses existing agent version on the service) - second_agent = await provider.get_agent( - name="BasicWeatherAgent", - tools=get_weather, - ) - - # Attach the existing service session ID so conversation context is preserved - session = second_agent.get_session(service_session_id=existing_session_id) - - second_query = "What was the last city I asked about?" - print(f"User: {second_query}") - second_result = await second_agent.run(second_query, session=session) - print(f"Agent: {second_result.text}") - print("Note: The agent continues the conversation from the previous session by using session ID.\n") - - -async def main() -> None: - print("=== Azure AI Agent Session Management Examples ===\n") - - await example_with_automatic_session_creation() - await example_with_session_persistence_in_memory() - await example_with_existing_session_id() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_sharepoint.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_sharepoint.py deleted file mode 100644 index ce6bf85837..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_sharepoint.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import asyncio -import os - -from agent_framework.azure import AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with SharePoint Example - -This sample demonstrates usage of AzureAIProjectAgentProvider with SharePoint -to search through SharePoint content and answer user questions about it. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. -2. Ensure you have a SharePoint connection configured in your Azure AI project - and set SHAREPOINT_PROJECT_CONNECTION_ID environment variable. -""" - - -async def main() -> None: - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="MySharePointAgent", - instructions="""You are a helpful agent that can use SharePoint tools to assist users. - Use the available SharePoint tools to answer questions and perform tasks.""", - tools={ - "type": "sharepoint_grounding_preview", - "sharepoint_grounding_preview": { - "project_connections": [ - { - "project_connection_id": os.environ["SHAREPOINT_PROJECT_CONNECTION_ID"], - } - ] - }, - }, - ) - - query = "What is Contoso whistleblower policy?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Result: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai/azure_ai_with_web_search.py b/python/samples/02-agents/providers/azure_ai/azure_ai_with_web_search.py deleted file mode 100644 index 5134b275d4..0000000000 --- a/python/samples/02-agents/providers/azure_ai/azure_ai_with_web_search.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent With Web Search - -This sample demonstrates basic usage of AzureAIProjectAgentProvider to create an agent -that can perform web searches using get_web_search_tool(). - -Pre-requisites: -- Make sure to set up the AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME - environment variables before running this sample. -""" - - -async def main() -> None: - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIProjectAgentProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIClient(credential=credential) - # Create web search tool using instance method - web_search_tool = client.get_web_search_tool() - - agent = await provider.create_agent( - name="WebsearchAgent", - instructions="You are a helpful assistant that can search the web", - tools=[web_search_tool], - ) - - query = "What's the weather today in Seattle?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - """ - Sample output: - User: What's the weather today in Seattle? - Agent: Here is the updated weather forecast for Seattle: The current temperature is approximately 57°F, - mostly cloudy conditions, with light winds and a chance of rain later tonight. Check out more details - at the [National Weather Service](https://forecast.weather.gov/zipcity.php?inputstring=Seattle%2CWA). - """ - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/README.md b/python/samples/02-agents/providers/azure_ai_agent/README.md deleted file mode 100644 index 3a52984006..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# Azure AI Agent Examples - -This folder contains examples demonstrating different ways to create and use agents with Azure AI using the `AzureAIAgentsProvider` from the `agent_framework.azure` package. These examples use the `azure-ai-agents` 1.x (V1) API surface. For updated V2 (`azure-ai-projects` 2.x) samples, see the [Azure AI V2 examples folder](../azure_ai/). - -## Provider Pattern - -All examples in this folder use the `AzureAIAgentsProvider` class which provides a high-level interface for agent operations: - -- **`create_agent()`** - Create a new agent on the Azure AI service -- **`get_agent()`** - Retrieve an existing agent by ID or from a pre-fetched Agent object -- **`as_agent()`** - Wrap an SDK Agent object as a Agent without HTTP calls - -```python -from agent_framework.azure import AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential - -async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, -): - agent = await provider.create_agent( - name="MyAgent", - instructions="You are a helpful assistant.", - tools=my_function, - ) - result = await agent.run("Hello!") -``` - -## Examples - -| File | Description | -|------|-------------| -| [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive example demonstrating all `AzureAIAgentsProvider` methods: `create_agent()`, `get_agent()`, `as_agent()`, and managing multiple agents from a single provider. | -| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIAgentsProvider`. It automatically handles all configuration using environment variables. Shows both streaming and non-streaming responses. | -| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to find real-time information from the web using custom search configurations. Demonstrates how to use `AzureAIAgentClient.get_web_search_tool()` with custom search instances. | -| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to find real-time information from the web. Demonstrates `AzureAIAgentClient.get_web_search_tool()` with proper source citations and comprehensive error handling. | -| [`azure_ai_with_bing_grounding_citations.py`](azure_ai_with_bing_grounding_citations.py) | Demonstrates how to extract and display citations from Bing Grounding search responses. Shows how to collect citation annotations (title, URL, snippet) during streaming responses, enabling users to verify sources and access referenced content. | -| [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. | -| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use `AzureAIAgentClient.get_code_interpreter_tool()` with Azure AI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | -| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with an existing SDK Agent object using `provider.as_agent()`. This wraps the agent without making HTTP calls. | -| [`azure_ai_with_existing_session.py`](azure_ai_with_existing_session.py) | Shows how to work with a pre-existing session by providing the session ID. Demonstrates proper cleanup of manually created sessions. | -| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured provider settings, including project endpoint and model deployment name. | -| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Demonstrates how to use Azure AI Search with Azure AI agents. Shows how to create an agent with search tools using the SDK directly and wrap it with `provider.get_agent()`. | -| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Demonstrates how to use `AzureAIAgentClient.get_file_search_tool()` with Azure AI agents to search through uploaded documents. Shows file upload, vector store creation, and querying document content. | -| [`azure_ai_with_function_tools.py`](azure_ai_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | -| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to use `AzureAIAgentClient.get_mcp_tool()` with hosted Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates remote MCP server connections and tool discovery. | -| [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate Azure AI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates both agent-level and run-level tool configuration. | -| [`azure_ai_with_multiple_tools.py`](azure_ai_with_multiple_tools.py) | Demonstrates how to use multiple tools together with Azure AI agents, including web search, MCP servers, and function tools using client static methods. Shows coordinated multi-tool interactions and approval workflows. | -| [`azure_ai_with_openapi_tools.py`](azure_ai_with_openapi_tools.py) | Demonstrates how to use OpenAPI tools with Azure AI agents to integrate external REST APIs. Shows OpenAPI specification loading, anonymous authentication, session context management, and coordinated multi-API conversations. | -| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Demonstrates how to use structured outputs with Azure AI agents using Pydantic models. | -| [`azure_ai_with_session.py`](azure_ai_with_session.py) | Demonstrates session management with Azure AI agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | - -## Environment Variables - -Before running the examples, you need to set up your environment variables. You can do this in one of two ways: - -### Option 1: Using a .env file (Recommended) - -1. Copy the `.env.example` file from the `python` directory to create a `.env` file: - ```bash - cp ../../.env.example ../../.env - ``` - -2. Edit the `.env` file and add your values: - ``` - AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" - AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" - ``` - -3. For samples using Bing Grounding search (like `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`), you'll also need: - ``` - BING_CONNECTION_ID="your-bing-connection-id" - ``` - - To get your Bing connection details: - - Go to [Azure AI Foundry portal](https://ai.azure.com) - - Navigate to your project's "Connected resources" section - - Add a new connection for "Grounding with Bing Search" - - Copy the ID - -4. For samples using Bing Custom Search (like `azure_ai_with_bing_custom_search.py`), you'll also need: - ``` - BING_CUSTOM_CONNECTION_ID="your-bing-custom-connection-id" - BING_CUSTOM_INSTANCE_NAME="your-bing-custom-instance-name" - ``` - - To get your Bing Custom Search connection details: - - Go to [Azure AI Foundry portal](https://ai.azure.com) - - Navigate to your project's "Connected resources" section - - Add a new connection for "Grounding with Bing Custom Search" - - Copy the connection ID and instance name - -### Option 2: Using environment variables directly - -Set the environment variables in your shell: - -```bash -export AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" -export AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" -export BING_CONNECTION_ID="your-bing-connection-id" -export BING_CUSTOM_CONNECTION_ID="your-bing-custom-connection-id" -export BING_CUSTOM_INSTANCE_NAME="your-bing-custom-instance-name" -``` - -### Required Variables - -- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint (required for all examples) -- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (required for all examples) - -### Optional Variables - -- `BING_CONNECTION_ID`: Your Bing connection ID (required for `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`) -- `BING_CUSTOM_CONNECTION_ID`: Your Bing Custom Search connection ID (required for `azure_ai_with_bing_custom_search.py`) -- `BING_CUSTOM_INSTANCE_NAME`: Your Bing Custom Search instance name (required for `azure_ai_with_bing_custom_search.py`) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_basic.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_basic.py deleted file mode 100644 index 640653df26..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_basic.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent Basic Example - -This sample demonstrates basic usage of AzureAIAgentsProvider to create agents with automatic -lifecycle management. Shows both streaming and non-streaming responses with function tools. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def non_streaming_example() -> None: - """Example of non-streaming response (get the complete result at once).""" - print("=== Non-streaming Response Example ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="WeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - query = "What's the weather like in Seattle?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - -async def streaming_example() -> None: - """Example of streaming response (get results as they are generated).""" - print("=== Streaming Response Example ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="WeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - query = "What's the weather like in Portland?" - print(f"User: {query}") - print("Agent: ", end="", flush=True) - async for chunk in agent.run(query, stream=True): - if chunk.text: - print(chunk.text, end="", flush=True) - print("\n") - - -async def main() -> None: - print("=== Basic Azure AI Chat Client Agent Example ===") - - await non_streaming_example() - await streaming_example() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_provider_methods.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_provider_methods.py deleted file mode 100644 index 571f737315..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_provider_methods.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIAgentsProvider -from azure.ai.agents.aio import AgentsClient -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent Provider Methods Example - -This sample demonstrates the methods available on the AzureAIAgentsProvider class: -- create_agent(): Create a new agent on the service -- get_agent(): Retrieve an existing agent by ID -- as_agent(): Wrap an SDK Agent object without making HTTP calls -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def create_agent_example() -> None: - """Create a new agent using provider.create_agent().""" - print("\n--- create_agent() ---") - - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="WeatherAgent", - instructions="You are a helpful weather assistant.", - tools=get_weather, - ) - - print(f"Created: {agent.name} (ID: {agent.id})") - result = await agent.run("What's the weather in Seattle?") - print(f"Response: {result}") - - -async def get_agent_example() -> None: - """Retrieve an existing agent by ID using provider.get_agent().""" - print("\n--- get_agent() ---") - - async with ( - AzureCliCredential() as credential, - AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, - AzureAIAgentsProvider(agents_client=agents_client) as provider, - ): - # Create an agent directly with SDK (simulating pre-existing agent) - sdk_agent = await agents_client.create_agent( - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - name="ExistingAgent", - instructions="You always respond with 'Hello!'", - ) - - try: - # Retrieve using provider - agent = await provider.get_agent(sdk_agent.id) - print(f"Retrieved: {agent.name} (ID: {agent.id})") - - result = await agent.run("Hi there!") - print(f"Response: {result}") - finally: - await agents_client.delete_agent(sdk_agent.id) - - -async def as_agent_example() -> None: - """Wrap an SDK Agent object using provider.as_agent().""" - print("\n--- as_agent() ---") - - async with ( - AzureCliCredential() as credential, - AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, - AzureAIAgentsProvider(agents_client=agents_client) as provider, - ): - # Create agent using SDK - sdk_agent = await agents_client.create_agent( - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - name="WrappedAgent", - instructions="You respond with poetry.", - ) - - try: - # Wrap synchronously (no HTTP call) - agent = provider.as_agent(sdk_agent) - print(f"Wrapped: {agent.name} (ID: {agent.id})") - - result = await agent.run("Tell me about the sunset.") - print(f"Response: {result}") - finally: - await agents_client.delete_agent(sdk_agent.id) - - -async def multiple_agents_example() -> None: - """Create and manage multiple agents with a single provider.""" - print("\n--- Multiple Agents ---") - - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - weather_agent = await provider.create_agent( - name="WeatherSpecialist", - instructions="You are a weather specialist.", - tools=get_weather, - ) - - greeter_agent = await provider.create_agent( - name="GreeterAgent", - instructions="You are a friendly greeter.", - ) - - print(f"Created: {weather_agent.name}, {greeter_agent.name}") - - greeting = await greeter_agent.run("Hello!") - print(f"Greeter: {greeting}") - - weather = await weather_agent.run("What's the weather in Tokyo?") - print(f"Weather: {weather}") - - -async def main() -> None: - print("Azure AI Agent Provider Methods") - - await create_agent_example() - await get_agent_example() - await as_agent_example() - await multiple_agents_example() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_azure_ai_search.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_azure_ai_search.py deleted file mode 100644 index 67833d2dd0..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_azure_ai_search.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os - -from agent_framework import Annotation -from agent_framework.azure import AzureAIAgentsProvider -from azure.ai.agents.aio import AgentsClient -from azure.ai.projects.aio import AIProjectClient -from azure.ai.projects.models import ConnectionType -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Azure AI Search Example - -This sample demonstrates how to create an Azure AI agent that uses Azure AI Search -to search through indexed hotel data and answer user questions about hotels. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables -2. Ensure you have an Azure AI Search connection configured in your Azure AI project -3. The search index "hotels-sample-index" should exist in your Azure AI Search service - (you can create this using the Azure portal with sample hotel data) - -NOTE: To ensure consistent search tool usage: -- Include explicit instructions for the agent to use the search tool -- Mention the search requirement in your queries -- Use `tool_choice="required"` to force tool usage - -More info on `query type` can be found here: -https://learn.microsoft.com/en-us/python/api/azure-ai-agents/azure.ai.agents.models.aisearchindexresource?view=azure-python-preview -""" - - -async def main() -> None: - """Main function demonstrating Azure AI agent with raw Azure AI Search tool.""" - print("=== Azure AI Agent with Raw Azure AI Search Tool ===") - - # Create the client and manually create an agent with Azure AI Search tool - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, - AzureAIAgentsProvider(agents_client=agents_client) as provider, - ): - ai_search_conn_id = "" - async for connection in project_client.connections.list(): - if connection.type == ConnectionType.AZURE_AI_SEARCH: - ai_search_conn_id = connection.id - break - - # 1. Create Azure AI agent with the search tool using SDK directly - # (Azure AI Search tool requires special tool_resources configuration) - azure_ai_agent = await agents_client.create_agent( - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - name="HotelSearchAgent", - instructions=( - "You are a helpful agent that searches hotel information using Azure AI Search. " - "Always use the search tool and index to find hotel data and provide accurate information." - ), - tools=[{"type": "azure_ai_search"}], - tool_resources={ - "azure_ai_search": { - "indexes": [ - { - "index_connection_id": ai_search_conn_id, - "index_name": "hotels-sample-index", - "query_type": "vector", - } - ] - } - }, - ) - - try: - # 2. Use provider.as_agent() to wrap the existing agent - agent = provider.as_agent(agent=azure_ai_agent) - - print("This agent uses raw Azure AI Search tool to search hotel data.\n") - - # 3. Simulate conversation with the agent - user_input = ( - "Use Azure AI search knowledge tool to find detailed information about a winter hotel." - " Use the search tool and index." # You can modify prompt to force tool usage - ) - print(f"User: {user_input}") - print("Agent: ", end="", flush=True) - # Stream the response and collect citations - citations: list[Annotation] = [] - async for chunk in agent.run(user_input, stream=True): - if chunk.text: - print(chunk.text, end="", flush=True) - # Collect citations from Azure AI Search responses - for content in getattr(chunk, "contents", []): - annotations = getattr(content, "annotations", []) - if annotations: - citations.extend(annotations) - - print() - - # Display collected citation - if citations: - print("\n\nCitation:") - for i, citation in enumerate(citations, 1): - print(f"[{i}] {citation.get('url')}") - - print("\n" + "=" * 50 + "\n") - print("Hotel search conversation completed!") - - finally: - # Clean up the agent manually - await agents_client.delete_agent(azure_ai_agent.id) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_custom_search.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_custom_search.py deleted file mode 100644 index 4268568f85..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_custom_search.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -The following sample demonstrates how to create an Azure AI agent that -uses Bing Custom Search to find real-time information from the web. - -More information on Bing Custom Search and difference from Bing Grounding can be found here: -https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/bing-custom-search - -Prerequisites: -1. A connected Grounding with Bing Custom Search resource in your Azure AI project -2. Set BING_CUSTOM_CONNECTION_ID environment variable - Example: BING_CUSTOM_CONNECTION_ID="your-bing-custom-connection-id" -3. Set BING_CUSTOM_INSTANCE_NAME environment variable - Example: BING_CUSTOM_INSTANCE_NAME="your-bing-custom-instance-name" - -To set up Bing Custom Search: -1. Go to Azure AI Foundry portal (https://ai.azure.com) -2. Navigate to your project's "Connected resources" section -3. Add a new connection for "Grounding with Bing Custom Search" -4. Copy the connection ID and instance name and set the appropriate environment variables -""" - - -async def main() -> None: - """Main function demonstrating Azure AI agent with Bing Custom Search.""" - # Use AzureAIAgentsProvider for agent creation and management - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIAgentClient(credential=credential) - # Create Bing Custom Search tool using instance method - # The connection ID and instance name will be automatically picked up from environment variables - # (BING_CUSTOM_CONNECTION_ID and BING_CUSTOM_INSTANCE_NAME) - bing_search_tool = client.get_web_search_tool() - - agent = await provider.create_agent( - name="BingSearchAgent", - instructions=( - "You are a helpful agent that can use Bing Custom Search tools to assist users. " - "Use the available Bing Custom Search tools to answer questions and perform tasks." - ), - tools=[bing_search_tool], - ) - - # 3. Demonstrate agent capabilities with bing custom search - print("=== Azure AI Agent with Bing Custom Search ===\n") - - user_input = "Tell me more about foundry agent service" - print(f"User: {user_input}") - response = await agent.run(user_input) - print(f"Agent: {response.text}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_grounding.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_grounding.py deleted file mode 100644 index 7fb83b378d..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_grounding.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -The following sample demonstrates how to create an Azure AI agent that -uses Bing Grounding search to find real-time information from the web. - -Prerequisites: -1. A connected Grounding with Bing Search resource in your Azure AI project -2. Set BING_CONNECTION_ID environment variable - Example: BING_CONNECTION_ID="your-bing-connection-id" - -To set up Bing Grounding: -1. Go to Azure AI Foundry portal (https://ai.azure.com) -2. Navigate to your project's "Connected resources" section -3. Add a new connection for "Grounding with Bing Search" -4. Copy either the connection name or ID and set the appropriate environment variable -""" - - -async def main() -> None: - """Main function demonstrating Azure AI agent with Bing Grounding search.""" - # Use AzureAIAgentsProvider for agent creation and management - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIAgentClient(credential=credential) - # Create Bing Grounding search tool using instance method - # The connection ID will be automatically picked up from environment variable - bing_search_tool = client.get_web_search_tool() - - agent = await provider.create_agent( - name="BingSearchAgent", - instructions=( - "You are a helpful assistant that can search the web for current information. " - "Use the Bing search tool to find up-to-date information and provide accurate, " - "well-sourced answers. Always cite your sources when possible." - ), - tools=[bing_search_tool], - ) - - # 3. Demonstrate agent capabilities with web search - print("=== Azure AI Agent with Bing Grounding Search ===\n") - - user_input = "What is the most popular programming language?" - print(f"User: {user_input}") - response = await agent.run(user_input) - print(f"Agent: {response.text}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_grounding_citations.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_grounding_citations.py deleted file mode 100644 index 1b240f9812..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_bing_grounding_citations.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework import Annotation -from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -This sample demonstrates how to create an Azure AI agent that uses Bing Grounding -search to find real-time information from the web with comprehensive citation support. -It shows how to extract and display citations (title, URL, and snippet) from Bing -Grounding responses, enabling users to verify sources and explore referenced content. - -Prerequisites: -1. A connected Grounding with Bing Search resource in your Azure AI project -2. Set BING_CONNECTION_ID environment variable - Example: BING_CONNECTION_ID="your-bing-connection-id" - -To set up Bing Grounding: -1. Go to Azure AI Foundry portal (https://ai.azure.com) -2. Navigate to your project's "Connected resources" section -3. Add a new connection for "Grounding with Bing Search" -4. Copy the connection ID and set the BING_CONNECTION_ID environment variable -""" - - -async def main() -> None: - """Main function demonstrating Azure AI agent with Bing Grounding search.""" - # Use AzureAIAgentsProvider for agent creation and management - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIAgentClient(credential=credential) - # Create Bing Grounding search tool using instance method - # The connection ID will be automatically picked up from environment variable - bing_search_tool = client.get_web_search_tool() - - agent = await provider.create_agent( - name="BingSearchAgent", - instructions=( - "You are a helpful assistant that can search the web for current information. " - "Use the Bing search tool to find up-to-date information and provide accurate, " - "well-sourced answers. Always cite your sources when possible." - ), - tools=[bing_search_tool], - ) - - # 3. Demonstrate agent capabilities with web search - print("=== Azure AI Agent with Bing Grounding Search ===\n") - - user_input = "What is the most popular programming language?" - print(f"User: {user_input}") - print("Agent: ", end="", flush=True) - - # Stream the response and collect citations - citations: list[Annotation] = [] - async for chunk in agent.run(user_input, stream=True): - if chunk.text: - print(chunk.text, end="", flush=True) - - # Collect citations from Bing Grounding responses - for content in getattr(chunk, "contents", []): - annotations = getattr(content, "annotations", []) - if annotations: - citations.extend(annotations) - - print() - - # Display collected citations - if citations: - print("\n\nCitations:") - for i, citation in enumerate(citations, 1): - print(f"[{i}] {citation['title']}: {citation.get('url')}") - if "snippet" in citation: - print(f" Snippet: {citation.get('snippet')}") - else: - print("\nNo citations found in the response.") - - print() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter.py deleted file mode 100644 index 353a10f6a0..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework import AgentResponse, ChatResponseUpdate -from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider -from azure.ai.agents.models import ( - RunStepDeltaCodeInterpreterDetailItemObject, -) -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Code Interpreter Example - -This sample demonstrates using get_code_interpreter_tool() with Azure AI Agents -for Python code execution and mathematical problem solving. -""" - - -def print_code_interpreter_inputs(response: AgentResponse) -> None: - """Helper method to access code interpreter data.""" - - print("\nCode Interpreter Inputs during the run:") - if response.raw_representation is None: - return - for chunk in response.raw_representation: - if isinstance(chunk, ChatResponseUpdate) and isinstance( - chunk.raw_representation, RunStepDeltaCodeInterpreterDetailItemObject - ): - print(chunk.raw_representation.input, end="") - print("\n") - - -async def main() -> None: - """Example showing how to use the code interpreter tool with Azure AI.""" - print("=== Azure AI Agent with Code Interpreter Example ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIAgentClient(credential=credential) - code_interpreter_tool = client.get_code_interpreter_tool() - - agent = await provider.create_agent( - name="CodingAgent", - instructions=("You are a helpful assistant that can write and execute Python code to solve problems."), - tools=[code_interpreter_tool], - ) - query = "Generate the factorial of 100 using python code, show the code and execute it." - print(f"User: {query}") - response = await agent.run(query) - print(f"Agent: {response}") - # To review the code interpreter outputs, you can access - # them from the response raw_representations, just uncomment the next line: - # print_code_interpreter_inputs(response) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py deleted file mode 100644 index e96b439b92..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os - -from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider -from azure.ai.agents.aio import AgentsClient -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent Code Interpreter File Generation Example - -This sample demonstrates using get_code_interpreter_tool() with AzureAIAgentsProvider -to generate a text file and then retrieve it. - -The test flow: -1. Create an agent with code interpreter tool -2. Ask the agent to generate a txt file using Python code -3. Capture the file_id from HostedFileContent in the response -4. Retrieve the file using the agents_client.files API -""" - - -async def main() -> None: - """Test file generation and retrieval with code interpreter.""" - - async with ( - AzureCliCredential() as credential, - AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, - AzureAIAgentsProvider(agents_client=agents_client) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIAgentClient(credential=credential) - code_interpreter_tool = client.get_code_interpreter_tool() - - agent = await provider.create_agent( - name="CodeInterpreterAgent", - instructions=( - "You are a Python code execution assistant. " - "ALWAYS use the code interpreter tool to execute Python code when asked to create files. " - "Write actual Python code to create files, do not just describe what you would do." - ), - tools=[code_interpreter_tool], - ) - - # Be very explicit about wanting code execution and a download link - query = ( - "Use the code interpreter to execute this Python code and then provide me " - "with a download link for the generated file:\n" - "```python\n" - "with open('/mnt/data/sample.txt', 'w') as f:\n" - " f.write('Hello, World! This is a test file.')\n" - "'/mnt/data/sample.txt'\n" # Return the path so it becomes downloadable - "```" - ) - print(f"User: {query}\n") - print("=" * 60) - - # Collect file_ids from the response - file_ids: list[str] = [] - - async for chunk in agent.run(query, stream=True): - for content in chunk.contents: - if content.type == "text": - print(content.text, end="", flush=True) - elif content.type == "hosted_file" and content.file_id: - file_ids.append(content.file_id) - print(f"\n[File generated: {content.file_id}]") - - print("\n" + "=" * 60) - - # Attempt to retrieve discovered files - if file_ids: - print(f"\nAttempting to retrieve {len(file_ids)} file(s):") - for file_id in file_ids: - try: - file_info = await agents_client.files.get(file_id) - print(f" File {file_id}: Retrieved successfully") - print(f" Filename: {file_info.filename}") - print(f" Purpose: {file_info.purpose}") - print(f" Bytes: {file_info.bytes}") - except Exception as e: - print(f" File {file_id}: FAILED to retrieve - {e}") - else: - print("No file IDs were captured from the response.") - - # List all files to see if any exist - print("\nListing all files in the agent service:") - try: - files_list = await agents_client.files.list() - count = 0 - for file_info in files_list.data: - count += 1 - print(f" - {file_info.id}: {file_info.filename} ({file_info.purpose})") - if count == 0: - print(" No files found.") - except Exception as e: - print(f" Failed to list files: {e}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_existing_agent.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_existing_agent.py deleted file mode 100644 index 95a93303f1..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_existing_agent.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os - -from agent_framework.azure import AzureAIAgentsProvider -from azure.ai.agents.aio import AgentsClient -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Existing Agent Example - -This sample demonstrates working with pre-existing Azure AI Agents by providing -agent IDs, showing agent reuse patterns for production scenarios. -""" - - -async def main() -> None: - print("=== Azure AI Agent with Existing Agent ===") - - # Create the client and provider - async with ( - AzureCliCredential() as credential, - AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, - AzureAIAgentsProvider(agents_client=agents_client) as provider, - ): - # Create an agent on the service with default instructions - # These instructions will persist on created agent for every run. - azure_ai_agent = await agents_client.create_agent( - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - instructions="End each response with [END].", - ) - - try: - # Wrap existing agent instance using provider.as_agent() - agent = provider.as_agent(azure_ai_agent) - - query = "How are you?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - finally: - # Clean up the agent manually - await agents_client.delete_agent(azure_ai_agent.id) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_existing_session.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_existing_session.py deleted file mode 100644 index 76f08b37ea..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_existing_session.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIAgentsProvider -from azure.ai.agents.aio import AgentsClient -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Existing Session Example - -This sample demonstrates working with pre-existing conversation sessions -by providing session IDs for session reuse patterns. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def main() -> None: - print("=== Azure AI Agent with Existing Session ===") - - # Create the client and provider - async with ( - AzureCliCredential() as credential, - AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, - AzureAIAgentsProvider(agents_client=agents_client) as provider, - ): - # Create a session that will persist - created_thread = await agents_client.threads.create() - - try: - # Create agent using provider - agent = await provider.create_agent( - name="WeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - session = agent.get_session(service_session_id=created_thread.id) - result = await agent.run("What's the weather like in Tokyo?", session=session) - print(f"Result: {result}\n") - finally: - # Clean up the session manually - await agents_client.threads.delete(created_thread.id) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_explicit_settings.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_explicit_settings.py deleted file mode 100644 index f34d9e41e9..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_explicit_settings.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Explicit Settings Example - -This sample demonstrates creating Azure AI Agents with explicit configuration -settings rather than relying on environment variable defaults. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def main() -> None: - print("=== Azure AI Agent with Explicit Settings ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - credential=credential, - ) as provider, - ): - agent = await provider.create_agent( - name="WeatherAgent", - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - result = await agent.run("What's the weather like in New York?") - print(f"Result: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_file_search.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_file_search.py deleted file mode 100644 index 7721152e6e..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_file_search.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os -from pathlib import Path - -from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider -from azure.ai.agents.aio import AgentsClient -from azure.ai.agents.models import FileInfo, VectorStore -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -The following sample demonstrates how to create a simple, Azure AI agent that -uses a file search tool to answer user questions. -""" - - -# Simulate a conversation with the agent -USER_INPUTS = [ - "Who is the youngest employee?", - "Who works in sales?", - "I have a customer request, who can help me?", -] - - -async def main() -> None: - """Main function demonstrating Azure AI agent with file search capabilities.""" - file: FileInfo | None = None - vector_store: VectorStore | None = None - - async with ( - AzureCliCredential() as credential, - AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, - AzureAIAgentsProvider(agents_client=agents_client) as provider, - ): - try: - # 1. Upload file and create vector store - pdf_file_path = Path(__file__).parents[3] / "shared" / "resources" / "employees.pdf" - print(f"Uploading file from: {pdf_file_path}") - - file = await agents_client.files.upload_and_poll(file_path=str(pdf_file_path), purpose="assistants") - print(f"Uploaded file, file ID: {file.id}") - - vector_store = await agents_client.vector_stores.create_and_poll(file_ids=[file.id], name="my_vectorstore") - print(f"Created vector store, vector store ID: {vector_store.id}") - - # 2. Create a client to access hosted tool factory methods - client = AzureAIAgentClient(credential=credential) - file_search_tool = client.get_file_search_tool(vector_store_ids=[vector_store.id]) - - # 3. Create an agent with file search capabilities - agent = await provider.create_agent( - name="EmployeeSearchAgent", - instructions=( - "You are a helpful assistant that can search through uploaded employee files " - "to answer questions about employees." - ), - tools=[file_search_tool], - ) - - # 4. Simulate conversation with the agent - for user_input in USER_INPUTS: - print(f"# User: '{user_input}'") - response = await agent.run(user_input) - print(f"# Agent: {response.text}") - - finally: - # 5. Cleanup: Delete the vector store and file - try: - if vector_store: - await agents_client.vector_stores.delete(vector_store.id) - if file: - await agents_client.files.delete(file.id) - except Exception: - # Ignore cleanup errors to avoid masking issues - pass - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_function_tools.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_function_tools.py deleted file mode 100644 index 3366af58ce..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_function_tools.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from datetime import datetime, timezone -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Function Tools Example - -This sample demonstrates function tool integration with Azure AI Agents, -showing both agent-level and query-level tool configuration patterns. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -@tool(approval_mode="never_require") -def get_time() -> str: - """Get the current UTC time.""" - current_time = datetime.now(timezone.utc) - return f"The current UTC time is {current_time.strftime('%Y-%m-%d %H:%M:%S')}." - - -async def tools_on_agent_level() -> None: - """Example showing tools defined when creating the agent.""" - print("=== Tools Defined on Agent Level ===") - - # Tools are provided when creating the agent - # The agent can use these tools for any query during its lifetime - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="AssistantAgent", - instructions="You are a helpful assistant that can provide weather and time information.", - tools=[get_weather, get_time], # Tools defined at agent creation - ) - - # First query - agent can use weather tool - query1 = "What's the weather like in New York?" - print(f"User: {query1}") - result1 = await agent.run(query1) - print(f"Agent: {result1}\n") - - # Second query - agent can use time tool - query2 = "What's the current UTC time?" - print(f"User: {query2}") - result2 = await agent.run(query2) - print(f"Agent: {result2}\n") - - # Third query - agent can use both tools if needed - query3 = "What's the weather in London and what's the current UTC time?" - print(f"User: {query3}") - result3 = await agent.run(query3) - print(f"Agent: {result3}\n") - - -async def tools_on_run_level() -> None: - """Example showing tools passed to the run method.""" - print("=== Tools Passed to Run Method ===") - - # Agent created without tools - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="AssistantAgent", - instructions="You are a helpful assistant.", - # No tools defined here - ) - - # First query with weather tool - query1 = "What's the weather like in Seattle?" - print(f"User: {query1}") - result1 = await agent.run(query1, tools=[get_weather]) # Tool passed to run method - print(f"Agent: {result1}\n") - - # Second query with time tool - query2 = "What's the current UTC time?" - print(f"User: {query2}") - result2 = await agent.run(query2, tools=[get_time]) # Different tool for this query - print(f"Agent: {result2}\n") - - # Third query with multiple tools - query3 = "What's the weather in Chicago and what's the current UTC time?" - print(f"User: {query3}") - result3 = await agent.run(query3, tools=[get_weather, get_time]) # Multiple tools - print(f"Agent: {result3}\n") - - -async def mixed_tools_example() -> None: - """Example showing both agent-level tools and run-method tools.""" - print("=== Mixed Tools Example (Agent + Run Method) ===") - - # Agent created with some base tools - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="AssistantAgent", - instructions="You are a comprehensive assistant that can help with various information requests.", - tools=[get_weather], # Base tool available for all queries - ) - - # Query using both agent tool and additional run-method tools - query = "What's the weather in Denver and what's the current UTC time?" - print(f"User: {query}") - - # Agent has access to get_weather (from creation) + additional tools from run method - result = await agent.run( - query, - tools=[get_time], # Additional tools for this specific query - ) - print(f"Agent: {result}\n") - - -async def main() -> None: - print("=== Azure AI Chat Client Agent with Function Tools Examples ===\n") - - await tools_on_agent_level() - await tools_on_run_level() - await mixed_tools_example() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_hosted_mcp.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_hosted_mcp.py deleted file mode 100644 index 2401ee9331..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_hosted_mcp.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from typing import Any - -from agent_framework import AgentResponse, AgentSession, SupportsAgentRun -from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Hosted MCP Example - -This sample demonstrates integration of Azure AI Agents with hosted Model Context Protocol (MCP) -servers, including user approval workflows for function call security. -""" - - -async def handle_approvals_with_session( - query: str, agent: "SupportsAgentRun", session: "AgentSession" -) -> AgentResponse: - """Here we let the session deal with the previous responses, and we just rerun with the approval.""" - from agent_framework import Message - - result = await agent.run(query, session=session, store=True) - while len(result.user_input_requests) > 0: - new_input: list[Any] = [] - for user_input_needed in result.user_input_requests: - print( - f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}" - f" with arguments: {user_input_needed.function_call.arguments}" - ) - user_approval = input("Approve function call? (y/n): ") - new_input.append( - Message( - role="user", - contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], - ) - ) - result = await agent.run(new_input, session=session, store=True) - return result - - -async def main() -> None: - """Example showing Hosted MCP tools for a Azure AI Agent.""" - - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIAgentClient(credential=credential) - # Create MCP tool using instance method - mcp_tool = client.get_mcp_tool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ) - - agent = await provider.create_agent( - name="DocsAgent", - instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=[mcp_tool], - ) - session = agent.create_session() - # First query - query1 = "How to create an Azure storage account using az cli?" - print(f"User: {query1}") - result1 = await handle_approvals_with_session(query1, agent, session) - print(f"{agent.name}: {result1}\n") - print("\n=======================================\n") - # Second query - query2 = "What is Microsoft Agent Framework?" - print(f"User: {query2}") - result2 = await handle_approvals_with_session(query2, agent, session) - print(f"{agent.name}: {result2}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_local_mcp.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_local_mcp.py deleted file mode 100644 index f7165e4c8d..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_local_mcp.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework import MCPStreamableHTTPTool -from agent_framework.azure import AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Local MCP Example - -This sample demonstrates integration of Azure AI Agents with local Model Context Protocol (MCP) -servers, showing both agent-level and run-level tool configuration patterns. -""" - - -async def mcp_tools_on_run_level() -> None: - """Example showing MCP tools defined when running the agent.""" - print("=== Tools Defined on Run Level ===") - - # Tools are provided when running the agent - # This means we have to ensure we connect to the MCP server before running the agent - # and pass the tools to the run method. - async with ( - AzureCliCredential() as credential, - MCPStreamableHTTPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ) as mcp_server, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="DocsAgent", - instructions="You are a helpful assistant that can help with microsoft documentation questions.", - ) - # First query - query1 = "How to create an Azure storage account using az cli?" - print(f"User: {query1}") - result1 = await agent.run(query1, tools=mcp_server) - print(f"{agent.name}: {result1}\n") - print("\n=======================================\n") - # Second query - query2 = "What is Microsoft Agent Framework?" - print(f"User: {query2}") - result2 = await agent.run(query2, tools=mcp_server) - print(f"{agent.name}: {result2}\n") - - -async def mcp_tools_on_agent_level() -> None: - """Example showing local MCP tools passed when creating the agent.""" - print("=== Tools Defined on Agent Level ===") - - # Tools are provided when creating the agent - # The Agent will connect to the MCP server through its context manager - # and discover tools at runtime - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="DocsAgent", - instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=MCPStreamableHTTPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ), - ) - # Use agent as context manager to connect MCP tools - async with agent: - # First query - query1 = "How to create an Azure storage account using az cli?" - print(f"User: {query1}") - result1 = await agent.run(query1) - print(f"{agent.name}: {result1}\n") - print("\n=======================================\n") - # Second query - query2 = "What is Microsoft Agent Framework?" - print(f"User: {query2}") - result2 = await agent.run(query2) - print(f"{agent.name}: {result2}\n") - - -async def main() -> None: - print("=== Azure AI Chat Client Agent with MCP Tools Examples ===\n") - - await mcp_tools_on_agent_level() - await mcp_tools_on_run_level() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_multiple_tools.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_multiple_tools.py deleted file mode 100644 index c7e8056219..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_multiple_tools.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from datetime import datetime, timezone -from typing import Any - -from agent_framework import ( - AgentSession, - SupportsAgentRun, - tool, -) -from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Multiple Tools Example - -This sample demonstrates integrating multiple tools (MCP and Web Search) with Azure AI Agents, -including user approval workflows for function call security. - -Prerequisites: -1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables -2. For Bing search functionality, set BING_CONNECTION_ID environment variable to your Bing connection ID - Example: BING_CONNECTION_ID="/subscriptions/{subscription-id}/resourceGroups/{resource-group}/ - providers/Microsoft.CognitiveServices/accounts/{ai-service-name}/projects/{project-name}/ - connections/{connection-name}" - -To set up Bing Grounding: -1. Go to Azure AI Foundry portal (https://ai.azure.com) -2. Navigate to your project's "Connected resources" section -3. Add a new connection for "Grounding with Bing Search" -4. Copy the connection ID and set it as the BING_CONNECTION_ID environment variable -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_time() -> str: - """Get the current UTC time.""" - current_time = datetime.now(timezone.utc) - return f"The current UTC time is {current_time.strftime('%Y-%m-%d %H:%M:%S')}." - - -async def handle_approvals_with_session(query: str, agent: "SupportsAgentRun", session: "AgentSession"): - """Here we let the session deal with the previous responses, and we just rerun with the approval.""" - from agent_framework import Message - - result = await agent.run(query, session=session, store=True) - while len(result.user_input_requests) > 0: - new_input: list[Any] = [] - for user_input_needed in result.user_input_requests: - print( - f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}" - f" with arguments: {user_input_needed.function_call.arguments}" - ) - user_approval = input("Approve function call? (y/n): ") - new_input.append( - Message( - role="user", - contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], - ) - ) - result = await agent.run(new_input, session=session, store=True) - return result - - -async def main() -> None: - """Example showing multiple tools for an Azure AI Agent.""" - - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - # Create a client to access hosted tool factory methods - client = AzureAIAgentClient(credential=credential) - # Create tools using instance methods - mcp_tool = client.get_mcp_tool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ) - web_search_tool = client.get_web_search_tool() - - agent = await provider.create_agent( - name="DocsAgent", - instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=[ - mcp_tool, - web_search_tool, - get_time, - ], - ) - session = agent.create_session() - # First query - query1 = "How to create an Azure storage account using az cli and what time is it?" - print(f"User: {query1}") - result1 = await handle_approvals_with_session(query1, agent, session) - print(f"{agent.name}: {result1}\n") - print("\n=======================================\n") - # Second query - query2 = "What is Microsoft Agent Framework and use a web search to see what is Reddit saying about it?" - print(f"User: {query2}") - result2 = await handle_approvals_with_session(query2, agent, session) - print(f"{agent.name}: {result2}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_openapi_tools.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_openapi_tools.py deleted file mode 100644 index 6eb032ae2c..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_openapi_tools.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import json -from pathlib import Path -from typing import Any - -from agent_framework.azure import AzureAIAgentsProvider -from azure.ai.agents.models import OpenApiAnonymousAuthDetails, OpenApiTool -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -""" -The following sample demonstrates how to create a simple, Azure AI agent that -uses OpenAPI tools to answer user questions. -""" - -# Simulate a conversation with the agent -USER_INPUTS = [ - "What is the name and population of the country that uses currency with abbreviation THB?", - "What is the current weather in the capital city of that country?", -] - - -def load_openapi_specs() -> tuple[dict[str, Any], dict[str, Any]]: - """Load OpenAPI specification files.""" - resources_path = Path(__file__).parents[3] / "shared" / "resources" - - with open(resources_path / "weather.json") as weather_file: - weather_spec = json.load(weather_file) - - with open(resources_path / "countries.json") as countries_file: - countries_spec = json.load(countries_file) - - return weather_spec, countries_spec - - -async def main() -> None: - """Main function demonstrating Azure AI agent with OpenAPI tools.""" - # 1. Load OpenAPI specifications (synchronous operation) - weather_openapi_spec, countries_openapi_spec = load_openapi_specs() - - # 2. Use AzureAIAgentsProvider for agent creation and management - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - # 3. Create OpenAPI tools using Azure AI's OpenApiTool - auth = OpenApiAnonymousAuthDetails() - - openapi_weather = OpenApiTool( - name="get_weather", - spec=weather_openapi_spec, - description="Retrieve weather information for a location using wttr.in service", - auth=auth, - ) - - openapi_countries = OpenApiTool( - name="get_country_info", - spec=countries_openapi_spec, - description="Retrieve country information including population and capital city", - auth=auth, - ) - - # 4. Create an agent with OpenAPI tools - # Note: We need to pass the Azure AI native OpenApiTool definitions directly - # since the agent framework doesn't have a HostedOpenApiTool wrapper yet - agent = await provider.create_agent( - name="OpenAPIAgent", - instructions=( - "You are a helpful assistant that can search for country information " - "and weather data using APIs. When asked about countries, use the country " - "API to find information. When asked about weather, use the weather API. " - "Provide clear, informative answers based on the API results." - ), - # Pass the raw tool definitions from Azure AI's OpenApiTool - tools=[*openapi_countries.definitions, *openapi_weather.definitions], - ) - - # 5. Simulate conversation with the agent maintaining session context - print("=== Azure AI Agent with OpenAPI Tools ===\n") - - # Create a session to maintain conversation context across multiple runs - session = agent.create_session() - - for user_input in USER_INPUTS: - print(f"User: {user_input}") - # Pass the session to maintain context across multiple agent.run() calls - response = await agent.run(user_input, session=session) - print(f"Agent: {response.text}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_response_format.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_response_format.py deleted file mode 100644 index 8fd4a7d365..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_response_format.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework.azure import AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import BaseModel, ConfigDict - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent Provider Response Format Example - -This sample demonstrates using AzureAIAgentsProvider with response_format -for structured outputs in two ways: -1. Setting default response_format at agent creation time (default_options) -2. Overriding response_format at runtime (options parameter in agent.run) -""" - - -class WeatherInfo(BaseModel): - """Structured weather information.""" - - location: str - temperature: int - conditions: str - recommendation: str - model_config = ConfigDict(extra="forbid") - - -class CityInfo(BaseModel): - """Structured city information.""" - - city_name: str - population: int - country: str - model_config = ConfigDict(extra="forbid") - - -async def main() -> None: - """Example of using response_format at creation time and runtime.""" - - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - # Create agent with default response_format (WeatherInfo) - agent = await provider.create_agent( - name="StructuredReporter", - instructions="Return structured JSON based on the requested format.", - default_options={"response_format": WeatherInfo}, - ) - - # Request 1: Uses default response_format from agent creation - print("--- Request 1: Using default response_format (WeatherInfo) ---") - query1 = "What's the weather like in Paris today?" - print(f"User: {query1}") - - result1 = await agent.run(query1) - - try: - weather = result1.value - print("Agent:") - print(f" Location: {weather.location}") - print(f" Temperature: {weather.temperature}") - print(f" Conditions: {weather.conditions}") - print(f" Recommendation: {weather.recommendation}") - except Exception: - print(f"Failed to parse response: {result1.text}") - - # Request 2: Override response_format at runtime with CityInfo - print("\n--- Request 2: Runtime override with CityInfo ---") - query2 = "Tell me about Tokyo." - print(f"User: {query2}") - - result2 = await agent.run(query2, options={"response_format": CityInfo}) - - try: - city = result2.value - print("Agent:") - print(f" City: {city.city_name}") - print(f" Population: {city.population}") - print(f" Country: {city.country}") - except Exception: - print(f"Failed to parse response: {result2.text}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_session.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_session.py deleted file mode 100644 index 7ea7e4b5db..0000000000 --- a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_session.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from random import randint -from typing import Annotated - -from agent_framework import AgentSession, tool -from agent_framework.azure import AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure AI Agent with Session Management Example - -This sample demonstrates session management with Azure AI Agents, comparing -automatic session creation with explicit session management for persistent context. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def example_with_automatic_session_creation() -> None: - """Example showing automatic session creation (service-managed session).""" - print("=== Automatic Session Creation Example ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="WeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - # First conversation - no session provided, will be created automatically - first_query = "What's the weather like in Seattle?" - print(f"User: {first_query}") - first_result = await agent.run(first_query) - print(f"Agent: {first_result.text}") - - # Second conversation - still no session provided, will create another new session - second_query = "What was the last city I asked about?" - print(f"\nUser: {second_query}") - second_result = await agent.run(second_query) - print(f"Agent: {second_result.text}") - print("Note: Each call creates a separate session, so the agent doesn't remember previous context.\n") - - -async def example_with_session_persistence() -> None: - """Example showing session persistence across multiple conversations.""" - print("=== Session Persistence Example ===") - print("Using the same session across multiple conversations to maintain context.\n") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="WeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - # Create a new session that will be reused - session = agent.create_session() - - # First conversation - first_query = "What's the weather like in Tokyo?" - print(f"User: {first_query}") - first_result = await agent.run(first_query, session=session) - print(f"Agent: {first_result.text}") - - # Second conversation using the same session - maintains context - second_query = "How about London?" - print(f"\nUser: {second_query}") - second_result = await agent.run(second_query, session=session) - print(f"Agent: {second_result.text}") - - # Third conversation - agent should remember both previous cities - third_query = "Which of the cities I asked about has better weather?" - print(f"\nUser: {third_query}") - third_result = await agent.run(third_query, session=session) - print(f"Agent: {third_result.text}") - print("Note: The agent remembers context from previous messages in the same session.\n") - - -async def example_with_existing_session_id() -> None: - """Example showing how to work with an existing session ID from the service.""" - print("=== Existing Session ID Example ===") - print("Using a specific session ID to continue an existing conversation.\n") - - # First, create a conversation and capture the session ID - existing_session_id = None - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="WeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - # Start a conversation and get the session ID - session = agent.create_session() - first_query = "What's the weather in Paris?" - print(f"User: {first_query}") - first_result = await agent.run(first_query, session=session) - print(f"Agent: {first_result.text}") - - # The session ID is set after the first response - existing_session_id = session.service_session_id - print(f"Session ID: {existing_session_id}") - - if existing_session_id: - print("\n--- Continuing with the same session ID in a new agent instance ---") - - # Create a new provider and agent but use the existing session ID - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - agent = await provider.create_agent( - name="WeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) - - # Create a session with the existing ID - session = AgentSession(service_session_id=existing_session_id) - - second_query = "What was the last city I asked about?" - print(f"User: {second_query}") - second_result = await agent.run(second_query, session=session) - print(f"Agent: {second_result.text}") - print("Note: The agent continues the conversation from the previous session.\n") - - -async def main() -> None: - print("=== Azure AI Chat Client Agent Session Management Examples ===\n") - - await example_with_automatic_session_creation() - await example_with_session_persistence() - await example_with_existing_session_id() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_openai/README.md b/python/samples/02-agents/providers/azure_openai/README.md deleted file mode 100644 index 6971183ccf..0000000000 --- a/python/samples/02-agents/providers/azure_openai/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Azure OpenAI Agent Examples - -This folder contains examples demonstrating different ways to create and use agents with the different Azure OpenAI chat client from the `agent_framework.azure` package. - -## Examples - -| File | Description | -|------|-------------| -| [`azure_assistants_basic.py`](azure_assistants_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIAssistantsClient`. Shows both streaming and non-streaming responses with automatic assistant creation and cleanup. | -| [`azure_assistants_with_code_interpreter.py`](azure_assistants_with_code_interpreter.py) | Shows how to use `AzureOpenAIAssistantsClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | -| [`azure_assistants_with_existing_assistant.py`](azure_assistants_with_existing_assistant.py) | Shows how to work with a pre-existing assistant by providing the assistant ID to the Azure Assistants client. Demonstrates proper cleanup of manually created assistants. | -| [`azure_assistants_with_explicit_settings.py`](azure_assistants_with_explicit_settings.py) | Shows how to initialize an agent with a specific assistants client, configuring settings explicitly including endpoint and deployment name. | -| [`azure_assistants_with_function_tools.py`](azure_assistants_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | -| [`azure_assistants_with_session.py`](azure_assistants_with_session.py) | Demonstrates session management with Azure agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | -| [`azure_chat_client_basic.py`](azure_chat_client_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIChatClient`. Shows both streaming and non-streaming responses for chat-based interactions with Azure OpenAI models. | -| [`azure_chat_client_with_explicit_settings.py`](azure_chat_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific chat client, configuring settings explicitly including endpoint and deployment name. | -| [`azure_chat_client_with_function_tools.py`](azure_chat_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | -| [`azure_chat_client_with_session.py`](azure_chat_client_with_session.py) | Demonstrates session management with Azure agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | -| [`azure_responses_client_basic.py`](azure_responses_client_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with Azure OpenAI models. | -| [`azure_responses_client_code_interpreter_files.py`](azure_responses_client_code_interpreter_files.py) | Demonstrates using `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with file uploads for data analysis. Shows how to create, upload, and analyze CSV files using Python code execution with Azure OpenAI Responses. | -| [`azure_responses_client_image_analysis.py`](azure_responses_client_image_analysis.py) | Shows how to use Azure OpenAI Responses for image analysis and vision tasks. Demonstrates multi-modal messages combining text and image content using remote URLs. | -| [`azure_responses_client_with_code_interpreter.py`](azure_responses_client_with_code_interpreter.py) | Shows how to use `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | -| [`azure_responses_client_with_explicit_settings.py`](azure_responses_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific responses client, configuring settings explicitly including endpoint and deployment name. | -| [`azure_responses_client_with_file_search.py`](azure_responses_client_with_file_search.py) | Demonstrates using `AzureOpenAIResponsesClient.get_file_search_tool()` with Azure OpenAI Responses Client for direct document-based question answering and information retrieval from vector stores. | -| [`azure_responses_client_with_foundry.py`](azure_responses_client_with_foundry.py) | Shows how to create an agent using an Azure AI Foundry project endpoint instead of a direct Azure OpenAI endpoint. Requires the `azure-ai-projects` package. | -| [`azure_responses_client_with_function_tools.py`](azure_responses_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | -| [`azure_responses_client_with_hosted_mcp.py`](azure_responses_client_with_hosted_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with hosted Model Context Protocol (MCP) servers using `AzureOpenAIResponsesClient.get_mcp_tool()` for extended functionality. | -| [`azure_responses_client_with_local_mcp.py`](azure_responses_client_with_local_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with local Model Context Protocol (MCP) servers using MCPStreamableHTTPTool for extended functionality. | -| [`azure_responses_client_with_session.py`](azure_responses_client_with_session.py) | Demonstrates session management with Azure agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | - -## Environment Variables - -Make sure to set the following environment variables before running the examples: - -- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint -- `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`: The name of your Azure OpenAI chat model deployment -- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: The name of your Azure OpenAI Responses deployment - -For the Foundry project sample (`azure_responses_client_with_foundry.py`), also set: -- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint - -Optionally, you can set: -- `AZURE_OPENAI_API_VERSION`: The API version to use (default is `2024-02-15-preview`) -- `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (if not using `AzureCliCredential`) -- `AZURE_OPENAI_BASE_URL`: Your Azure OpenAI base URL (if different from the endpoint) - -## Authentication - -All examples use `AzureCliCredential` for authentication. Run `az login` in your terminal before running the examples, or replace `AzureCliCredential` with your preferred authentication method. - -## Required role-based access control (RBAC) roles - -To access the Azure OpenAI API, your Azure account or service principal needs one of the following RBAC roles assigned to the Azure OpenAI resource: - -- **Cognitive Services OpenAI User**: Provides read access to Azure OpenAI resources and the ability to call the inference APIs. This is the minimum role required for running these examples. -- **Cognitive Services OpenAI Contributor**: Provides full access to Azure OpenAI resources, including the ability to create, update, and delete deployments and models. - -For most scenarios, the **Cognitive Services OpenAI User** role is sufficient. You can assign this role through the Azure portal under the Azure OpenAI resource's "Access control (IAM)" section. - -For more detailed information about Azure OpenAI RBAC roles, see: [Role-based access control for Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/role-based-access-control) diff --git a/python/samples/02-agents/providers/azure_openai/azure_assistants_basic.py b/python/samples/02-agents/providers/azure_openai/azure_assistants_basic.py deleted file mode 100644 index a1e61be0e8..0000000000 --- a/python/samples/02-agents/providers/azure_openai/azure_assistants_basic.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureOpenAIAssistantsClient -from azure.identity import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure OpenAI Assistants Basic Example - -This sample demonstrates basic usage of AzureOpenAIAssistantsClient with automatic -assistant lifecycle management, showing both streaming and non-streaming responses. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def non_streaming_example() -> None: - """Example of non-streaming response (get the complete result at once).""" - print("=== Non-streaming Response Example ===") - - # Since no assistant ID is provided, the assistant will be automatically created - # and deleted after getting a response - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with AzureOpenAIAssistantsClient(credential=AzureCliCredential()).as_agent( - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent: - query = "What's the weather like in Seattle?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") - - -async def streaming_example() -> None: - """Example of streaming response (get results as they are generated).""" - print("=== Streaming Response Example ===") - - # Since no assistant ID is provided, the assistant will be automatically created - # and deleted after getting a response - async with AzureOpenAIAssistantsClient(credential=AzureCliCredential()).as_agent( - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent: - query = "What's the weather like in Portland?" - print(f"User: {query}") - print("Agent: ", end="", flush=True) - async for chunk in agent.run(query, stream=True): - if chunk.text: - print(chunk.text, end="", flush=True) - print("\n") - - -async def main() -> None: - print("=== Basic Azure OpenAI Assistants Chat Client Agent Example ===") - - await non_streaming_example() - await streaming_example() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_openai/azure_assistants_with_code_interpreter.py b/python/samples/02-agents/providers/azure_openai/azure_assistants_with_code_interpreter.py deleted file mode 100644 index c1bbd54e20..0000000000 --- a/python/samples/02-agents/providers/azure_openai/azure_assistants_with_code_interpreter.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework import Agent, AgentResponseUpdate, ChatResponseUpdate -from agent_framework.azure import AzureOpenAIAssistantsClient -from dotenv import load_dotenv -from openai.types.beta.threads.runs import ( - CodeInterpreterToolCallDelta, - RunStepDelta, - RunStepDeltaEvent, - ToolCallDeltaObject, -) -from openai.types.beta.threads.runs.code_interpreter_tool_call_delta import CodeInterpreter - -# Load environment variables from .env file -load_dotenv() - -""" -Azure OpenAI Assistants with Code Interpreter Example - -This sample demonstrates using get_code_interpreter_tool() with Azure OpenAI Assistants -for Python code execution and mathematical problem solving. -""" - - -def get_code_interpreter_chunk(chunk: AgentResponseUpdate) -> str | None: - """Helper method to access code interpreter data.""" - if ( - isinstance(chunk.raw_representation, ChatResponseUpdate) - and isinstance(chunk.raw_representation.raw_representation, RunStepDeltaEvent) - and isinstance(chunk.raw_representation.raw_representation.delta, RunStepDelta) - and isinstance(chunk.raw_representation.raw_representation.delta.step_details, ToolCallDeltaObject) - and chunk.raw_representation.raw_representation.delta.step_details.tool_calls - ): - for tool_call in chunk.raw_representation.raw_representation.delta.step_details.tool_calls: - if ( - isinstance(tool_call, CodeInterpreterToolCallDelta) - and isinstance(tool_call.code_interpreter, CodeInterpreter) - and tool_call.code_interpreter.input is not None - ): - return tool_call.code_interpreter.input - return None - - -async def main() -> None: - """Example showing how to use the code interpreter tool with Azure OpenAI Assistants.""" - print("=== Azure OpenAI Assistants Agent with Code Interpreter Example ===") - - # Create code interpreter tool using static method - client = AzureOpenAIAssistantsClient() - code_interpreter_tool = client.get_code_interpreter_tool() - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with Agent( - client=client, - instructions="You are a helpful assistant that can write and execute Python code to solve problems.", - tools=[code_interpreter_tool], - ) as agent: - query = "What is current datetime?" - print(f"User: {query}") - print("Agent: ", end="", flush=True) - generated_code = "" - async for chunk in agent.run(query, stream=True): - if chunk.text: - print(chunk.text, end="", flush=True) - code_interpreter_chunk = get_code_interpreter_chunk(chunk) - if code_interpreter_chunk is not None: - generated_code += code_interpreter_chunk - - print(f"\nGenerated code:\n{generated_code}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_openai/azure_assistants_with_existing_assistant.py b/python/samples/02-agents/providers/azure_openai/azure_assistants_with_existing_assistant.py deleted file mode 100644 index e110d52cb8..0000000000 --- a/python/samples/02-agents/providers/azure_openai/azure_assistants_with_existing_assistant.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os -from random import randint -from typing import Annotated - -from agent_framework import Agent, tool -from agent_framework.azure import AzureOpenAIAssistantsClient -from azure.identity import AzureCliCredential, get_bearer_token_provider -from dotenv import load_dotenv -from openai import AsyncAzureOpenAI -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure OpenAI Assistants with Existing Assistant Example - -This sample demonstrates working with pre-existing Azure OpenAI Assistants -using existing assistant IDs rather than creating new ones. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def main() -> None: - print("=== Azure OpenAI Assistants Chat Client with Existing Assistant ===") - - token_provider = get_bearer_token_provider(AzureCliCredential(), "https://cognitiveservices.azure.com/.default") - - client = AsyncAzureOpenAI( - azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], - azure_ad_token_provider=token_provider, - api_version="2025-01-01-preview", - ) - - # Create an assistant that will persist - created_assistant = await client.beta.assistants.create( - model=os.environ["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], name="WeatherAssistant" - ) - - try: - async with Agent( - client=AzureOpenAIAssistantsClient(async_client=client, assistant_id=created_assistant.id), - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent: - result = await agent.run("What's the weather like in Tokyo?") - print(f"Result: {result}\n") - finally: - # Clean up the assistant manually - await client.beta.assistants.delete(created_assistant.id) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_openai/azure_assistants_with_explicit_settings.py b/python/samples/02-agents/providers/azure_openai/azure_assistants_with_explicit_settings.py deleted file mode 100644 index 8d4ca0bc7f..0000000000 --- a/python/samples/02-agents/providers/azure_openai/azure_assistants_with_explicit_settings.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import os -from random import randint -from typing import Annotated - -from agent_framework import tool -from agent_framework.azure import AzureOpenAIAssistantsClient -from azure.identity import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure OpenAI Assistants with Explicit Settings Example - -This sample demonstrates creating Azure OpenAI Assistants with explicit configuration -settings rather than relying on environment variable defaults. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def main() -> None: - print("=== Azure Assistants Client with Explicit Settings ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with AzureOpenAIAssistantsClient( - endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], - deployment_name=os.environ["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ).as_agent( - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent: - result = await agent.run("What's the weather like in New York?") - print(f"Result: {result}\n") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_openai/azure_assistants_with_function_tools.py b/python/samples/02-agents/providers/azure_openai/azure_assistants_with_function_tools.py deleted file mode 100644 index 1543a25f42..0000000000 --- a/python/samples/02-agents/providers/azure_openai/azure_assistants_with_function_tools.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from datetime import datetime, timezone -from random import randint -from typing import Annotated - -from agent_framework import Agent, tool -from agent_framework.azure import AzureOpenAIAssistantsClient -from azure.identity import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure OpenAI Assistants with Function Tools Example - -This sample demonstrates function tool integration with Azure OpenAI Assistants, -showing both agent-level and query-level tool configuration patterns. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -@tool(approval_mode="never_require") -def get_time() -> str: - """Get the current UTC time.""" - current_time = datetime.now(timezone.utc) - return f"The current UTC time is {current_time.strftime('%Y-%m-%d %H:%M:%S')}." - - -async def tools_on_agent_level() -> None: - """Example showing tools defined when creating the agent.""" - print("=== Tools Defined on Agent Level ===") - - # Tools are provided when creating the agent - # The agent can use these tools for any query during its lifetime - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with Agent( - client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), - instructions="You are a helpful assistant that can provide weather and time information.", - tools=[get_weather, get_time], # Tools defined at agent creation - ) as agent: - # First query - agent can use weather tool - query1 = "What's the weather like in New York?" - print(f"User: {query1}") - result1 = await agent.run(query1) - print(f"Agent: {result1}\n") - - # Second query - agent can use time tool - query2 = "What's the current UTC time?" - print(f"User: {query2}") - result2 = await agent.run(query2) - print(f"Agent: {result2}\n") - - # Third query - agent can use both tools if needed - query3 = "What's the weather in London and what's the current UTC time?" - print(f"User: {query3}") - result3 = await agent.run(query3) - print(f"Agent: {result3}\n") - - -async def tools_on_run_level() -> None: - """Example showing tools passed to the run method.""" - print("=== Tools Passed to Run Method ===") - - # Agent created without tools - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with Agent( - client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), - instructions="You are a helpful assistant.", - # No tools defined here - ) as agent: - # First query with weather tool - query1 = "What's the weather like in Seattle?" - print(f"User: {query1}") - result1 = await agent.run(query1, tools=[get_weather]) # Tool passed to run method - print(f"Agent: {result1}\n") - - # Second query with time tool - query2 = "What's the current UTC time?" - print(f"User: {query2}") - result2 = await agent.run(query2, tools=[get_time]) # Different tool for this query - print(f"Agent: {result2}\n") - - # Third query with multiple tools - query3 = "What's the weather in Chicago and what's the current UTC time?" - print(f"User: {query3}") - result3 = await agent.run(query3, tools=[get_weather, get_time]) # Multiple tools - print(f"Agent: {result3}\n") - - -async def mixed_tools_example() -> None: - """Example showing both agent-level tools and run-method tools.""" - print("=== Mixed Tools Example (Agent + Run Method) ===") - - # Agent created with some base tools - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with Agent( - client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), - instructions="You are a comprehensive assistant that can help with various information requests.", - tools=[get_weather], # Base tool available for all queries - ) as agent: - # Query using both agent tool and additional run-method tools - query = "What's the weather in Denver and what's the current UTC time?" - print(f"User: {query}") - - # Agent has access to get_weather (from creation) + additional tools from run method - result = await agent.run( - query, - tools=[get_time], # Additional tools for this specific query - ) - print(f"Agent: {result}\n") - - -async def main() -> None: - print("=== Azure OpenAI Assistants Chat Client Agent with Function Tools Examples ===\n") - - await tools_on_agent_level() - await tools_on_run_level() - await mixed_tools_example() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/azure_openai/azure_assistants_with_session.py b/python/samples/02-agents/providers/azure_openai/azure_assistants_with_session.py deleted file mode 100644 index 4f3e952d86..0000000000 --- a/python/samples/02-agents/providers/azure_openai/azure_assistants_with_session.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from random import randint -from typing import Annotated - -from agent_framework import Agent, AgentSession, tool -from agent_framework.azure import AzureOpenAIAssistantsClient -from azure.identity import AzureCliCredential -from dotenv import load_dotenv -from pydantic import Field - -# Load environment variables from .env file -load_dotenv() - -""" -Azure OpenAI Assistants with Session Management Example - -This sample demonstrates session management with Azure OpenAI Assistants, comparing -automatic session creation with explicit session management for persistent context. -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def example_with_automatic_session_creation() -> None: - """Example showing automatic session creation (service-managed session).""" - print("=== Automatic Session Creation Example ===") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with Agent( - client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent: - # First conversation - no session provided, will be created automatically - query1 = "What's the weather like in Seattle?" - print(f"User: {query1}") - result1 = await agent.run(query1) - print(f"Agent: {result1.text}") - - # Second conversation - still no session provided, will create another new session - query2 = "What was the last city I asked about?" - print(f"\nUser: {query2}") - result2 = await agent.run(query2) - print(f"Agent: {result2.text}") - print("Note: Each call creates a separate session, so the agent doesn't remember previous context.\n") - - -async def example_with_session_persistence() -> None: - """Example showing session persistence across multiple conversations.""" - print("=== Session Persistence Example ===") - print("Using the same session across multiple conversations to maintain context.\n") - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with Agent( - client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent: - # Create a new session that will be reused - session = agent.create_session() - - # First conversation - query1 = "What's the weather like in Tokyo?" - print(f"User: {query1}") - result1 = await agent.run(query1, session=session) - print(f"Agent: {result1.text}") - - # Second conversation using the same session - maintains context - query2 = "How about London?" - print(f"\nUser: {query2}") - result2 = await agent.run(query2, session=session) - print(f"Agent: {result2.text}") - - # Third conversation - agent should remember both previous cities - query3 = "Which of the cities I asked about has better weather?" - print(f"\nUser: {query3}") - result3 = await agent.run(query3, session=session) - print(f"Agent: {result3.text}") - print("Note: The agent remembers context from previous messages in the same session.\n") - - -async def example_with_existing_session_id() -> None: - """Example showing how to work with an existing session ID from the service.""" - print("=== Existing Session ID Example ===") - print("Using a specific session ID to continue an existing conversation.\n") - - # First, create a conversation and capture the session ID - existing_session_id = None - - # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred - # authentication option. - async with Agent( - client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent: - # Start a conversation and get the session ID - session = agent.create_session() - query1 = "What's the weather in Paris?" - print(f"User: {query1}") - result1 = await agent.run(query1, session=session) - print(f"Agent: {result1.text}") - - # The session ID is set after the first response - existing_session_id = session.service_session_id - print(f"Session ID: {existing_session_id}") - - if existing_session_id: - print("\n--- Continuing with the same session ID in a new agent instance ---") - - # Create a new agent instance but use the existing session ID - async with Agent( - client=AzureOpenAIAssistantsClient(thread_id=existing_session_id, credential=AzureCliCredential()), - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent: - # Create a session with the existing ID - session = AgentSession(service_session_id=existing_session_id) - - query2 = "What was the last city I asked about?" - print(f"User: {query2}") - result2 = await agent.run(query2, session=session) - print(f"Agent: {result2.text}") - print("Note: The agent continues the conversation from the previous session.\n") - - -async def main() -> None: - print("=== Azure OpenAI Assistants Chat Client Agent Session Management Examples ===\n") - - await example_with_automatic_session_creation() - await example_with_session_persistence() - await example_with_existing_session_id() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/providers/foundry_local/foundry_local_agent.py b/python/samples/02-agents/providers/foundry_local/foundry_local_agent.py index 0ea0c15bc2..e065967894 100644 --- a/python/samples/02-agents/providers/foundry_local/foundry_local_agent.py +++ b/python/samples/02-agents/providers/foundry_local/foundry_local_agent.py @@ -59,7 +59,7 @@ async def streaming_example(agent: Agent) -> None: async def main() -> None: print("=== Basic Foundry Local Client Agent Example ===") - client = FoundryLocalClient(model_id="phi-4-mini") + client = FoundryLocalClient(model="phi-4-mini") print(f"Client Model ID: {client.model_id}\n") print("Other available models (tool calling supported only):") for model in client.manager.list_catalog_models(): @@ -67,7 +67,7 @@ async def main() -> None: print( f"- {model.alias} for {model.task} - id={model.id} - {(model.file_size_mb / 1000):.2f} GB - {model.license}" ) - agent = client.as_agent( + agent = Agent(client=client, name="LocalAgent", instructions="You are a helpful agent.", tools=get_weather, diff --git a/python/samples/02-agents/providers/ollama/ollama_agent_basic.py b/python/samples/02-agents/providers/ollama/ollama_agent_basic.py index 28cad484c1..3652b069c4 100644 --- a/python/samples/02-agents/providers/ollama/ollama_agent_basic.py +++ b/python/samples/02-agents/providers/ollama/ollama_agent_basic.py @@ -3,7 +3,7 @@ import asyncio from datetime import datetime -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.ollama import OllamaChatClient from dotenv import load_dotenv @@ -36,7 +36,8 @@ async def non_streaming_example() -> None: """Example of non-streaming response (get the complete result at once).""" print("=== Non-streaming Response Example ===") - agent = OllamaChatClient().as_agent( + agent = Agent( + client=OllamaChatClient(), name="TimeAgent", instructions="You are a helpful time agent answer in one sentence.", tools=get_time, @@ -52,7 +53,8 @@ async def streaming_example() -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") - agent = OllamaChatClient().as_agent( + agent = Agent( + client=OllamaChatClient(), name="TimeAgent", instructions="You are a helpful time agent answer in one sentence.", tools=get_time, diff --git a/python/samples/02-agents/providers/ollama/ollama_agent_reasoning.py b/python/samples/02-agents/providers/ollama/ollama_agent_reasoning.py index 97c24086a0..b23727311b 100644 --- a/python/samples/02-agents/providers/ollama/ollama_agent_reasoning.py +++ b/python/samples/02-agents/providers/ollama/ollama_agent_reasoning.py @@ -2,6 +2,7 @@ import asyncio +from agent_framework import Agent from agent_framework.ollama import OllamaChatClient from dotenv import load_dotenv @@ -24,7 +25,8 @@ async def main() -> None: print("=== Response Reasoning Example ===") - agent = OllamaChatClient().as_agent( + agent = Agent( + client=OllamaChatClient(), name="TimeAgent", instructions="You are a helpful agent answer in one sentence.", default_options={"think": True}, # Enable Reasoning on agent level diff --git a/python/samples/02-agents/providers/ollama/ollama_with_openai_chat_client.py b/python/samples/02-agents/providers/ollama/ollama_with_openai_chat_client.py index 4069128346..9671ee234f 100644 --- a/python/samples/02-agents/providers/ollama/ollama_with_openai_chat_client.py +++ b/python/samples/02-agents/providers/ollama/ollama_with_openai_chat_client.py @@ -5,7 +5,7 @@ from random import randint from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIChatClient from dotenv import load_dotenv @@ -41,14 +41,16 @@ async def non_streaming_example() -> None: """Example of non-streaming response (get the complete result at once).""" print("=== Non-streaming Response Example ===") - agent = OpenAIChatClient( + _client = OpenAIChatClient( api_key="ollama", # Just a placeholder, Ollama doesn't require API key base_url=os.getenv("OLLAMA_ENDPOINT"), - model_id=os.getenv("OLLAMA_MODEL"), - ).as_agent( + model=os.getenv("OLLAMA_MODEL"), + ) + agent = Agent( + client=_client, name="WeatherAgent", instructions="You are a helpful weather agent.", - tools=get_weather, + tools=[get_weather], ) query = "What's the weather like in Seattle?" @@ -61,14 +63,16 @@ async def streaming_example() -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") - agent = OpenAIChatClient( + _client = OpenAIChatClient( api_key="ollama", # Just a placeholder, Ollama doesn't require API key base_url=os.getenv("OLLAMA_ENDPOINT"), - model_id=os.getenv("OLLAMA_MODEL"), - ).as_agent( + model=os.getenv("OLLAMA_MODEL"), + ) + agent = Agent( + client=_client, name="WeatherAgent", instructions="You are a helpful weather agent.", - tools=get_weather, + tools=[get_weather], ) query = "What's the weather like in Portland?" diff --git a/python/samples/02-agents/providers/openai/openai_assistants_basic.py b/python/samples/02-agents/providers/openai/openai_assistants_basic.py index 3631691474..5901b6ef38 100644 --- a/python/samples/02-agents/providers/openai/openai_assistants_basic.py +++ b/python/samples/02-agents/providers/openai/openai_assistants_basic.py @@ -44,7 +44,7 @@ async def non_streaming_example() -> None: # Create a new assistant via the provider agent = await provider.create_agent( name="WeatherAssistant", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful weather agent.", tools=[get_weather], ) @@ -69,7 +69,7 @@ async def streaming_example() -> None: # Create a new assistant via the provider agent = await provider.create_agent( name="WeatherAssistant", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful weather agent.", tools=[get_weather], ) diff --git a/python/samples/02-agents/providers/openai/openai_assistants_provider_methods.py b/python/samples/02-agents/providers/openai/openai_assistants_provider_methods.py index f479177928..0cc9d33f73 100644 --- a/python/samples/02-agents/providers/openai/openai_assistants_provider_methods.py +++ b/python/samples/02-agents/providers/openai/openai_assistants_provider_methods.py @@ -5,7 +5,7 @@ from random import randint from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIAssistantProvider from dotenv import load_dotenv from openai import AsyncOpenAI @@ -46,7 +46,7 @@ async def create_agent_example() -> None: ): agent = await provider.create_agent( name="WeatherAssistant", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful weather assistant.", tools=[get_weather], ) @@ -69,7 +69,7 @@ async def get_agent_example() -> None: ): # Create an assistant directly with SDK (simulating pre-existing assistant) sdk_assistant = await client.beta.assistants.create( - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), name="ExistingAssistant", instructions="You always respond with 'Hello!'", ) @@ -86,7 +86,7 @@ async def get_agent_example() -> None: async def as_agent_example() -> None: - """Wrap an SDK Assistant object using provider.as_agent().""" + """Wrap an SDK Assistant object using Agent(client=provider, ...).""" print("\n--- as_agent() ---") async with ( @@ -95,14 +95,14 @@ async def as_agent_example() -> None: ): # Create assistant using SDK sdk_assistant = await client.beta.assistants.create( - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), name="WrappedAssistant", instructions="You respond with poetry.", ) try: # Wrap synchronously (no HTTP call) - agent = provider.as_agent(sdk_assistant) + agent = Agent(client=provider, agent=sdk_assistant) print(f"Wrapped: {agent.name} (ID: {agent.id})") result = await agent.run("Tell me about the sunset.") @@ -121,14 +121,14 @@ async def multiple_agents_example() -> None: ): weather_agent = await provider.create_agent( name="WeatherSpecialist", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a weather specialist.", tools=[get_weather], ) greeter_agent = await provider.create_agent( name="GreeterAgent", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a friendly greeter.", ) diff --git a/python/samples/02-agents/providers/openai/openai_assistants_with_code_interpreter.py b/python/samples/02-agents/providers/openai/openai_assistants_with_code_interpreter.py index 9f43996fc4..044804e3c5 100644 --- a/python/samples/02-agents/providers/openai/openai_assistants_with_code_interpreter.py +++ b/python/samples/02-agents/providers/openai/openai_assistants_with_code_interpreter.py @@ -55,7 +55,7 @@ async def main() -> None: agent = await provider.create_agent( name="CodeHelper", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful assistant that can write and execute Python code to solve problems.", tools=[chat_client.get_code_interpreter_tool()], ) diff --git a/python/samples/02-agents/providers/openai/openai_assistants_with_existing_assistant.py b/python/samples/02-agents/providers/openai/openai_assistants_with_existing_assistant.py index 5716779558..563dbb38a4 100644 --- a/python/samples/02-agents/providers/openai/openai_assistants_with_existing_assistant.py +++ b/python/samples/02-agents/providers/openai/openai_assistants_with_existing_assistant.py @@ -5,7 +5,7 @@ from random import randint from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIAssistantProvider from dotenv import load_dotenv from openai import AsyncOpenAI @@ -43,7 +43,7 @@ async def example_get_agent_by_id() -> None: # Create an assistant via SDK (simulating an existing assistant) created_assistant = await client.beta.assistants.create( - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), name="WeatherAssistant", tools=[ { @@ -86,7 +86,7 @@ async def example_as_agent_wrap_sdk_object() -> None: # Create and fetch an assistant via SDK created_assistant = await client.beta.assistants.create( - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), name="SimpleAssistant", instructions="You are a friendly assistant.", ) @@ -94,8 +94,9 @@ async def example_as_agent_wrap_sdk_object() -> None: try: # Use as_agent() to wrap the SDK object - agent = provider.as_agent( - created_assistant, + agent = Agent( + client=provider, + agent=created_assistant, instructions="You are an extremely helpful assistant. Be enthusiastic!", ) diff --git a/python/samples/02-agents/providers/openai/openai_assistants_with_explicit_settings.py b/python/samples/02-agents/providers/openai/openai_assistants_with_explicit_settings.py index 24dfa0e827..d7adef004c 100644 --- a/python/samples/02-agents/providers/openai/openai_assistants_with_explicit_settings.py +++ b/python/samples/02-agents/providers/openai/openai_assistants_with_explicit_settings.py @@ -43,7 +43,7 @@ async def main() -> None: agent = await provider.create_agent( name="WeatherAssistant", - model=os.environ["OPENAI_CHAT_MODEL_ID"], + model=os.environ["OPENAI_MODEL"], instructions="You are a helpful weather agent.", tools=[get_weather], ) diff --git a/python/samples/02-agents/providers/openai/openai_assistants_with_file_search.py b/python/samples/02-agents/providers/openai/openai_assistants_with_file_search.py index ba6de333c5..ad67986d4e 100644 --- a/python/samples/02-agents/providers/openai/openai_assistants_with_file_search.py +++ b/python/samples/02-agents/providers/openai/openai_assistants_with_file_search.py @@ -50,7 +50,7 @@ async def main() -> None: agent = await provider.create_agent( name="SearchAssistant", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful assistant that searches files in a knowledge base.", tools=[chat_client.get_file_search_tool()], ) diff --git a/python/samples/02-agents/providers/openai/openai_assistants_with_function_tools.py b/python/samples/02-agents/providers/openai/openai_assistants_with_function_tools.py index eebbb07b87..ffd64d9ca2 100644 --- a/python/samples/02-agents/providers/openai/openai_assistants_with_function_tools.py +++ b/python/samples/02-agents/providers/openai/openai_assistants_with_function_tools.py @@ -53,7 +53,7 @@ async def tools_on_agent_level() -> None: # The agent can use these tools for any query during its lifetime agent = await provider.create_agent( name="InfoAssistant", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) @@ -90,7 +90,7 @@ async def tools_on_run_level() -> None: # Agent created with base tools, additional tools can be passed at run time agent = await provider.create_agent( name="FlexibleAssistant", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful assistant.", tools=[get_weather], # Base tool ) @@ -127,7 +127,7 @@ async def mixed_tools_example() -> None: # Agent created with some base tools agent = await provider.create_agent( name="ComprehensiveAssistant", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) diff --git a/python/samples/02-agents/providers/openai/openai_assistants_with_response_format.py b/python/samples/02-agents/providers/openai/openai_assistants_with_response_format.py index 79dad58afe..740b36107d 100644 --- a/python/samples/02-agents/providers/openai/openai_assistants_with_response_format.py +++ b/python/samples/02-agents/providers/openai/openai_assistants_with_response_format.py @@ -50,7 +50,7 @@ async def main() -> None: # Create agent with default response_format (WeatherInfo) agent = await provider.create_agent( name="StructuredReporter", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="Return structured JSON based on the requested format.", default_options={"response_format": WeatherInfo}, ) diff --git a/python/samples/02-agents/providers/openai/openai_assistants_with_session.py b/python/samples/02-agents/providers/openai/openai_assistants_with_session.py index ba55904315..2259c5638d 100644 --- a/python/samples/02-agents/providers/openai/openai_assistants_with_session.py +++ b/python/samples/02-agents/providers/openai/openai_assistants_with_session.py @@ -43,7 +43,7 @@ async def example_with_automatic_session_creation() -> None: agent = await provider.create_agent( name="WeatherAssistant", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful weather agent.", tools=[get_weather], ) @@ -75,7 +75,7 @@ async def example_with_session_persistence() -> None: agent = await provider.create_agent( name="WeatherAssistant", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful weather agent.", tools=[get_weather], ) @@ -120,7 +120,7 @@ async def example_with_existing_session_id() -> None: agent = await provider.create_agent( name="WeatherAssistant", - model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), + model=os.environ.get("OPENAI_MODEL", "gpt-4"), instructions="You are a helpful weather agent.", tools=[get_weather], ) diff --git a/python/samples/02-agents/providers/openai/openai_chat_client_basic.py b/python/samples/02-agents/providers/openai/openai_chat_client_basic.py index 5b36460427..d2834fe1e9 100644 --- a/python/samples/02-agents/providers/openai/openai_chat_client_basic.py +++ b/python/samples/02-agents/providers/openai/openai_chat_client_basic.py @@ -4,7 +4,7 @@ from random import randint from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIChatClient from dotenv import load_dotenv @@ -35,7 +35,8 @@ async def non_streaming_example() -> None: """Example of non-streaming response (get the complete result at once).""" print("=== Non-streaming Response Example ===") - agent = OpenAIChatClient().as_agent( + agent = Agent( + client=OpenAIChatClient(), name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, @@ -51,7 +52,8 @@ async def streaming_example() -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") - agent = OpenAIChatClient().as_agent( + agent = Agent( + client=OpenAIChatClient(), name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, diff --git a/python/samples/02-agents/providers/openai/openai_chat_client_with_explicit_settings.py b/python/samples/02-agents/providers/openai/openai_chat_client_with_explicit_settings.py index 4601557d40..af439f7a05 100644 --- a/python/samples/02-agents/providers/openai/openai_chat_client_with_explicit_settings.py +++ b/python/samples/02-agents/providers/openai/openai_chat_client_with_explicit_settings.py @@ -5,7 +5,7 @@ from random import randint from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIChatClient from dotenv import load_dotenv from pydantic import Field @@ -36,10 +36,12 @@ def get_weather( async def main() -> None: print("=== OpenAI Chat Client with Explicit Settings ===") - agent = OpenAIChatClient( - model_id=os.environ["OPENAI_CHAT_MODEL_ID"], + _client = OpenAIChatClient( + model=os.environ["OPENAI_MODEL"], api_key=os.environ["OPENAI_API_KEY"], - ).as_agent( + ) + + agent = Agent(client=_client, instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/02-agents/providers/openai/openai_chat_client_with_local_mcp.py b/python/samples/02-agents/providers/openai/openai_chat_client_with_local_mcp.py index bb06046fa3..00057dd76d 100644 --- a/python/samples/02-agents/providers/openai/openai_chat_client_with_local_mcp.py +++ b/python/samples/02-agents/providers/openai/openai_chat_client_with_local_mcp.py @@ -59,7 +59,8 @@ async def mcp_tools_on_agent_level() -> None: # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime # The agent will connect to the MCP server through its context manager. - async with OpenAIChatClient().as_agent( + async with Agent( + client=OpenAIChatClient(), name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", tools=MCPStreamableHTTPTool( # Tools defined at agent creation diff --git a/python/samples/02-agents/providers/openai/openai_chat_client_with_runtime_json_schema.py b/python/samples/02-agents/providers/openai/openai_chat_client_with_runtime_json_schema.py index 8045da9e81..ba21d0a325 100644 --- a/python/samples/02-agents/providers/openai/openai_chat_client_with_runtime_json_schema.py +++ b/python/samples/02-agents/providers/openai/openai_chat_client_with_runtime_json_schema.py @@ -3,6 +3,7 @@ import asyncio import json +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions from dotenv import load_dotenv @@ -36,7 +37,8 @@ async def non_streaming_example() -> None: print("=== Non-streaming runtime JSON schema example ===") - agent = OpenAIChatClient[OpenAIChatOptions]().as_agent( + agent = Agent( + client=OpenAIChatClient[OpenAIChatOptions](), name="RuntimeSchemaAgent", instructions="Return only JSON that matches the provided schema. Do not add commentary.", ) @@ -69,7 +71,8 @@ async def non_streaming_example() -> None: async def streaming_example() -> None: print("=== Streaming runtime JSON schema example ===") - agent = OpenAIChatClient().as_agent( + agent = Agent( + client=OpenAIChatClient(), name="RuntimeSchemaAgent", instructions="Return only JSON that matches the provided schema. Do not add commentary.", ) diff --git a/python/samples/02-agents/providers/openai/openai_chat_client_with_web_search.py b/python/samples/02-agents/providers/openai/openai_chat_client_with_web_search.py index 384bce5aa8..623dc25f43 100644 --- a/python/samples/02-agents/providers/openai/openai_chat_client_with_web_search.py +++ b/python/samples/02-agents/providers/openai/openai_chat_client_with_web_search.py @@ -18,7 +18,7 @@ async def main() -> None: - client = OpenAIChatClient(model_id="gpt-4o-search-preview") + client = OpenAIChatClient(model="gpt-4o-search-preview") # Create web search tool with location context web_search_tool = client.get_web_search_tool( diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_image_analysis.py b/python/samples/02-agents/providers/openai/openai_responses_client_image_analysis.py index 572a487563..82fee38455 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_image_analysis.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_image_analysis.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import Content +from agent_framework import Agent, Content from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -21,7 +21,8 @@ async def main(): print("=== OpenAI Responses Agent with Image Analysis ===") # 1. Create an OpenAI Responses agent with vision capabilities - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="VisionAgent", instructions="You are a image analysist, you get a image and need to respond with what you see in the picture.", ) diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py b/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py index 6ed7a48fd0..abb6a24b29 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py @@ -6,7 +6,7 @@ import urllib.request as urllib_request from pathlib import Path -from agent_framework import Content +from agent_framework import Agent, Content from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -61,7 +61,7 @@ async def main() -> None: # Create an agent with customized image generation options client = OpenAIResponsesClient() - agent = client.as_agent( + agent = Agent(client=client, instructions="You are a helpful AI that can generate images.", tools=[ client.get_image_generation_tool( diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_reasoning.py b/python/samples/02-agents/providers/openai/openai_responses_client_reasoning.py index 4e83347fab..a4fc3849b8 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_reasoning.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_reasoning.py @@ -2,6 +2,7 @@ import asyncio +from agent_framework import Agent from agent_framework.openai import OpenAIResponsesClient, OpenAIResponsesOptions from dotenv import load_dotenv @@ -23,7 +24,8 @@ """ -agent = OpenAIResponsesClient[OpenAIResponsesOptions](model_id="gpt-5").as_agent( +agent = Agent( + client=OpenAIResponsesClient[OpenAIResponsesOptions](model_id="gpt-5"), name="MathHelper", instructions="You are a personal math tutor. When asked a math question, " "reason over how best to approach the problem and share your thought process.", diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_streaming_image_generation.py b/python/samples/02-agents/providers/openai/openai_responses_client_streaming_image_generation.py index d256445328..7aafd6f704 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_streaming_image_generation.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_streaming_image_generation.py @@ -6,23 +6,19 @@ from pathlib import Path import anyio -from agent_framework import Content +from agent_framework import Agent, Content from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() - """OpenAI Responses Client Streaming Image Generation Example - Demonstrates streaming partial image generation using OpenAI's image generation tool. Shows progressive image rendering with partial images for improved user experience. - Note: The number of partial images received depends on generation speed: - High quality/complex images: More partials (generation takes longer) - Low quality/simple images: Fewer partials (generation completes quickly) - You may receive fewer partial images than requested if generation is fast - Important: The final partial image IS the complete, full-quality image. Each partial represents a progressive refinement, with the last one being the finished result. """ @@ -35,7 +31,6 @@ async def save_image_from_data_uri(data_uri: str, filename: str) -> None: # Extract base64 data base64_data = data_uri.split(",", 1)[1] image_bytes = base64.b64decode(base64_data) - # Save to file await anyio.Path(filename).write_bytes(image_bytes) print(f" Saved: {filename} ({len(image_bytes) / 1024:.1f} KB)") @@ -46,10 +41,10 @@ async def save_image_from_data_uri(data_uri: str, filename: str) -> None: async def main(): """Demonstrate streaming image generation with partial images.""" print("=== OpenAI Streaming Image Generation Example ===\n") - # Create agent with streaming image generation enabled client = OpenAIResponsesClient() - agent = client.as_agent( + agent = Agent( + client=client, instructions="You are a helpful agent that can generate images.", tools=[ client.get_image_generation_tool( @@ -59,18 +54,14 @@ async def main(): ) ], ) - query = "Draw a beautiful sunset over a calm ocean with sailboats" print(f" User: {query}") print() - # Track partial images image_count = 0 - # Use temp directory for output output_dir = Path(tempfile.gettempdir()) / "generated_images" output_dir.mkdir(exist_ok=True) - print(" Streaming response:") async for update in agent.run(query, stream=True): for content in update.contents: @@ -81,18 +72,14 @@ async def main(): image_output: Content = content.outputs if image_output.type == "data" and image_output.additional_properties.get("is_partial_image"): print(f" Image {image_count} received") - # Extract file extension from media_type (e.g., "image/png" -> "png") extension = "png" # Default fallback if image_output.media_type and "/" in image_output.media_type: extension = image_output.media_type.split("/")[-1] - # Save images with correct extension filename = output_dir / f"image{image_count}.{extension}" await save_image_from_data_uri(image_output.uri, str(filename)) - image_count += 1 - # Summary print("\n Summary:") print(f" Images received: {image_count}") diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_with_agent_as_tool.py b/python/samples/02-agents/providers/openai/openai_responses_client_with_agent_as_tool.py index 8c858b417f..aa581058be 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_with_agent_as_tool.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_with_agent_as_tool.py @@ -3,7 +3,7 @@ import asyncio from collections.abc import Awaitable, Callable -from agent_framework import FunctionInvocationContext +from agent_framework import Agent, FunctionInvocationContext from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -40,7 +40,7 @@ async def main() -> None: client = OpenAIResponsesClient() # Create a specialized writer agent - writer = client.as_agent( + writer = Agent(client=client, name="WriterAgent", instructions="You are a creative writer. Write short, engaging content.", ) @@ -54,7 +54,7 @@ async def main() -> None: ) # Create coordinator agent with writer as a tool - coordinator = client.as_agent( + coordinator = Agent(client=client, name="CoordinatorAgent", instructions="You coordinate with specialized agents. Delegate writing tasks to the creative_writer tool.", tools=[writer_tool], diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_with_explicit_settings.py b/python/samples/02-agents/providers/openai/openai_responses_client_with_explicit_settings.py index f45b5b8778..432ed7a1aa 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_with_explicit_settings.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_with_explicit_settings.py @@ -5,7 +5,7 @@ from random import randint from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv from pydantic import Field @@ -36,10 +36,12 @@ def get_weather( async def main() -> None: print("=== OpenAI Responses Client with Explicit Settings ===") - agent = OpenAIResponsesClient( - model_id=os.environ["OPENAI_RESPONSES_MODEL_ID"], + _client = OpenAIResponsesClient( + model=os.environ["OPENAI_MODEL"], api_key=os.environ["OPENAI_API_KEY"], - ).as_agent( + ) + + agent = Agent(client=_client, instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_with_runtime_json_schema.py b/python/samples/02-agents/providers/openai/openai_responses_client_with_runtime_json_schema.py index 8a08e50a31..cdc4ce13fb 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_with_runtime_json_schema.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_with_runtime_json_schema.py @@ -3,6 +3,7 @@ import asyncio import json +from agent_framework import Agent from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -36,7 +37,8 @@ async def non_streaming_example() -> None: print("=== Non-streaming runtime JSON schema example ===") - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="RuntimeSchemaAgent", instructions="Return only JSON that matches the provided schema. Do not add commentary.", ) @@ -69,7 +71,8 @@ async def non_streaming_example() -> None: async def streaming_example() -> None: print("=== Streaming runtime JSON schema example ===") - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="RuntimeSchemaAgent", instructions="Return only JSON that matches the provided schema. Do not add commentary.", ) diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_with_structured_output.py b/python/samples/02-agents/providers/openai/openai_responses_client_with_structured_output.py index 429786245d..d2599c0bd8 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_with_structured_output.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_with_structured_output.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import AgentResponse +from agent_framework import Agent, AgentResponse from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv from pydantic import BaseModel @@ -29,7 +29,8 @@ async def non_streaming_example() -> None: print("=== Non-streaming example ===") # Create an OpenAI Responses agent - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="CityAgent", instructions="You are a helpful agent that describes cities in a structured format.", ) @@ -54,7 +55,8 @@ async def streaming_example() -> None: print("=== Streaming example ===") # Create an OpenAI Responses agent - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="CityAgent", instructions="You are a helpful agent that describes cities in a structured format.", ) diff --git a/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py b/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py index aeef342a06..871fc6913a 100644 --- a/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py +++ b/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py @@ -7,7 +7,7 @@ from typing import Any from agent_framework import Agent, Skill, SkillResource, SkillsProvider -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -128,12 +128,12 @@ def convert_units(value: float, factor: float, **kwargs: Any) -> str: async def main() -> None: """Run the code-defined skills demo.""" - endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] - deployment = os.environ.get("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "gpt-4o-mini") + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini") - client = AzureOpenAIResponsesClient( + client = FoundryChatClient( project_endpoint=endpoint, - deployment_name=deployment, + model=deployment, credential=AzureCliCredential(), ) diff --git a/python/samples/02-agents/skills/file_based_skill/file_based_skill.py b/python/samples/02-agents/skills/file_based_skill/file_based_skill.py index de712c4fb1..63f5d5c0b8 100644 --- a/python/samples/02-agents/skills/file_based_skill/file_based_skill.py +++ b/python/samples/02-agents/skills/file_based_skill/file_based_skill.py @@ -6,7 +6,7 @@ from pathlib import Path from agent_framework import Agent, SkillsProvider -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -40,13 +40,13 @@ async def main() -> None: """Run the file-based skills demo.""" - endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] - deployment = os.environ.get("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "gpt-4o-mini") + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini") # Create the chat client - client = AzureOpenAIResponsesClient( + client = FoundryChatClient( project_endpoint=endpoint, - deployment_name=deployment, + model=deployment, credential=AzureCliCredential(), ) diff --git a/python/samples/02-agents/skills/file_based_skill/skills/unit-converter/scripts/convert.py b/python/samples/02-agents/skills/file_based_skill/skills/unit-converter/scripts/convert.py index 228c8809ff..9629d22635 100644 --- a/python/samples/02-agents/skills/file_based_skill/skills/unit-converter/scripts/convert.py +++ b/python/samples/02-agents/skills/file_based_skill/skills/unit-converter/scripts/convert.py @@ -7,8 +7,6 @@ import argparse import json - - def main() -> None: parser = argparse.ArgumentParser( description="Convert a value using a multiplication factor.", @@ -20,10 +18,7 @@ def main() -> None: parser.add_argument("--value", type=float, required=True, help="The numeric value to convert.") parser.add_argument("--factor", type=float, required=True, help="The conversion factor from the table.") args = parser.parse_args() - result = round(args.value * args.factor, 4) print(json.dumps({"value": args.value, "factor": args.factor, "result": result})) - - if __name__ == "__main__": main() diff --git a/python/samples/02-agents/skills/mixed_skills/mixed_skills.py b/python/samples/02-agents/skills/mixed_skills/mixed_skills.py index 9916b430b6..92bb947c0c 100644 --- a/python/samples/02-agents/skills/mixed_skills/mixed_skills.py +++ b/python/samples/02-agents/skills/mixed_skills/mixed_skills.py @@ -13,7 +13,7 @@ Skill, SkillsProvider, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -107,13 +107,13 @@ def convert_volume(value: float, factor: float) -> str: async def main() -> None: """Run the combined skills demo.""" - endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] - deployment = os.environ.get("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "gpt-4o-mini") + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini") # Create the chat client - client = AzureOpenAIResponsesClient( + client = FoundryChatClient( project_endpoint=endpoint, - deployment_name=deployment, + model=deployment, credential=AzureCliCredential(), ) diff --git a/python/samples/02-agents/skills/mixed_skills/skills/unit-converter/scripts/convert.py b/python/samples/02-agents/skills/mixed_skills/skills/unit-converter/scripts/convert.py index 228c8809ff..9629d22635 100644 --- a/python/samples/02-agents/skills/mixed_skills/skills/unit-converter/scripts/convert.py +++ b/python/samples/02-agents/skills/mixed_skills/skills/unit-converter/scripts/convert.py @@ -7,8 +7,6 @@ import argparse import json - - def main() -> None: parser = argparse.ArgumentParser( description="Convert a value using a multiplication factor.", @@ -20,10 +18,7 @@ def main() -> None: parser.add_argument("--value", type=float, required=True, help="The numeric value to convert.") parser.add_argument("--factor", type=float, required=True, help="The conversion factor from the table.") args = parser.parse_args() - result = round(args.value * args.factor, 4) print(json.dumps({"value": args.value, "factor": args.factor, "result": result})) - - if __name__ == "__main__": main() diff --git a/python/samples/02-agents/skills/script_approval/script_approval.py b/python/samples/02-agents/skills/script_approval/script_approval.py index b1613ef28f..fbae14d99d 100644 --- a/python/samples/02-agents/skills/script_approval/script_approval.py +++ b/python/samples/02-agents/skills/script_approval/script_approval.py @@ -5,7 +5,7 @@ from textwrap import dedent from agent_framework import Agent, Skill, SkillsProvider -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -29,8 +29,8 @@ an error if rejected. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME (defaults to "gpt-4o-mini"). +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_MODEL (defaults to "gpt-4o-mini"). """ # Load environment variables from .env file @@ -56,12 +56,12 @@ def deploy(version: str, environment: str = "staging") -> str: async def main() -> None: """Run the skill script approval demo.""" - endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] - deployment = os.environ.get("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "gpt-4o-mini") + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini") - client = AzureOpenAIResponsesClient( + client = FoundryChatClient( project_endpoint=endpoint, - deployment_name=deployment, + model=deployment, credential=AzureCliCredential(), ) diff --git a/python/samples/02-agents/skills/subprocess_script_runner.py b/python/samples/02-agents/skills/subprocess_script_runner.py index 1d38bae754..a49b224d1c 100644 --- a/python/samples/02-agents/skills/subprocess_script_runner.py +++ b/python/samples/02-agents/skills/subprocess_script_runner.py @@ -1,11 +1,9 @@ # Copyright (c) Microsoft. All rights reserved. """Sample subprocess-based skill script runner. - Executes file-based skill scripts as local Python subprocesses. This is provided for demonstration purposes only. """ - from __future__ import annotations import subprocess @@ -18,30 +16,23 @@ def subprocess_script_runner(skill: Skill, script: SkillScript, args: dict[str, Any] | None = None) -> str: """Run a skill script as a local Python subprocess. - Resolves the script's absolute path from the skill directory, converts the ``args`` dict to CLI flags, and returns captured output. - Args: skill: The skill that owns the script. script: The script to run. args: Optional arguments forwarded as CLI flags. - Returns: The combined stdout/stderr output, or an error message. """ if not skill.path: return f"Error: Skill '{skill.name}' has no directory path." - if not script.path: return f"Error: Script '{script.name}' has no file path. Only file-based scripts can be executed locally." - script_path = Path(skill.path) / script.path if not script_path.is_file(): return f"Error: Script file not found: {script_path}" - cmd = [sys.executable, str(script_path)] - # Convert args dict to CLI flags if args: for key, value in args.items(): @@ -51,7 +42,6 @@ def subprocess_script_runner(skill: Skill, script: SkillScript, args: dict[str, elif value is not None: cmd.append(f"--{key}") cmd.append(str(value)) - try: result = subprocess.run( cmd, @@ -60,15 +50,12 @@ def subprocess_script_runner(skill: Skill, script: SkillScript, args: dict[str, timeout=30, cwd=str(script_path.parent), ) - output = result.stdout if result.stderr: output += f"\nStderr:\n{result.stderr}" if result.returncode != 0: output += f"\nScript exited with code {result.returncode}" - return output.strip() or "(no output)" - except subprocess.TimeoutExpired: return f"Error: Script '{script.name}' timed out after 30 seconds." except OSError as e: diff --git a/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py b/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py index fa78a9ede5..cbcac5952f 100644 --- a/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py +++ b/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py @@ -3,7 +3,7 @@ import asyncio from collections.abc import Awaitable, Callable -from agent_framework import AgentContext, AgentSession, FunctionInvocationContext, tool +from agent_framework import Agent, AgentContext, AgentSession, FunctionInvocationContext, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -65,7 +65,7 @@ async def main() -> None: client = OpenAIResponsesClient() - research_agent = client.as_agent( + research_agent = Agent(client=client, name="ResearchAgent", instructions="You are a research assistant. Provide concise answers and store your findings.", middleware=[log_session], @@ -80,7 +80,7 @@ async def main() -> None: propagate_session=True, ) - coordinator = client.as_agent( + coordinator = Agent(client=client, name="CoordinatorAgent", instructions=( "You coordinate research. Use the 'research' tool to start research " diff --git a/python/samples/02-agents/tools/control_total_tool_executions.py b/python/samples/02-agents/tools/control_total_tool_executions.py index ae19d1a077..c53b228430 100644 --- a/python/samples/02-agents/tools/control_total_tool_executions.py +++ b/python/samples/02-agents/tools/control_total_tool_executions.py @@ -3,7 +3,7 @@ import asyncio from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -88,7 +88,8 @@ async def scenario_max_iterations(): client.function_invocation_configuration["max_iterations"] = 3 print(f" max_iterations = {client.function_invocation_configuration['max_iterations']}") - agent = client.as_agent( + agent = Agent( + client=client, name="ResearchAgent", instructions=( "You are a research assistant. Use the search_web tool to answer " @@ -125,7 +126,8 @@ async def scenario_max_function_calls(): print(f" max_iterations = {client.function_invocation_configuration['max_iterations']}") print(f" max_function_calls = {client.function_invocation_configuration['max_function_calls']}") - agent = client.as_agent( + agent = Agent( + client=client, name="ResearchAgent", instructions=( "You are a research assistant. Use the search_web and get_weather " @@ -155,7 +157,8 @@ async def scenario_max_invocations(): print("Scenario 3: max_invocations — lifetime cap on a tool") print("=" * 60) - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="APIAgent", instructions="Use call_expensive_api when asked to analyze something.", tools=[call_expensive_api], @@ -212,12 +215,14 @@ def _do_lookup(query: Annotated[str, "Search query."]) -> str: agent_b_lookup = tool(name="lookup", approval_mode="never_require", max_invocations=5)(_do_lookup) client = OpenAIResponsesClient() - agent_a = client.as_agent( + agent_a = Agent( + client=client, name="AgentA", instructions="Use the lookup tool to answer questions.", tools=[agent_a_lookup], ) - agent_b = client.as_agent( + agent_b = Agent( + client=client, name="AgentB", instructions="Use the lookup tool to answer questions.", tools=[agent_b_lookup], @@ -270,7 +275,8 @@ def premium_lookup(topic: Annotated[str, "Topic to look up."]) -> str: print(f" premium_lookup.max_invocations = {premium_lookup.max_invocations}") - agent = client.as_agent( + agent = Agent( + client=client, name="MultiToolAgent", instructions="Use all available tools to answer comprehensively.", tools=[search_web, get_weather, premium_lookup], diff --git a/python/samples/02-agents/tools/function_invocation_configuration.py b/python/samples/02-agents/tools/function_invocation_configuration.py index 3c3479c87a..78fdecbb2c 100644 --- a/python/samples/02-agents/tools/function_invocation_configuration.py +++ b/python/samples/02-agents/tools/function_invocation_configuration.py @@ -3,7 +3,7 @@ import asyncio from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -33,7 +33,7 @@ async def main(): client.function_invocation_configuration["max_iterations"] = 40 print(f"Function invocation configured as: \n{client.function_invocation_configuration}") - agent = client.as_agent(name="ToolAgent", instructions="Use the provided tools.", tools=add) + agent = Agent(client=client, name="ToolAgent", instructions="Use the provided tools.", tools=add) print("=" * 60) print("Call add(239847293, 29834)") diff --git a/python/samples/02-agents/tools/function_tool_declaration_only.py b/python/samples/02-agents/tools/function_tool_declaration_only.py index 2d7c54c791..efa98d85bc 100644 --- a/python/samples/02-agents/tools/function_tool_declaration_only.py +++ b/python/samples/02-agents/tools/function_tool_declaration_only.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import FunctionTool +from agent_framework import Agent, FunctionTool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -25,7 +25,8 @@ async def main(): description="Get the current time in ISO 8601 format.", ) - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="DeclarationOnlyToolAgent", instructions="You are a helpful agent that uses tools.", tools=function_declaration, diff --git a/python/samples/02-agents/tools/function_tool_from_dict_with_dependency_injection.py b/python/samples/02-agents/tools/function_tool_from_dict_with_dependency_injection.py index 39eef147be..df80c1b1e9 100644 --- a/python/samples/02-agents/tools/function_tool_from_dict_with_dependency_injection.py +++ b/python/samples/02-agents/tools/function_tool_from_dict_with_dependency_injection.py @@ -21,7 +21,7 @@ import asyncio -from agent_framework import FunctionTool +from agent_framework import Agent, FunctionTool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -61,7 +61,8 @@ def func(a, b) -> int: # - "func": the parameter name that will receive the injected function tool = FunctionTool.from_dict(definition, dependencies={"function_tool": {"name:add_numbers": {"func": func}}}) - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="FunctionToolAgent", instructions="You are a helpful assistant.", tools=tool ) response = await agent.run("What is 5 + 3?") diff --git a/python/samples/02-agents/tools/function_tool_recover_from_failures.py b/python/samples/02-agents/tools/function_tool_recover_from_failures.py index 527a35c79a..48f7b80a56 100644 --- a/python/samples/02-agents/tools/function_tool_recover_from_failures.py +++ b/python/samples/02-agents/tools/function_tool_recover_from_failures.py @@ -3,7 +3,7 @@ import asyncio from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -45,7 +45,8 @@ def safe_divide( async def main(): # tools = Tools() - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="ToolAgent", instructions="Use the provided tools.", tools=[greet, safe_divide], diff --git a/python/samples/02-agents/tools/function_tool_with_approval_and_sessions.py b/python/samples/02-agents/tools/function_tool_with_approval_and_sessions.py index 089eb7dbdd..1ade82df2a 100644 --- a/python/samples/02-agents/tools/function_tool_with_approval_and_sessions.py +++ b/python/samples/02-agents/tools/function_tool_with_approval_and_sessions.py @@ -4,7 +4,7 @@ from typing import Annotated from agent_framework import Agent, Message, tool -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -32,7 +32,7 @@ async def approval_example() -> None: print("=== Tool Approval with Session ===\n") agent = Agent( - client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), name="CalendarAgent", instructions="You are a helpful calendar assistant.", tools=[add_to_calendar], @@ -68,7 +68,7 @@ async def rejection_example() -> None: print("=== Tool Rejection with Session ===\n") agent = Agent( - client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), name="CalendarAgent", instructions="You are a helpful calendar assistant.", tools=[add_to_calendar], diff --git a/python/samples/02-agents/tools/function_tool_with_explicit_schema.py b/python/samples/02-agents/tools/function_tool_with_explicit_schema.py index 10fbde187f..231a980b45 100644 --- a/python/samples/02-agents/tools/function_tool_with_explicit_schema.py +++ b/python/samples/02-agents/tools/function_tool_with_explicit_schema.py @@ -17,7 +17,7 @@ import asyncio from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv from pydantic import BaseModel, Field @@ -69,7 +69,8 @@ def get_current_time(timezone: str = "UTC") -> str: async def main(): - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="AssistantAgent", instructions="You are a helpful assistant. Use the available tools to answer questions.", tools=[get_weather, get_current_time], diff --git a/python/samples/02-agents/tools/function_tool_with_kwargs.py b/python/samples/02-agents/tools/function_tool_with_kwargs.py index 61db84eb17..77664d6f8c 100644 --- a/python/samples/02-agents/tools/function_tool_with_kwargs.py +++ b/python/samples/02-agents/tools/function_tool_with_kwargs.py @@ -3,7 +3,7 @@ import asyncio from typing import Annotated -from agent_framework import FunctionInvocationContext, tool +from agent_framework import Agent, FunctionInvocationContext, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv from pydantic import Field @@ -43,7 +43,8 @@ def get_weather( async def main() -> None: - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="WeatherAgent", instructions="You are a helpful weather assistant.", tools=[get_weather], diff --git a/python/samples/02-agents/tools/function_tool_with_max_exceptions.py b/python/samples/02-agents/tools/function_tool_with_max_exceptions.py index 7830b53794..b8ed2f5b58 100644 --- a/python/samples/02-agents/tools/function_tool_with_max_exceptions.py +++ b/python/samples/02-agents/tools/function_tool_with_max_exceptions.py @@ -3,7 +3,7 @@ import asyncio from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -35,7 +35,8 @@ def safe_divide( async def main(): # tools = Tools() - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="ToolAgent", instructions="Use the provided tools.", tools=[safe_divide], diff --git a/python/samples/02-agents/tools/function_tool_with_max_invocations.py b/python/samples/02-agents/tools/function_tool_with_max_invocations.py index 1619cb4046..0cea02c23e 100644 --- a/python/samples/02-agents/tools/function_tool_with_max_invocations.py +++ b/python/samples/02-agents/tools/function_tool_with_max_invocations.py @@ -3,7 +3,7 @@ import asyncio from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -24,7 +24,8 @@ def unicorn_function(times: Annotated[int, "The number of unicorns to return."]) async def main(): # tools = Tools() - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="ToolAgent", instructions="Use the provided tools.", tools=[unicorn_function], diff --git a/python/samples/02-agents/tools/function_tool_with_session_injection.py b/python/samples/02-agents/tools/function_tool_with_session_injection.py index 282a79c1fb..21df5cc2c9 100644 --- a/python/samples/02-agents/tools/function_tool_with_session_injection.py +++ b/python/samples/02-agents/tools/function_tool_with_session_injection.py @@ -3,7 +3,7 @@ import asyncio from typing import Annotated -from agent_framework import AgentSession, FunctionInvocationContext, tool +from agent_framework import Agent, AgentSession, FunctionInvocationContext, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv from pydantic import Field @@ -36,7 +36,8 @@ async def get_weather( async def main() -> None: - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="WeatherAgent", instructions="You are a helpful weather assistant.", tools=[get_weather], diff --git a/python/samples/02-agents/tools/tool_in_class.py b/python/samples/02-agents/tools/tool_in_class.py index 61f3230148..7a4c1051b7 100644 --- a/python/samples/02-agents/tools/tool_in_class.py +++ b/python/samples/02-agents/tools/tool_in_class.py @@ -3,7 +3,7 @@ import asyncio from typing import Annotated -from agent_framework import tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -49,7 +49,8 @@ async def main(): # Applying the tool decorator to one of the methods of the class add_function = tool(description="Add two numbers.")(tools.add) - agent = OpenAIResponsesClient().as_agent( + agent = Agent( + client=OpenAIResponsesClient(), name="ToolAgent", instructions="Use the provided tools.", ) diff --git a/python/samples/02-agents/typed_options.py b/python/samples/02-agents/typed_options.py index 65e59ec3d4..d97fcff393 100644 --- a/python/samples/02-agents/typed_options.py +++ b/python/samples/02-agents/typed_options.py @@ -39,7 +39,7 @@ async def demo_anthropic_chat_client() -> None: print("\n=== Anthropic ChatClient with TypedDict Options ===\n") # Create Anthropic client - client = AnthropicClient(model_id="claude-sonnet-4-5-20250929") + client = AnthropicClient(model="claude-sonnet-4-5-20250929") # Standard options work great: response = await client.get_response( @@ -61,7 +61,7 @@ async def demo_anthropic_agent() -> None: """Demonstrate Agent with Anthropic client and typed options.""" print("\n=== Agent with Anthropic and Typed Options ===\n") - client = AnthropicClient(model_id="claude-sonnet-4-5-20250929") + client = AnthropicClient(model="claude-sonnet-4-5-20250929") # Create a typed agent for Anthropic - IDE knows Anthropic-specific options! agent = Agent( @@ -148,7 +148,7 @@ async def demo_openai_agent() -> None: # or on the client when constructing the client instance: # client = OpenAIChatClient[OpenAIReasoningChatOptions]() agent = Agent[OpenAIReasoningChatOptions]( - client=OpenAIChatClient(model_id="o3"), + client=FoundryChatClient(model="o3"), name="weather-assistant", instructions="You are a helpful assistant. Answer concisely.", # Options can be set at construction time diff --git a/python/samples/03-workflows/_start-here/step2_agents_in_a_workflow.py b/python/samples/03-workflows/_start-here/step2_agents_in_a_workflow.py index 6ace56da55..46f56eb9f5 100644 --- a/python/samples/03-workflows/_start-here/step2_agents_in_a_workflow.py +++ b/python/samples/03-workflows/_start-here/step2_agents_in_a_workflow.py @@ -4,8 +4,8 @@ import os from typing import cast -from agent_framework import AgentResponse, WorkflowBuilder -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, AgentResponse, WorkflowBuilder +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -19,12 +19,12 @@ evaluates and provides feedback. Purpose: -Show how to create agents from AzureOpenAIResponsesClient and use them directly in a workflow. Demonstrate +Show how to create agents from FoundryChatClient and use them directly in a workflow. Demonstrate how agents can be used in a workflow. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Basic familiarity with WorkflowBuilder, edges, events, and streaming or non-streaming runs. """ @@ -33,19 +33,19 @@ async def main(): """Build and run a simple two node agent workflow: Writer then Reviewer.""" # Create the Azure chat client. AzureCliCredential uses your current az login. - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - writer_agent = client.as_agent( + writer_agent = Agent(client=client, instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), name="writer", ) - reviewer_agent = client.as_agent( + reviewer_agent = Agent(client=client, instructions=( "You are an excellent content reviewer." "Provide actionable feedback to the writer about the provided content." diff --git a/python/samples/03-workflows/_start-here/step3_streaming.py b/python/samples/03-workflows/_start-here/step3_streaming.py index 1850cfc4a4..238c2d3041 100644 --- a/python/samples/03-workflows/_start-here/step3_streaming.py +++ b/python/samples/03-workflows/_start-here/step3_streaming.py @@ -3,8 +3,8 @@ import asyncio import os -from agent_framework import AgentResponseUpdate, Message, WorkflowBuilder -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, AgentResponseUpdate, Message, WorkflowBuilder +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -18,12 +18,12 @@ evaluates and provides feedback. Purpose: -Show how to create agents from AzureOpenAIResponsesClient and use them directly in a workflow. Demonstrate +Show how to create agents from FoundryChatClient and use them directly in a workflow. Demonstrate how agents can be used in a workflow. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming runs. """ @@ -32,19 +32,19 @@ async def main(): """Build the two node workflow and run it with streaming to observe events.""" # Create the Azure chat client. AzureCliCredential uses your current az login. - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - writer_agent = client.as_agent( + writer_agent = Agent(client=client, instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), name="writer", ) - reviewer_agent = client.as_agent( + reviewer_agent = Agent(client=client, instructions=( "You are an excellent content reviewer." "Provide actionable feedback to the writer about the provided content." diff --git a/python/samples/03-workflows/agents/azure_ai_agents_streaming.py b/python/samples/03-workflows/agents/azure_ai_agents_streaming.py index bac2506468..2750d8edae 100644 --- a/python/samples/03-workflows/agents/azure_ai_agents_streaming.py +++ b/python/samples/03-workflows/agents/azure_ai_agents_streaming.py @@ -3,8 +3,8 @@ import asyncio import os -from agent_framework import AgentResponseUpdate, WorkflowBuilder -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, AgentResponseUpdate, WorkflowBuilder +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -17,7 +17,7 @@ This sample shows how to create agents backed by Azure OpenAI Responses and use them in a workflow with streaming. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - AZURE_AI_MODEL_DEPLOYMENT_NAME must be set to your Azure OpenAI model deployment name. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Basic familiarity with WorkflowBuilder, edges, events, and streaming runs. @@ -25,21 +25,21 @@ async def main() -> None: - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create two agents: a Writer and a Reviewer. - writer_agent = client.as_agent( + writer_agent = Agent(client=client, name="Writer", instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), ) - reviewer_agent = client.as_agent( + reviewer_agent = Agent(client=client, name="Reviewer", instructions=( "You are an excellent content reviewer. " diff --git a/python/samples/03-workflows/agents/azure_ai_agents_with_shared_session.py b/python/samples/03-workflows/agents/azure_ai_agents_with_shared_session.py index 7a69892a77..3084f39445 100644 --- a/python/samples/03-workflows/agents/azure_ai_agents_with_shared_session.py +++ b/python/samples/03-workflows/agents/azure_ai_agents_with_shared_session.py @@ -4,6 +4,7 @@ import os from agent_framework import ( + Agent, AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, @@ -13,7 +14,7 @@ WorkflowRunState, executor, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -33,11 +34,11 @@ - Not all agents can share threads; usually only the same type of agents can share threads. Demonstrate: -- Creating multiple agents with AzureOpenAIResponsesClient. +- Creating multiple agents with FoundryChatClient. - Setting up a shared thread between agents. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - AZURE_AI_MODEL_DEPLOYMENT_NAME must be set to your Azure OpenAI model deployment name. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Basic familiarity with agents, workflows, and executors in the agent framework. @@ -57,20 +58,22 @@ async def intercept_agent_response( async def main() -> None: - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # set the same context provider (same default source_id) for both agents to share the thread - writer = client.as_agent( + writer = Agent( + client=client, instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), name="writer", context_providers=[InMemoryHistoryProvider()], ) - reviewer = client.as_agent( + reviewer = Agent( + client=client, instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), name="reviewer", context_providers=[InMemoryHistoryProvider()], diff --git a/python/samples/03-workflows/agents/azure_chat_agents_and_executor.py b/python/samples/03-workflows/agents/azure_chat_agents_and_executor.py index 76436a0ce1..7632543586 100644 --- a/python/samples/03-workflows/agents/azure_chat_agents_and_executor.py +++ b/python/samples/03-workflows/agents/azure_chat_agents_and_executor.py @@ -5,6 +5,7 @@ from typing import Final from agent_framework import ( + Agent, AgentExecutorRequest, AgentExecutorResponse, AgentResponseUpdate, @@ -13,7 +14,7 @@ WorkflowContext, executor, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -35,8 +36,8 @@ - Consuming an AgentExecutorResponse and forwarding an AgentExecutorRequest for the next agent. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Run `az login` before executing. """ @@ -100,22 +101,24 @@ async def enrich_with_references( async def main() -> None: """Run the workflow and stream combined updates from both agents.""" # Create the agents - research_agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + research_agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), name="research_agent", instructions=( "Produce a short, bullet-style briefing with two actionable ideas. Label the section as 'Initial Draft'." ), ) - final_editor_agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + final_editor_agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), name="final_editor_agent", instructions=( "Use all conversation context (including external notes) to produce the final answer. " diff --git a/python/samples/03-workflows/agents/azure_chat_agents_streaming.py b/python/samples/03-workflows/agents/azure_chat_agents_streaming.py index 13c20a0a57..40b6b7ba1c 100644 --- a/python/samples/03-workflows/agents/azure_chat_agents_streaming.py +++ b/python/samples/03-workflows/agents/azure_chat_agents_streaming.py @@ -4,7 +4,7 @@ import os from agent_framework import AgentResponseUpdate, WorkflowBuilder -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -17,8 +17,8 @@ This sample shows how to create AzureOpenAI Chat Agents and use them in a workflow with streaming. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Basic familiarity with WorkflowBuilder, edges, events, and streaming runs. """ @@ -27,22 +27,26 @@ async def main(): """Build and run a simple two node agent workflow: Writer then Reviewer.""" # Create the agents - writer_agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + _writer_client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ) + writer_agent = Agent( + client=_writer_client, instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), name="writer", ) - reviewer_agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + _reviewer_client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ) + reviewer_agent = Agent( + client=_reviewer_client, instructions=( "You are an excellent content reviewer." "Provide actionable feedback to the writer about the provided content." diff --git a/python/samples/03-workflows/agents/azure_chat_agents_tool_calls_with_feedback.py b/python/samples/03-workflows/agents/azure_chat_agents_tool_calls_with_feedback.py index 68c9eb5ae0..ff50a323fd 100644 --- a/python/samples/03-workflows/agents/azure_chat_agents_tool_calls_with_feedback.py +++ b/python/samples/03-workflows/agents/azure_chat_agents_tool_calls_with_feedback.py @@ -22,7 +22,7 @@ response_handler, tool, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import Field @@ -48,8 +48,8 @@ - Streaming AgentRunUpdateEvent updates alongside human-in-the-loop pauses. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Run `az login` before executing. """ @@ -175,13 +175,14 @@ async def on_human_feedback( def create_writer_agent() -> Agent: """Creates a writer agent with tools.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - # This sample has been tested only on `gpt-5.1` and may not work as intended on other models - # This sample is known to fail on `gpt-5-mini` reasoning input (GH issue #4059) - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + + + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), name="writer_agent", instructions=( "You are a marketing writer. Call the available tools before drafting copy so you are precise. " @@ -195,11 +196,12 @@ def create_writer_agent() -> Agent: def create_final_editor_agent() -> Agent: """Creates a final editor agent.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), name="final_editor_agent", instructions=( "You are an editor who polishes marketing copy after human approval. " diff --git a/python/samples/03-workflows/agents/concurrent_workflow_as_agent.py b/python/samples/03-workflows/agents/concurrent_workflow_as_agent.py index 48c3155edd..37956c1fe0 100644 --- a/python/samples/03-workflows/agents/concurrent_workflow_as_agent.py +++ b/python/samples/03-workflows/agents/concurrent_workflow_as_agent.py @@ -1,9 +1,9 @@ # Copyright (c) Microsoft. All rights reserved. - import asyncio import os -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import ConcurrentBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -15,30 +15,30 @@ Sample: Build a concurrent workflow orchestration and wrap it as an agent. This script wires up a fan-out/fan-in workflow using `ConcurrentBuilder`, and then -invokes the entire orchestration through the `workflow.as_agent(...)` interface so +invokes the entire orchestration through the `Agent(client=workflow,...)` interface so downstream coordinators can reuse the orchestration as a single agent. Demonstrates: - Fan-out to multiple agents, fan-in aggregation of final ChatMessages. -- Reusing the orchestrated workflow as an agent entry point with `workflow.as_agent(...)`. +- Reusing the orchestrated workflow as an agent entry point with `Agent(client=workflow,...)`. - Workflow completion when idle with no pending work Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI access configured for AzureOpenAIResponsesClient (use az login + env vars) +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI access configured for FoundryChatClient (use az login + env vars) - Familiarity with Workflow events (WorkflowEvent with type "output") """ async def main() -> None: - # 1) Create three domain agents using AzureOpenAIResponsesClient - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + # 1) Create three domain agents using FoundryChatClient + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - researcher = client.as_agent( + researcher = Agent(client=client, instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -46,7 +46,7 @@ async def main() -> None: name="researcher", ) - marketer = client.as_agent( + marketer = Agent(client=client, instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -54,7 +54,7 @@ async def main() -> None: name="marketer", ) - legal = client.as_agent( + legal = Agent(client=client, instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." @@ -66,7 +66,7 @@ async def main() -> None: workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build() # 3) Expose the concurrent workflow as an agent for easy reuse - agent = workflow.as_agent(name="ConcurrentWorkflowAgent") + agent = Agent(client=workflow, name="ConcurrentWorkflowAgent") prompt = "We are launching a new budget-friendly electric bike for urban commuters." agent_response = await agent.run(prompt) diff --git a/python/samples/03-workflows/agents/custom_agent_executors.py b/python/samples/03-workflows/agents/custom_agent_executors.py index 4534ebe39e..daa713b642 100644 --- a/python/samples/03-workflows/agents/custom_agent_executors.py +++ b/python/samples/03-workflows/agents/custom_agent_executors.py @@ -11,7 +11,7 @@ WorkflowContext, handler, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -25,15 +25,15 @@ then hands the conversation to a Reviewer agent which evaluates and finalizes the result. Purpose: -Show how to wrap chat agents created by AzureOpenAIResponsesClient inside workflow executors. Demonstrate the @handler +Show how to wrap chat agents created by FoundryChatClient inside workflow executors. Demonstrate the @handler pattern with typed inputs and typed WorkflowContext[T] outputs, connect executors with the fluent WorkflowBuilder, and finish by yielding outputs from the terminal node. Note: When an agent is passed to a workflow, the workflow wraps the agent in a more sophisticated executor. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming or non streaming runs. """ @@ -50,12 +50,13 @@ class Writer(Executor): agent: Agent def __init__(self, id: str = "writer"): - # Create a domain specific agent using your configured AzureOpenAIResponsesClient. - self.agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + # Create a domain specific agent using your configured FoundryChatClient. + self.agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), @@ -97,11 +98,12 @@ class Reviewer(Executor): def __init__(self, id: str = "reviewer"): # Create a domain specific agent that evaluates and refines content. - self.agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + self.agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You are an excellent content reviewer. You review the content and provide feedback to the writer." ), diff --git a/python/samples/03-workflows/agents/group_chat_workflow_as_agent.py b/python/samples/03-workflows/agents/group_chat_workflow_as_agent.py index 05994723cb..676648bfd5 100644 --- a/python/samples/03-workflows/agents/group_chat_workflow_as_agent.py +++ b/python/samples/03-workflows/agents/group_chat_workflow_as_agent.py @@ -4,7 +4,7 @@ import os from agent_framework import Agent -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import GroupChatBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -20,8 +20,8 @@ - The orchestrator coordinates a researcher (chat completions) and a writer (responses API) to solve a task. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Environment variables configured for `AzureOpenAIResponsesClient`. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Environment variables configured for `FoundryChatClient`. """ @@ -30,9 +30,9 @@ async def main() -> None: name="Researcher", description="Collects relevant background information.", instructions="Gather concise facts that help a teammate answer the question.", - client=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ), ) @@ -41,23 +41,26 @@ async def main() -> None: name="Writer", description="Synthesizes a polished answer using the gathered notes.", instructions="Compose clear and structured answers using any notes provided.", - client=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ), ) + _orch_client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ) + # intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds # (Intermediate outputs will be emitted as WorkflowOutputEvent events) workflow = GroupChatBuilder( participants=[researcher, writer], intermediate_outputs=True, - orchestrator_agent=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ).as_agent( + orchestrator_agent=Agent( + client=_orch_client, name="Orchestrator", instructions="You coordinate a team conversation to solve the user's task.", ), @@ -69,7 +72,7 @@ async def main() -> None: print(f"Input: {task}\n") try: - workflow_agent = workflow.as_agent(name="GroupChatWorkflowAgent") + workflow_agent = Agent(client=workflow, name="GroupChatWorkflowAgent") agent_result = await workflow_agent.run(task) if agent_result.messages: diff --git a/python/samples/03-workflows/agents/handoff_workflow_as_agent.py b/python/samples/03-workflows/agents/handoff_workflow_as_agent.py index f059009a8e..a6e9baadab 100644 --- a/python/samples/03-workflows/agents/handoff_workflow_as_agent.py +++ b/python/samples/03-workflows/agents/handoff_workflow_as_agent.py @@ -12,7 +12,7 @@ WorkflowAgent, tool, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -29,9 +29,9 @@ them to transfer control to each other based on the conversation context. Prerequisites: - - AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. + - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - `az login` (Azure CLI authentication) - - Environment variables configured for AzureOpenAIResponsesClient (AZURE_AI_MODEL_DEPLOYMENT_NAME) + - Environment variables configured for FoundryChatClient (AZURE_AI_MODEL_DEPLOYMENT_NAME) Key Concepts: - Auto-registered handoff tools: HandoffBuilder automatically creates handoff tools @@ -63,17 +63,18 @@ def process_return(order_number: Annotated[str, "Order number to process return return f"Return initiated successfully for order {order_number}. You will receive return instructions via email." -def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Agent, Agent]: +def create_agents(client: FoundryChatClient) -> tuple[Agent, Agent, Agent, Agent]: """Create and configure the triage and specialist agents. Args: - client: The AzureOpenAIResponsesClient to use for creating agents. + client: The FoundryChatClient to use for creating agents. Returns: Tuple of (triage_agent, refund_agent, order_agent, return_agent) """ # Triage agent: Acts as the frontline dispatcher - triage_agent = client.as_agent( + triage_agent = Agent( + client=client, instructions=( "You are frontline support triage. Route customer issues to the appropriate specialist agents " "based on the problem described." @@ -82,7 +83,8 @@ def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Age ) # Refund specialist: Handles refund requests - refund_agent = client.as_agent( + refund_agent = Agent( + client=client, instructions="You process refund requests.", name="refund_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -90,7 +92,8 @@ def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Age ) # Order/shipping specialist: Resolves delivery issues - order_agent = client.as_agent( + order_agent = Agent( + client=client, instructions="You handle order and shipping inquiries.", name="order_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -98,7 +101,8 @@ def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Age ) # Return specialist: Handles return requests - return_agent = client.as_agent( + return_agent = Agent( + client=client, instructions="You manage product return requests.", name="return_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -153,9 +157,9 @@ async def main() -> None: replace the scripted_responses with actual user input collection. """ # Initialize the Azure OpenAI chat client - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) @@ -170,20 +174,21 @@ async def main() -> None: # Without this, the default behavior continues requesting user input until max_turns # is reached. Here we use a custom condition that checks if the conversation has ended # naturally (when one of the agents says something like "you're welcome"). - agent = ( - HandoffBuilder( - name="customer_support_handoff", - participants=[triage, refund, order, support], - # Custom termination: Check if one of the agents has provided a closing message. - # This looks for the last message containing "welcome", which indicates the - # conversation has concluded naturally. - termination_condition=lambda conversation: ( - len(conversation) > 0 and "welcome" in conversation[-1].text.lower() - ), - ) - .with_start_agent(triage) - .build() - .as_agent() # Convert workflow to agent interface + agent = Agent( + client=( + HandoffBuilder( + name="customer_support_handoff", + participants=[triage, refund, order, support], + # Custom termination: Check if one of the agents has provided a closing message. + # This looks for the last message containing "welcome", which indicates the + # conversation has concluded naturally. + termination_condition=lambda conversation: ( + len(conversation) > 0 and "welcome" in conversation[-1].text.lower() + ), + ) + .with_start_agent(triage) + .build() + ), ) # Scripted user responses for reproducible demo diff --git a/python/samples/03-workflows/agents/magentic_workflow_as_agent.py b/python/samples/03-workflows/agents/magentic_workflow_as_agent.py index 833d89cd19..b0b576de51 100644 --- a/python/samples/03-workflows/agents/magentic_workflow_as_agent.py +++ b/python/samples/03-workflows/agents/magentic_workflow_as_agent.py @@ -6,7 +6,7 @@ from agent_framework import ( Agent, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import MagenticBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -18,12 +18,12 @@ Sample: Build a Magentic orchestration and wrap it as an agent. The script configures a Magentic workflow with streaming callbacks, then invokes the -orchestration through `workflow.as_agent(...)` so the entire Magentic loop can be reused +orchestration through `Agent(client=workflow, ...)` so the entire Magentic loop can be reused like any other agent while still emitting callback telemetry. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- OpenAI credentials configured for `AzureOpenAIResponsesClient` and `AzureOpenAIResponsesClient`. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- OpenAI credentials configured for `FoundryChatClient` and `FoundryChatClient`. """ @@ -35,17 +35,17 @@ async def main() -> None: "You are a Researcher. You find information without additional computation or quantitative analysis." ), # This agent requires the gpt-4o-search-preview model to perform web searches. - client=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ), ) # Create code interpreter tool using instance method - coder_client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + coder_client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) code_interpreter_tool = coder_client.get_code_interpreter_tool() @@ -63,9 +63,9 @@ async def main() -> None: name="MagenticManager", description="Orchestrator that coordinates the research and coding workflow", instructions="You coordinate a team to complete complex tasks efficiently.", - client=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ), ) @@ -98,7 +98,7 @@ async def main() -> None: try: # Wrap the workflow as an agent for composition scenarios print("\nWrapping workflow as an agent and running...") - workflow_agent = workflow.as_agent(name="MagenticWorkflowAgent") + workflow_agent = Agent(client=workflow, name="MagenticWorkflowAgent") last_response_id: str | None = None async for update in workflow_agent.run(task, stream=True): diff --git a/python/samples/03-workflows/agents/sequential_workflow_as_agent.py b/python/samples/03-workflows/agents/sequential_workflow_as_agent.py index 74f4fc568b..4123f0cff6 100644 --- a/python/samples/03-workflows/agents/sequential_workflow_as_agent.py +++ b/python/samples/03-workflows/agents/sequential_workflow_as_agent.py @@ -1,9 +1,9 @@ # Copyright (c) Microsoft. All rights reserved. - import asyncio import os -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -15,7 +15,7 @@ Sample: Build a sequential workflow orchestration and wrap it as an agent. The script assembles a sequential conversation flow with `SequentialBuilder`, then -invokes the entire orchestration through the `workflow.as_agent(...)` interface so +invokes the entire orchestration through the `Agent(client=workflow,...)` interface so other coordinators can reuse the chain as a single participant. Note on internal adapters: @@ -26,25 +26,25 @@ You can safely ignore them when focusing on agent progress. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI access configured for AzureOpenAIResponsesClient (use az login + env vars) +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI access configured for FoundryChatClient (use az login + env vars) """ async def main() -> None: # 1) Create agents - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - writer = client.as_agent( + writer = Agent(client=client, instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), name="writer", ) - reviewer = client.as_agent( + reviewer = Agent(client=client, instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), name="reviewer", ) @@ -53,7 +53,7 @@ async def main() -> None: workflow = SequentialBuilder(participants=[writer, reviewer]).build() # 3) Treat the workflow itself as an agent for follow-up invocations - agent = workflow.as_agent(name="SequentialWorkflowAgent") + agent = Agent(client=workflow, name="SequentialWorkflowAgent") prompt = "Write a tagline for a budget-friendly eBike." agent_response = await agent.run(prompt) diff --git a/python/samples/03-workflows/agents/workflow_as_agent_human_in_the_loop.py b/python/samples/03-workflows/agents/workflow_as_agent_human_in_the_loop.py index fc6bd2c0de..744d09769b 100644 --- a/python/samples/03-workflows/agents/workflow_as_agent_human_in_the_loop.py +++ b/python/samples/03-workflows/agents/workflow_as_agent_human_in_the_loop.py @@ -8,7 +8,8 @@ from pathlib import Path from typing import Any -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -48,8 +49,8 @@ to the Worker. The workflow completes when idle. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- OpenAI account configured and accessible for AzureOpenAIResponsesClient. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- OpenAI account configured and accessible for FoundryChatClient. - Familiarity with WorkflowBuilder, Executor, and WorkflowContext from agent_framework. - Understanding of request-response message handling in executors. - (Optional) Review of reflection and escalation patterns, such as those in @@ -110,20 +111,21 @@ async def main() -> None: # and escalation paths for human review. worker = Worker( id="worker", - client=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ), ) reviewer = ReviewerWithHumanInTheLoop(worker_id="worker") - agent = ( - WorkflowBuilder(start_executor=worker) - .add_edge(worker, reviewer) # Worker sends requests to Reviewer - .add_edge(reviewer, worker) # Reviewer sends feedback to Worker - .build() - .as_agent() # Convert workflow into an agent interface + agent = Agent( + client=( + WorkflowBuilder(start_executor=worker) + .add_edge(worker, reviewer) + .add_edge(reviewer, worker) + .build() + ), ) print("Running workflow agent with user query...") diff --git a/python/samples/03-workflows/agents/workflow_as_agent_kwargs.py b/python/samples/03-workflows/agents/workflow_as_agent_kwargs.py index eb46578b67..12c424d4c4 100644 --- a/python/samples/03-workflows/agents/workflow_as_agent_kwargs.py +++ b/python/samples/03-workflows/agents/workflow_as_agent_kwargs.py @@ -5,8 +5,8 @@ import os from typing import Annotated, Any -from agent_framework import tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -19,22 +19,22 @@ Sample: Workflow as Agent with kwargs Propagation to @tool Tools This sample demonstrates how to flow custom context (skill data, user tokens, etc.) -through a workflow exposed via .as_agent() to @tool functions using the **kwargs pattern. +through a workflow exposed Agent(client=via,) to @tool functions using the **kwargs pattern. Key Concepts: - Build a workflow using SequentialBuilder (or any builder pattern) -- Expose the workflow as a reusable agent via workflow.as_agent() +- Expose the workflow as a reusable agent via Agent(client=workflow,) - Pass custom context as kwargs when invoking workflow_agent.run() - kwargs are stored in State and propagated to all agent invocations - @tool functions receive kwargs via **kwargs parameter -When to use workflow.as_agent(): +When to use Agent(client=workflow,): - To treat an entire workflow orchestration as a single agent - To compose workflows into higher-level orchestrations - To maintain a consistent agent interface for callers Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - Environment variables configured """ @@ -87,14 +87,14 @@ async def main() -> None: print("=" * 70) # Create chat client - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create agent with tools that use kwargs - agent = client.as_agent( + agent = Agent(client=client, name="assistant", instructions=( "You are a helpful assistant. Use the available tools to help users. " @@ -107,8 +107,8 @@ async def main() -> None: # Build a sequential workflow workflow = SequentialBuilder(participants=[agent]).build() - # Expose the workflow as an agent using .as_agent() - workflow_agent = workflow.as_agent(name="WorkflowAgent") + # Expose the workflow as an agent Agent(client=using,) + workflow_agent = Agent(client=workflow, name="WorkflowAgent") # Define custom context that will flow to tools via kwargs custom_data = { diff --git a/python/samples/03-workflows/agents/workflow_as_agent_reflection_pattern.py b/python/samples/03-workflows/agents/workflow_as_agent_reflection_pattern.py index 4611ac45fa..e80b4ee303 100644 --- a/python/samples/03-workflows/agents/workflow_as_agent_reflection_pattern.py +++ b/python/samples/03-workflows/agents/workflow_as_agent_reflection_pattern.py @@ -6,6 +6,7 @@ from uuid import uuid4 from agent_framework import ( + Agent, AgentResponse, Executor, Message, @@ -14,7 +15,7 @@ WorkflowContext, handler, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import BaseModel @@ -39,8 +40,8 @@ - State management for pending requests and retry logic. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- OpenAI account configured and accessible for AzureOpenAIResponsesClient. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- OpenAI account configured and accessible for FoundryChatClient. - Familiarity with WorkflowBuilder, Executor, WorkflowContext, and event handling. - Understanding of how agent messages are generated, reviewed, and re-submitted. """ @@ -195,27 +196,28 @@ async def main() -> None: print("Building workflow with Worker ↔ Reviewer cycle...") worker = Worker( id="worker", - client=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ), ) reviewer = Reviewer( id="reviewer", - client=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ), ) - agent = ( - WorkflowBuilder(start_executor=worker) - .add_edge(worker, reviewer) # Worker sends responses to Reviewer - .add_edge(reviewer, worker) # Reviewer provides feedback to Worker - .build() - .as_agent() # Wrap workflow as an agent + agent = Agent( + client=( + WorkflowBuilder(start_executor=worker) + .add_edge(worker, reviewer) + .add_edge(reviewer, worker) + .build() + ), ) print("Running workflow agent with user query...") diff --git a/python/samples/03-workflows/agents/workflow_as_agent_with_session.py b/python/samples/03-workflows/agents/workflow_as_agent_with_session.py index 26fb4cff53..3b65cd94d7 100644 --- a/python/samples/03-workflows/agents/workflow_as_agent_with_session.py +++ b/python/samples/03-workflows/agents/workflow_as_agent_with_session.py @@ -3,8 +3,8 @@ import asyncio import os -from agent_framework import AgentSession, InMemoryHistoryProvider -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, AgentSession, InMemoryHistoryProvider +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -24,7 +24,7 @@ persistence, allowing workflows to be paused and resumed. Key concepts: -- Workflows can be wrapped as agents using workflow.as_agent() +- Workflows can be wrapped as agents using Agent(client=workflow,) - AgentSession preserves conversation history - Each call to agent.run() includes session history + new message - Participants in the workflow see the full conversation context @@ -37,20 +37,20 @@ - Long-running workflows that need pause/resume capability Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Environment variables configured for AzureOpenAIResponsesClient +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Environment variables configured for FoundryChatClient """ async def main() -> None: # Create a chat client - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - assistant = client.as_agent( + assistant = Agent(client=client, name="assistant", instructions=( "You are a helpful assistant. Answer questions based on the conversation " @@ -58,7 +58,7 @@ async def main() -> None: ), ) - summarizer = client.as_agent( + summarizer = Agent(client=client, name="summarizer", instructions=( "You are a summarizer. After the assistant responds, provide a brief " @@ -70,7 +70,7 @@ async def main() -> None: workflow = SequentialBuilder(participants=[assistant, summarizer]).build() # Wrap the workflow as an agent - agent = workflow.as_agent(name="ConversationalWorkflowAgent") + agent = Agent(client=workflow, name="ConversationalWorkflowAgent") # Create a session to maintain history session = agent.create_session() @@ -129,19 +129,19 @@ async def demonstrate_session_serialization() -> None: This shows how conversation history can be persisted and restored, enabling long-running conversational workflows. """ - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - memory_assistant = client.as_agent( + memory_assistant = Agent(client=client, name="memory_assistant", instructions="You are a helpful assistant with good memory. Remember details from our conversation.", ) workflow = SequentialBuilder(participants=[memory_assistant]).build() - agent = workflow.as_agent(name="MemoryWorkflowAgent") + agent = Agent(client=workflow, name="MemoryWorkflowAgent") # Create initial session and have a conversation session = agent.create_session() diff --git a/python/samples/03-workflows/checkpoint/checkpoint_with_human_in_the_loop.py b/python/samples/03-workflows/checkpoint/checkpoint_with_human_in_the_loop.py index ed0f46ee92..c39cb84c79 100644 --- a/python/samples/03-workflows/checkpoint/checkpoint_with_human_in_the_loop.py +++ b/python/samples/03-workflows/checkpoint/checkpoint_with_human_in_the_loop.py @@ -9,6 +9,7 @@ from typing import Any from agent_framework import ( + Agent, AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, @@ -21,7 +22,7 @@ handler, response_handler, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -178,11 +179,12 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow: # Wire the workflow DAG. Edges mirror the numbered steps described in the # module docstring. Because `WorkflowBuilder` is declarative, reading these # edges is often the quickest way to understand execution order. - writer_agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + writer_agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions="Write concise, warm release notes that sound human and helpful.", name="writer", ) diff --git a/python/samples/03-workflows/checkpoint/workflow_as_agent_checkpoint.py b/python/samples/03-workflows/checkpoint/workflow_as_agent_checkpoint.py index 82a0fc035e..33f71b442d 100644 --- a/python/samples/03-workflows/checkpoint/workflow_as_agent_checkpoint.py +++ b/python/samples/03-workflows/checkpoint/workflow_as_agent_checkpoint.py @@ -20,18 +20,19 @@ - These are complementary: sessions track conversation, checkpoints track workflow state Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Environment variables configured for AzureOpenAIResponsesClient +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Environment variables configured for FoundryChatClient """ import asyncio import os from agent_framework import ( + Agent, InMemoryCheckpointStorage, InMemoryHistoryProvider, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -47,24 +48,26 @@ async def basic_checkpointing() -> None: print("Basic Checkpointing with Workflow as Agent") print("=" * 60) - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - assistant = client.as_agent( + assistant = Agent( + client=client, name="assistant", instructions="You are a helpful assistant. Keep responses brief.", ) - reviewer = client.as_agent( + reviewer = Agent( + client=client, name="reviewer", instructions="You are a reviewer. Provide a one-sentence summary of the assistant's response.", ) workflow = SequentialBuilder(participants=[assistant, reviewer]).build() - agent = workflow.as_agent(name="CheckpointedAgent") + agent = Agent(client=workflow, name="CheckpointedAgent") # Create checkpoint storage checkpoint_storage = InMemoryCheckpointStorage() @@ -92,19 +95,20 @@ async def checkpointing_with_thread() -> None: print("Checkpointing with Thread Conversation History") print("=" * 60) - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - assistant = client.as_agent( + assistant = Agent( + client=client, name="memory_assistant", instructions="You are a helpful assistant with good memory. Reference previous conversation when relevant.", ) workflow = SequentialBuilder(participants=[assistant]).build() - agent = workflow.as_agent(name="MemoryAgent") + agent = Agent(client=workflow, name="MemoryAgent") # Create both session (for conversation) and checkpoint storage (for workflow state) session = agent.create_session() @@ -139,19 +143,20 @@ async def streaming_with_checkpoints() -> None: print("Streaming with Checkpointing") print("=" * 60) - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - assistant = client.as_agent( + assistant = Agent( + client=client, name="streaming_assistant", instructions="You are a helpful assistant.", ) workflow = SequentialBuilder(participants=[assistant]).build() - agent = workflow.as_agent(name="StreamingCheckpointAgent") + agent = Agent(client=workflow, name="StreamingCheckpointAgent") checkpoint_storage = InMemoryCheckpointStorage() diff --git a/python/samples/03-workflows/composition/sub_workflow_kwargs.py b/python/samples/03-workflows/composition/sub_workflow_kwargs.py index b002db1da1..d12993b1a9 100644 --- a/python/samples/03-workflows/composition/sub_workflow_kwargs.py +++ b/python/samples/03-workflows/composition/sub_workflow_kwargs.py @@ -6,11 +6,12 @@ from typing import Annotated, Any from agent_framework import ( + Agent, Message, WorkflowExecutor, tool, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -32,7 +33,7 @@ - Useful for passing authentication tokens, configuration, or request context Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - Environment variables configured """ @@ -81,14 +82,15 @@ async def main() -> None: print("=" * 70) # Create chat client - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create an agent with tools that use kwargs - inner_agent = client.as_agent( + inner_agent = Agent( + client=client, name="data_agent", instructions=( "You are a data access agent. Use the available tools to help users. " diff --git a/python/samples/03-workflows/control-flow/edge_condition.py b/python/samples/03-workflows/control-flow/edge_condition.py index 73476c61a8..4d1decd901 100644 --- a/python/samples/03-workflows/control-flow/edge_condition.py +++ b/python/samples/03-workflows/control-flow/edge_condition.py @@ -14,7 +14,7 @@ WorkflowContext, # Per-run context and event bus executor, # Decorator to declare a Python function as a workflow executor ) -from agent_framework.azure import AzureOpenAIResponsesClient # Thin client wrapper for Azure OpenAI chat models +from agent_framework.azure import FoundryChatClient # Thin client wrapper for Azure OpenAI chat models from azure.identity import AzureCliCredential # Uses your az CLI login for credentials from dotenv import load_dotenv from pydantic import BaseModel # Structured outputs for safer parsing @@ -36,10 +36,10 @@ - Illustrate how to transform one agent's structured result into a new AgentExecutorRequest for a downstream agent. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - You understand the basics of WorkflowBuilder, executors, and events in this framework. - You know the concept of edge conditions and how they gate routes using a predicate function. -- Azure OpenAI access is configured for AzureOpenAIResponsesClient. You should be logged in with Azure CLI (AzureCliCredential) +- Azure OpenAI access is configured for FoundryChatClient. You should be logged in with Azure CLI (AzureCliCredential) and have the Foundry V2 Project environment variables set as documented in the getting started chat client README. - The sample email resource file exists at workflow/resources/email.txt. @@ -136,11 +136,12 @@ async def to_email_assistant_request( def create_spam_detector_agent() -> Agent: """Helper to create a spam detection agent.""" # AzureCliCredential uses your current az login. This avoids embedding secrets in code. - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You are a spam detection assistant that identifies spam emails. " "Always return JSON with fields is_spam (bool), reason (string), and email_content (string). " @@ -154,11 +155,12 @@ def create_spam_detector_agent() -> Agent: def create_email_assistant_agent() -> Agent: """Helper to create an email assistant agent.""" # AzureCliCredential uses your current az login. This avoids embedding secrets in code. - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You are an email assistant that helps users draft professional responses to emails. " "Your input may be a JSON object that includes 'email_content'; base your reply on that content. " diff --git a/python/samples/03-workflows/control-flow/multi_selection_edge_group.py b/python/samples/03-workflows/control-flow/multi_selection_edge_group.py index 0b3d4ae43f..5cd700081b 100644 --- a/python/samples/03-workflows/control-flow/multi_selection_edge_group.py +++ b/python/samples/03-workflows/control-flow/multi_selection_edge_group.py @@ -20,7 +20,7 @@ WorkflowEvent, executor, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import BaseModel @@ -47,7 +47,7 @@ - Apply conditional persistence logic (short vs long emails). Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - Familiarity with WorkflowBuilder, executors, edges, and events. - Understanding of multi-selection edge groups and how their selection function maps to target ids. - Experience with workflow state for persisting and reusing objects. @@ -188,11 +188,12 @@ async def database_access(analysis: AnalysisResult, ctx: WorkflowContext[Never, def create_email_analysis_agent() -> Agent: """Creates the email analysis agent.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You are a spam detection assistant that identifies spam emails. " "Always return JSON with fields 'spam_decision' (one of NotSpam, Spam, Uncertain) " @@ -205,11 +206,12 @@ def create_email_analysis_agent() -> Agent: def create_email_assistant_agent() -> Agent: """Creates the email assistant agent.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=("You are an email assistant that helps users draft responses to emails with professionalism."), name="email_assistant_agent", default_options={"response_format": EmailResponse}, @@ -218,11 +220,12 @@ def create_email_assistant_agent() -> Agent: def create_email_summary_agent() -> Agent: """Creates the email summary agent.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=("You are an assistant that helps users summarize emails."), name="email_summary_agent", default_options={"response_format": EmailSummaryModel}, diff --git a/python/samples/03-workflows/control-flow/simple_loop.py b/python/samples/03-workflows/control-flow/simple_loop.py index 23bd3f2c70..4462abba13 100644 --- a/python/samples/03-workflows/control-flow/simple_loop.py +++ b/python/samples/03-workflows/control-flow/simple_loop.py @@ -16,7 +16,7 @@ WorkflowContext, handler, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -32,8 +32,8 @@ - The workflow completes when the correct number is guessed. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure AI/ Azure OpenAI for `AzureOpenAIResponsesClient` agent. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure AI/ Azure OpenAI for `FoundryChatClient` agent. - Authentication via `azure-identity` — uses `AzureCliCredential()` (run `az login`). """ @@ -123,11 +123,12 @@ async def parse(self, response: AgentExecutorResponse, ctx: WorkflowContext[Numb def create_judge_agent() -> Agent: """Create a judge agent that evaluates guesses.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=("You strictly respond with one of: MATCHED, ABOVE, BELOW based on the given target and guess."), name="judge_agent", ) diff --git a/python/samples/03-workflows/control-flow/switch_case_edge_group.py b/python/samples/03-workflows/control-flow/switch_case_edge_group.py index ccc8c57aca..e45bb05c92 100644 --- a/python/samples/03-workflows/control-flow/switch_case_edge_group.py +++ b/python/samples/03-workflows/control-flow/switch_case_edge_group.py @@ -18,7 +18,7 @@ WorkflowContext, # Per-run context and event bus executor, # Decorator to turn a function into a workflow executor ) -from agent_framework.azure import AzureOpenAIResponsesClient # Thin client for Azure OpenAI chat models +from agent_framework.azure import FoundryChatClient # Thin client for Azure OpenAI chat models from azure.identity import AzureCliCredential # Uses your az CLI login for credentials from dotenv import load_dotenv from pydantic import BaseModel # Structured outputs with validation @@ -43,10 +43,10 @@ - Use ctx.yield_output() to provide workflow results - the workflow completes when idle with no pending work. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - Familiarity with WorkflowBuilder, executors, edges, and events. - Understanding of switch-case edge groups and how Case and Default are evaluated in order. -- Working Azure OpenAI configuration for AzureOpenAIResponsesClient, with Azure CLI login and required environment variables. +- Working Azure OpenAI configuration for FoundryChatClient, with Azure CLI login and required environment variables. - Access to workflow/resources/ambiguous_email.txt, or accept the inline fallback string. """ @@ -159,11 +159,12 @@ async def handle_uncertain(detection: DetectionResult, ctx: WorkflowContext[Neve def create_spam_detection_agent() -> Agent: """Create and return the spam detection agent.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You are a spam detection assistant that identifies spam emails. " "Be less confident in your assessments. " @@ -177,11 +178,12 @@ def create_spam_detection_agent() -> Agent: def create_email_assistant_agent() -> Agent: """Create and return the email assistant agent.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=("You are an email assistant that helps users draft responses to emails with professionalism."), name="email_assistant_agent", default_options={"response_format": EmailResponse}, diff --git a/python/samples/03-workflows/declarative/agent_to_function_tool/main.py b/python/samples/03-workflows/declarative/agent_to_function_tool/main.py index d4346d826e..76108af735 100644 --- a/python/samples/03-workflows/declarative/agent_to_function_tool/main.py +++ b/python/samples/03-workflows/declarative/agent_to_function_tool/main.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """Agent to Function Tool sample - demonstrates chaining agent output to function tools. @@ -22,7 +23,7 @@ from pathlib import Path from typing import Any -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.declarative import WorkflowFactory from azure.identity import AzureCliCredential from pydantic import BaseModel, Field @@ -198,14 +199,14 @@ def format_order_confirmation(order_data: dict[str, Any], order_calculation: dic async def main(): """Run the agent to function tool workflow.""" # Create Azure OpenAI Responses client - chat_client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + chat_client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create the order analysis agent with structured output - order_analysis_agent = chat_client.as_agent( + order_analysis_agent = Agent(client=chat_client, name="OrderAnalysisAgent", instructions=ORDER_ANALYSIS_INSTRUCTIONS, default_options={"response_format": OrderAnalysis}, diff --git a/python/samples/03-workflows/declarative/customer_support/main.py b/python/samples/03-workflows/declarative/customer_support/main.py index 5d38725040..df75433aa4 100644 --- a/python/samples/03-workflows/declarative/customer_support/main.py +++ b/python/samples/03-workflows/declarative/customer_support/main.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """ CustomerSupport workflow sample. @@ -27,7 +28,7 @@ import uuid from pathlib import Path -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.declarative import ( AgentExternalInputRequest, AgentExternalInputResponse, @@ -168,49 +169,49 @@ async def main() -> None: plugin = TicketingPlugin() # Create Azure OpenAI client - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], # This sample has been tested only on `gpt-5.1` and may not work as intended on other models # This sample is known to fail on `gpt-5-mini` reasoning input (GH issue #4059) - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create agents with structured outputs - self_service_agent = client.as_agent( + self_service_agent = Agent(client=client, name="SelfServiceAgent", instructions=SELF_SERVICE_INSTRUCTIONS, default_options={"response_format": SelfServiceResponse}, ) - ticketing_agent = client.as_agent( + ticketing_agent = Agent(client=client, name="TicketingAgent", instructions=TICKETING_INSTRUCTIONS, tools=plugin.get_functions(), default_options={"response_format": TicketingResponse}, ) - routing_agent = client.as_agent( + routing_agent = Agent(client=client, name="TicketRoutingAgent", instructions=TICKET_ROUTING_INSTRUCTIONS, tools=[plugin.get_ticket], default_options={"response_format": RoutingResponse}, ) - windows_support_agent = client.as_agent( + windows_support_agent = Agent(client=client, name="WindowsSupportAgent", instructions=WINDOWS_SUPPORT_INSTRUCTIONS, tools=[plugin.get_ticket], default_options={"response_format": SupportResponse}, ) - resolution_agent = client.as_agent( + resolution_agent = Agent(client=client, name="TicketResolutionAgent", instructions=RESOLUTION_INSTRUCTIONS, tools=[plugin.resolve_ticket], ) - escalation_agent = client.as_agent( + escalation_agent = Agent(client=client, name="TicketEscalationAgent", instructions=ESCALATION_INSTRUCTIONS, tools=[plugin.get_ticket, plugin.send_notification], diff --git a/python/samples/03-workflows/declarative/customer_support/ticketing_plugin.py b/python/samples/03-workflows/declarative/customer_support/ticketing_plugin.py index f25f1b473d..5742ba0c4e 100644 --- a/python/samples/03-workflows/declarative/customer_support/ticketing_plugin.py +++ b/python/samples/03-workflows/declarative/customer_support/ticketing_plugin.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. """Ticketing plugin for CustomerSupport workflow.""" - import uuid from collections.abc import Callable from dataclasses import dataclass @@ -14,7 +13,6 @@ class TicketStatus(Enum): """Status of a support ticket.""" - OPEN = "open" IN_PROGRESS = "in_progress" RESOLVED = "resolved" @@ -24,7 +22,6 @@ class TicketStatus(Enum): @dataclass class TicketItem: """A support ticket.""" - id: str subject: str = "" description: str = "" @@ -34,7 +31,6 @@ class TicketItem: class TicketingPlugin: """Mock ticketing plugin for customer support workflow.""" - def __init__(self) -> None: self._ticket_store: dict[str, TicketItem] = {} diff --git a/python/samples/03-workflows/declarative/deep_research/main.py b/python/samples/03-workflows/declarative/deep_research/main.py index 62c6afc573..7edd7e5df4 100644 --- a/python/samples/03-workflows/declarative/deep_research/main.py +++ b/python/samples/03-workflows/declarative/deep_research/main.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """ DeepResearch workflow sample. @@ -25,7 +26,7 @@ import os from pathlib import Path -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.declarative import WorkflowFactory from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -124,45 +125,45 @@ class ManagerResponse(BaseModel): async def main() -> None: """Run the deep research workflow.""" # Create Azure OpenAI client - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create agents - research_agent = client.as_agent( + research_agent = Agent(client=client, name="ResearchAgent", instructions=RESEARCH_INSTRUCTIONS, ) - planner_agent = client.as_agent( + planner_agent = Agent(client=client, name="PlannerAgent", instructions=PLANNER_INSTRUCTIONS, ) - manager_agent = client.as_agent( + manager_agent = Agent(client=client, name="ManagerAgent", instructions=MANAGER_INSTRUCTIONS, default_options={"response_format": ManagerResponse}, ) - summary_agent = client.as_agent( + summary_agent = Agent(client=client, name="SummaryAgent", instructions=SUMMARY_INSTRUCTIONS, ) - knowledge_agent = client.as_agent( + knowledge_agent = Agent(client=client, name="KnowledgeAgent", instructions=KNOWLEDGE_INSTRUCTIONS, ) - coder_agent = client.as_agent( + coder_agent = Agent(client=client, name="CoderAgent", instructions=CODER_INSTRUCTIONS, ) - weather_agent = client.as_agent( + weather_agent = Agent(client=client, name="WeatherAgent", instructions=WEATHER_INSTRUCTIONS, ) diff --git a/python/samples/03-workflows/declarative/function_tools/main.py b/python/samples/03-workflows/declarative/function_tools/main.py index 4606afcefb..898acfb493 100644 --- a/python/samples/03-workflows/declarative/function_tools/main.py +++ b/python/samples/03-workflows/declarative/function_tools/main.py @@ -11,8 +11,8 @@ from pathlib import Path from typing import Annotated, Any -from agent_framework import FileCheckpointStorage, tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, FileCheckpointStorage, tool +from agent_framework.azure import FoundryChatClient from agent_framework_declarative import ExternalInputRequest, ExternalInputResponse, WorkflowFactory from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -69,12 +69,12 @@ def get_item_price(name: Annotated[str, Field(description="Menu item name")]) -> async def main(): # Create agent with tools - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - menu_agent = client.as_agent( + menu_agent = Agent(client=client, name="MenuAgent", instructions="Answer questions about menu items, specials, and prices.", tools=[get_menu, get_specials, get_item_price], diff --git a/python/samples/03-workflows/declarative/marketing/main.py b/python/samples/03-workflows/declarative/marketing/main.py index 26fcbd54dd..021d525cc9 100644 --- a/python/samples/03-workflows/declarative/marketing/main.py +++ b/python/samples/03-workflows/declarative/marketing/main.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """ Run the marketing copy workflow sample. @@ -16,7 +17,7 @@ import os from pathlib import Path -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.declarative import WorkflowFactory from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -54,21 +55,21 @@ async def main() -> None: """Run the marketing workflow with real Azure AI agents.""" - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - analyst_agent = client.as_agent( + analyst_agent = Agent(client=client, name="AnalystAgent", instructions=ANALYST_INSTRUCTIONS, ) - writer_agent = client.as_agent( + writer_agent = Agent(client=client, name="WriterAgent", instructions=WRITER_INSTRUCTIONS, ) - editor_agent = client.as_agent( + editor_agent = Agent(client=client, name="EditorAgent", instructions=EDITOR_INSTRUCTIONS, ) diff --git a/python/samples/03-workflows/declarative/simple_workflow/main.py b/python/samples/03-workflows/declarative/simple_workflow/main.py index 132a7a8a19..5e7752673f 100644 --- a/python/samples/03-workflows/declarative/simple_workflow/main.py +++ b/python/samples/03-workflows/declarative/simple_workflow/main.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. """Simple workflow sample - demonstrates basic variable setting and output.""" - import asyncio from pathlib import Path @@ -12,26 +11,19 @@ async def main() -> None: """Run the simple greeting workflow.""" # Create a workflow factory factory = WorkflowFactory() - # Load the workflow from YAML workflow_path = Path(__file__).parent / "workflow.yaml" workflow = factory.create_workflow_from_yaml_path(workflow_path) - print(f"Loaded workflow: {workflow.name}") print("-" * 40) - # Run with default name print("\nRunning with default name:") result = await workflow.run({}) for output in result.get_outputs(): print(f" Output: {output}") - # Run with a custom name print("\nRunning with custom name 'Alice':") result = await workflow.run({"name": "Alice"}) - for output in result.get_outputs(): - print(f" Output: {output}") - print("\n" + "-" * 40) print("Workflow completed!") diff --git a/python/samples/03-workflows/declarative/student_teacher/main.py b/python/samples/03-workflows/declarative/student_teacher/main.py index 815821d348..d4d8508e3c 100644 --- a/python/samples/03-workflows/declarative/student_teacher/main.py +++ b/python/samples/03-workflows/declarative/student_teacher/main.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """ Run the student-teacher (MathChat) workflow sample. @@ -15,7 +16,7 @@ Prerequisites: - Azure OpenAI deployment with chat completion capability - Environment variables: - AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry Agent Service (V2) project endpoint + FOUNDRY_PROJECT_ENDPOINT: Your Azure AI Foundry Agent Service (V2) project endpoint AZURE_AI_MODEL_DEPLOYMENT_NAME: Your model deployment name """ @@ -23,7 +24,7 @@ import os from pathlib import Path -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.declarative import WorkflowFactory from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -56,19 +57,19 @@ async def main() -> None: """Run the student-teacher workflow with real Azure AI agents.""" # Create chat client - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create student and teacher agents - student_agent = client.as_agent( + student_agent = Agent(client=client, name="StudentAgent", instructions=STUDENT_INSTRUCTIONS, ) - teacher_agent = client.as_agent( + teacher_agent = Agent(client=client, name="TeacherAgent", instructions=TEACHER_INSTRUCTIONS, ) diff --git a/python/samples/03-workflows/human-in-the-loop/agents_with_HITL.py b/python/samples/03-workflows/human-in-the-loop/agents_with_HITL.py index b7e6046d40..aed542e8ad 100644 --- a/python/samples/03-workflows/human-in-the-loop/agents_with_HITL.py +++ b/python/samples/03-workflows/human-in-the-loop/agents_with_HITL.py @@ -6,6 +6,7 @@ from dataclasses import dataclass, field from agent_framework import ( + Agent, AgentExecutorRequest, AgentExecutorResponse, AgentResponse, @@ -18,7 +19,7 @@ handler, response_handler, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from typing_extensions import Never @@ -42,8 +43,8 @@ - Handling human feedback and routing it to the appropriate agents. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Run `az login` before executing. """ @@ -168,21 +169,23 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: """Run the workflow and bridge human feedback between two agents.""" # Create the agents - writer_agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + writer_agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), name="writer_agent", instructions=("You are a marketing writer."), tool_choice="required", ) - final_editor_agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + final_editor_agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), name="final_editor_agent", instructions=( "You are an editor who polishes marketing copy after human approval. " diff --git a/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py b/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py index 83b0632f88..53b33e3dad 100644 --- a/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py +++ b/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py @@ -7,6 +7,7 @@ from typing import Annotated from agent_framework import ( + Agent, AgentExecutorResponse, Content, Executor, @@ -16,7 +17,7 @@ handler, tool, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from typing_extensions import Never @@ -51,7 +52,7 @@ - Handling approval requests during workflow execution. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - Azure AI Agent Service configured, along with the required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Basic familiarity with WorkflowBuilder, edges, events, request_info events (type='request_info'), and streaming runs. @@ -224,11 +225,12 @@ async def conclude_workflow( async def main() -> None: # Create agent - email_writer_agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + email_writer_agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), name="EmailWriter", instructions=("You are an excellent email assistant. You respond to incoming emails."), # tools with `approval_mode="always_require"` will trigger approval requests diff --git a/python/samples/03-workflows/human-in-the-loop/agents_with_declaration_only_tools.py b/python/samples/03-workflows/human-in-the-loop/agents_with_declaration_only_tools.py index 46cec3977e..3d1ff10c1d 100644 --- a/python/samples/03-workflows/human-in-the-loop/agents_with_declaration_only_tools.py +++ b/python/samples/03-workflows/human-in-the-loop/agents_with_declaration_only_tools.py @@ -16,7 +16,7 @@ 4. The workflow resumes — the agent sees the tool result and finishes. Prerequisites: - - AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. + - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - Azure OpenAI endpoint configured via environment variables. - `az login` for AzureCliCredential. """ @@ -26,8 +26,8 @@ import os from typing import Any -from agent_framework import Content, FunctionTool, WorkflowBuilder -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, Content, FunctionTool, WorkflowBuilder +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -51,11 +51,13 @@ async def main() -> None: - agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + _client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ) + agent = Agent( + client=_client, name="WeatherBot", instructions=( "You are a helpful weather assistant. " diff --git a/python/samples/03-workflows/human-in-the-loop/concurrent_request_info.py b/python/samples/03-workflows/human-in-the-loop/concurrent_request_info.py index 74e059b9f8..33a752a877 100644 --- a/python/samples/03-workflows/human-in-the-loop/concurrent_request_info.py +++ b/python/samples/03-workflows/human-in-the-loop/concurrent_request_info.py @@ -17,8 +17,8 @@ - Injecting human guidance for specific agents before aggregation Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables - Authentication via azure-identity (run az login before executing) """ @@ -28,11 +28,12 @@ from typing import Any from agent_framework import ( + Agent, AgentExecutorResponse, Message, WorkflowEvent, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import AgentRequestInfoResponse, ConcurrentBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -41,7 +42,7 @@ load_dotenv() # Store chat client at module level for aggregator access -_chat_client: AzureOpenAIResponsesClient | None = None +_chat_client: FoundryChatClient | None = None async def aggregate_with_synthesis(results: list[AgentExecutorResponse]) -> Any: @@ -148,14 +149,15 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: global _chat_client - _chat_client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + _chat_client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create agents that analyze from different perspectives - technical_analyst = _chat_client.as_agent( + technical_analyst = Agent( + client=_chat_client, name="technical_analyst", instructions=( "You are a technical analyst. When given a topic, provide a technical " @@ -164,7 +166,8 @@ async def main() -> None: ), ) - business_analyst = _chat_client.as_agent( + business_analyst = Agent( + client=_chat_client, name="business_analyst", instructions=( "You are a business analyst. When given a topic, provide a business " @@ -173,7 +176,8 @@ async def main() -> None: ), ) - user_experience_analyst = _chat_client.as_agent( + user_experience_analyst = Agent( + client=_chat_client, name="ux_analyst", instructions=( "You are a UX analyst. When given a topic, provide a user experience " diff --git a/python/samples/03-workflows/human-in-the-loop/group_chat_request_info.py b/python/samples/03-workflows/human-in-the-loop/group_chat_request_info.py index 8d1e4a0192..a6196ad02b 100644 --- a/python/samples/03-workflows/human-in-the-loop/group_chat_request_info.py +++ b/python/samples/03-workflows/human-in-the-loop/group_chat_request_info.py @@ -18,8 +18,8 @@ - Steering agent behavior with pre-agent human input Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables - Authentication via azure-identity (run az login before executing) """ @@ -29,11 +29,12 @@ from typing import cast from agent_framework import ( + Agent, AgentExecutorResponse, Message, WorkflowEvent, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import AgentRequestInfoResponse, GroupChatBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -96,14 +97,15 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create agents for a group discussion - optimist = client.as_agent( + optimist = Agent( + client=client, name="optimist", instructions=( "You are an optimistic team member. You see opportunities and potential " @@ -112,7 +114,8 @@ async def main() -> None: ), ) - pragmatist = client.as_agent( + pragmatist = Agent( + client=client, name="pragmatist", instructions=( "You are a pragmatic team member. You focus on practical implementation " @@ -121,7 +124,8 @@ async def main() -> None: ), ) - creative = client.as_agent( + creative = Agent( + client=client, name="creative", instructions=( "You are a creative team member. You propose innovative solutions and " @@ -131,7 +135,8 @@ async def main() -> None: ) # Orchestrator coordinates the discussion - orchestrator = client.as_agent( + orchestrator = Agent( + client=client, name="orchestrator", instructions=( "You are a discussion manager coordinating a team conversation between participants. " diff --git a/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py b/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py index f764de6cb7..490a461c5b 100644 --- a/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py +++ b/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from agent_framework import ( + Agent, AgentExecutorRequest, AgentExecutorResponse, AgentResponseUpdate, @@ -17,7 +18,7 @@ handler, response_handler, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import BaseModel @@ -42,8 +43,8 @@ - Driving the loop in application code with run and responses parameter. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming runs. """ @@ -196,11 +197,12 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: """Run the human-in-the-loop guessing game workflow.""" # Create agent and executor - guessing_agent = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + guessing_agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), name="GuessingAgent", instructions=( "You guess a number between 1 and 10. " diff --git a/python/samples/03-workflows/human-in-the-loop/sequential_request_info.py b/python/samples/03-workflows/human-in-the-loop/sequential_request_info.py index cfee77276d..2c7d0f5add 100644 --- a/python/samples/03-workflows/human-in-the-loop/sequential_request_info.py +++ b/python/samples/03-workflows/human-in-the-loop/sequential_request_info.py @@ -17,8 +17,8 @@ - Injecting responses back into the workflow via run(responses=..., stream=True) Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables - Authentication via azure-identity (run az login before executing) """ @@ -28,11 +28,12 @@ from typing import cast from agent_framework import ( + Agent, AgentExecutorResponse, Message, WorkflowEvent, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import AgentRequestInfoResponse, SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -93,19 +94,21 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create agents for a sequential document review workflow - drafter = client.as_agent( + drafter = Agent( + client=client, name="drafter", instructions=("You are a document drafter. When given a topic, create a brief draft (2-3 sentences)."), ) - editor = client.as_agent( + editor = Agent( + client=client, name="editor", instructions=( "You are an editor. Review the draft and make improvements. " @@ -113,7 +116,8 @@ async def main() -> None: ), ) - finalizer = client.as_agent( + finalizer = Agent( + client=client, name="finalizer", instructions=( "You are a finalizer. Take the edited content and create a polished final version. " diff --git a/python/samples/03-workflows/orchestrations/concurrent_agents.py b/python/samples/03-workflows/orchestrations/concurrent_agents.py index 78e56b38bf..4f3575e263 100644 --- a/python/samples/03-workflows/orchestrations/concurrent_agents.py +++ b/python/samples/03-workflows/orchestrations/concurrent_agents.py @@ -4,8 +4,8 @@ import os from typing import Any -from agent_framework import Message -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, Message +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import ConcurrentBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -27,22 +27,22 @@ - Workflow completion when idle with no pending work Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Familiarity with Workflow events (WorkflowEvent) """ async def main() -> None: - # 1) Create three domain agents using AzureOpenAIResponsesClient - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + # 1) Create three domain agents using FoundryChatClient + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - researcher = client.as_agent( + researcher = Agent(client=client, instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -50,7 +50,7 @@ async def main() -> None: name="researcher", ) - marketer = client.as_agent( + marketer = Agent(client=client, instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -58,7 +58,7 @@ async def main() -> None: name="marketer", ) - legal = client.as_agent( + legal = Agent(client=client, instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." diff --git a/python/samples/03-workflows/orchestrations/concurrent_custom_agent_executors.py b/python/samples/03-workflows/orchestrations/concurrent_custom_agent_executors.py index 3e0a9b63c6..a0a58faf4e 100644 --- a/python/samples/03-workflows/orchestrations/concurrent_custom_agent_executors.py +++ b/python/samples/03-workflows/orchestrations/concurrent_custom_agent_executors.py @@ -13,7 +13,7 @@ WorkflowContext, handler, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import ConcurrentBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -30,15 +30,15 @@ ConcurrentBuilder API and the default aggregator. Demonstrates: -- Executors that create their Agent in __init__ (via AzureOpenAIResponsesClient) +- Executors that create their Agent in __init__ (via FoundryChatClient) - A @handler that converts AgentExecutorRequest -> AgentExecutorResponse - ConcurrentBuilder(participants=[...]) to build fan-out/fan-in - Default aggregator returning list[Message] (one user + one assistant per agent) - Workflow completion when all participants become idle Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. """ @@ -46,8 +46,9 @@ class ResearcherExec(Executor): agent: Agent - def __init__(self, client: AzureOpenAIResponsesClient, id: str = "researcher"): - self.agent = client.as_agent( + def __init__(self, client: FoundryChatClient, id: str = "researcher"): + self.agent = Agent( + client=client, instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -66,8 +67,9 @@ async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExe class MarketerExec(Executor): agent: Agent - def __init__(self, client: AzureOpenAIResponsesClient, id: str = "marketer"): - self.agent = client.as_agent( + def __init__(self, client: FoundryChatClient, id: str = "marketer"): + self.agent = Agent( + client=client, instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -86,8 +88,9 @@ async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExe class LegalExec(Executor): agent: Agent - def __init__(self, client: AzureOpenAIResponsesClient, id: str = "legal"): - self.agent = client.as_agent( + def __init__(self, client: FoundryChatClient, id: str = "legal"): + self.agent = Agent( + client=client, instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." @@ -104,9 +107,9 @@ async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExe async def main() -> None: - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) diff --git a/python/samples/03-workflows/orchestrations/concurrent_custom_aggregator.py b/python/samples/03-workflows/orchestrations/concurrent_custom_aggregator.py index b622e0f6b7..4b44111538 100644 --- a/python/samples/03-workflows/orchestrations/concurrent_custom_aggregator.py +++ b/python/samples/03-workflows/orchestrations/concurrent_custom_aggregator.py @@ -4,8 +4,8 @@ import os from typing import Any -from agent_framework import Message -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, Message +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import ConcurrentBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -18,7 +18,7 @@ Build a concurrent workflow with ConcurrentBuilder that fans out one prompt to multiple domain agents and fans in their responses. Override the default -aggregator with a custom async callback that uses AzureOpenAIResponsesClient.get_response() +aggregator with a custom async callback that uses FoundryChatClient.get_response() to synthesize a concise, consolidated summary from the experts' outputs. The workflow completes when all participants become idle. @@ -29,34 +29,34 @@ - Workflow output yielded with the synthesized summary string Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. """ async def main() -> None: - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - researcher = client.as_agent( + researcher = Agent(client=client, instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." ), name="researcher", ) - marketer = client.as_agent( + marketer = Agent(client=client, instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." ), name="marketer", ) - legal = client.as_agent( + legal = Agent(client=client, instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." diff --git a/python/samples/03-workflows/orchestrations/group_chat_agent_manager.py b/python/samples/03-workflows/orchestrations/group_chat_agent_manager.py index 2fecc34282..b80c16717a 100644 --- a/python/samples/03-workflows/orchestrations/group_chat_agent_manager.py +++ b/python/samples/03-workflows/orchestrations/group_chat_agent_manager.py @@ -9,7 +9,7 @@ AgentResponseUpdate, Message, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import GroupChatBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -26,8 +26,8 @@ - Coordinates a researcher and writer agent to solve tasks collaboratively Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. """ @@ -43,9 +43,9 @@ async def main() -> None: # Create a Responses client using Azure OpenAI and Azure CLI credentials for all agents - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) diff --git a/python/samples/03-workflows/orchestrations/group_chat_philosophical_debate.py b/python/samples/03-workflows/orchestrations/group_chat_philosophical_debate.py index a8dd2aebfe..dc76a0c362 100644 --- a/python/samples/03-workflows/orchestrations/group_chat_philosophical_debate.py +++ b/python/samples/03-workflows/orchestrations/group_chat_philosophical_debate.py @@ -10,7 +10,7 @@ AgentResponseUpdate, Message, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import GroupChatBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -39,8 +39,8 @@ - Doctor from Scandinavia (public health, equity, societal support) Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. """ @@ -48,10 +48,10 @@ load_dotenv() -def _get_chat_client() -> AzureOpenAIResponsesClient: - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], +def _get_chat_client() -> FoundryChatClient: + return FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) diff --git a/python/samples/03-workflows/orchestrations/group_chat_simple_selector.py b/python/samples/03-workflows/orchestrations/group_chat_simple_selector.py index 984b46c6a4..0cdb676cba 100644 --- a/python/samples/03-workflows/orchestrations/group_chat_simple_selector.py +++ b/python/samples/03-workflows/orchestrations/group_chat_simple_selector.py @@ -9,7 +9,7 @@ AgentResponseUpdate, Message, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import GroupChatBuilder, GroupChatState from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -25,8 +25,8 @@ - Uses a pure Python function to control speaker selection based on conversation state Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. """ @@ -40,9 +40,9 @@ def round_robin_selector(state: GroupChatState) -> str: async def main() -> None: # Create a Responses client using Azure OpenAI and Azure CLI credentials for all agents - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) diff --git a/python/samples/03-workflows/orchestrations/handoff_autonomous.py b/python/samples/03-workflows/orchestrations/handoff_autonomous.py index 7b86e73cf8..e15f5e20d4 100644 --- a/python/samples/03-workflows/orchestrations/handoff_autonomous.py +++ b/python/samples/03-workflows/orchestrations/handoff_autonomous.py @@ -11,7 +11,7 @@ Message, resolve_agent_id, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import HandoffBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -29,8 +29,8 @@ User -> Coordinator -> Specialist (iterates N times) -> Handoff -> Final Output Prerequisites: - - AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - - Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. + - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. + - Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run `az login` before executing the sample. Key Concepts: @@ -43,10 +43,11 @@ def create_agents( - client: AzureOpenAIResponsesClient, + client: FoundryChatClient, ) -> tuple[Agent, Agent, Agent]: """Create coordinator and specialists for autonomous iteration.""" - coordinator = client.as_agent( + coordinator = Agent( + client=client, instructions=( "You are a coordinator. You break down a user query into a research task and a summary task. " "Assign the two tasks to the appropriate specialists, one after the other." @@ -54,7 +55,8 @@ def create_agents( name="coordinator", ) - research_agent = client.as_agent( + research_agent = Agent( + client=client, instructions=( "You are a research specialist that explores topics thoroughly using web search. " "When given a research task, break it down into multiple aspects and explore each one. " @@ -66,7 +68,8 @@ def create_agents( name="research_agent", ) - summary_agent = client.as_agent( + summary_agent = Agent( + client=client, instructions=( "You summarize research findings. Provide a concise, well-organized summary. When done, return " "control to the coordinator." @@ -79,9 +82,9 @@ def create_agents( async def main() -> None: """Run an autonomous handoff workflow with specialist iteration enabled.""" - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) coordinator, research_agent, summary_agent = create_agents(client) diff --git a/python/samples/03-workflows/orchestrations/handoff_simple.py b/python/samples/03-workflows/orchestrations/handoff_simple.py index 1b4820ccbe..afbe33aa10 100644 --- a/python/samples/03-workflows/orchestrations/handoff_simple.py +++ b/python/samples/03-workflows/orchestrations/handoff_simple.py @@ -12,7 +12,7 @@ WorkflowRunState, tool, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -26,8 +26,8 @@ them to transfer control to each other based on the conversation context. Prerequisites: - - AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - - Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. + - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. + - Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run `az login` before executing the sample. Key Concepts: @@ -60,17 +60,18 @@ def process_return(order_number: Annotated[str, "Order number to process return return f"Return initiated successfully for order {order_number}. You will receive return instructions via email." -def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Agent, Agent]: +def create_agents(client: FoundryChatClient) -> tuple[Agent, Agent, Agent, Agent]: """Create and configure the triage and specialist agents. Args: - client: The AzureOpenAIResponsesClient to use for creating agents. + client: The FoundryChatClient to use for creating agents. Returns: Tuple of (triage_agent, refund_agent, order_agent, return_agent) """ # Triage agent: Acts as the frontline dispatcher - triage_agent = client.as_agent( + triage_agent = Agent( + client=client, instructions=( "You are frontline support triage. Route customer issues to the appropriate specialist agents " "based on the problem described." @@ -79,7 +80,8 @@ def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Age ) # Refund specialist: Handles refund requests - refund_agent = client.as_agent( + refund_agent = Agent( + client=client, instructions="You process refund requests.", name="refund_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -87,7 +89,8 @@ def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Age ) # Order/shipping specialist: Resolves delivery issues - order_agent = client.as_agent( + order_agent = Agent( + client=client, instructions="You handle order and shipping inquiries.", name="order_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -95,7 +98,8 @@ def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Age ) # Return specialist: Handles return requests - return_agent = client.as_agent( + return_agent = Agent( + client=client, instructions="You manage product return requests.", name="return_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -195,9 +199,9 @@ async def main() -> None: replace the scripted_responses with actual user input collection. """ # Initialize the Azure OpenAI Responses client - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) diff --git a/python/samples/03-workflows/orchestrations/handoff_with_code_interpreter_file.py b/python/samples/03-workflows/orchestrations/handoff_with_code_interpreter_file.py index 627033e26d..d343b91487 100644 --- a/python/samples/03-workflows/orchestrations/handoff_with_code_interpreter_file.py +++ b/python/samples/03-workflows/orchestrations/handoff_with_code_interpreter_file.py @@ -12,7 +12,7 @@ HandoffBuilder workflows can be properly retrieved. Prerequisites: - - AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. + - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - `az login` (Azure CLI authentication) - AZURE_AI_MODEL_DEPLOYMENT_NAME """ @@ -23,12 +23,13 @@ from typing import cast from agent_framework import ( + Agent, AgentResponseUpdate, Message, WorkflowEvent, WorkflowRunState, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -90,13 +91,14 @@ async def main() -> None: """Run a simple handoff workflow with code interpreter file generation.""" print("=== Handoff Workflow with Code Interpreter File Generation ===\n") - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - triage = client.as_agent( + triage = Agent( + client=client, name="triage_agent", instructions=( "You are a triage agent. Route code-related requests to the code_specialist. " @@ -107,7 +109,8 @@ async def main() -> None: code_interpreter_tool = client.get_code_interpreter_tool() - code_specialist = client.as_agent( + code_specialist = Agent( + client=client, name="code_specialist", instructions=( "You are a Python code specialist. Use the code interpreter to execute Python code " diff --git a/python/samples/03-workflows/orchestrations/handoff_with_tool_approval_checkpoint_resume.py b/python/samples/03-workflows/orchestrations/handoff_with_tool_approval_checkpoint_resume.py index 1c47b2d1f5..6296a900a7 100644 --- a/python/samples/03-workflows/orchestrations/handoff_with_tool_approval_checkpoint_resume.py +++ b/python/samples/03-workflows/orchestrations/handoff_with_tool_approval_checkpoint_resume.py @@ -14,7 +14,7 @@ WorkflowEvent, tool, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -45,9 +45,9 @@ workflow.run(stream=True, checkpoint_id=..., responses=responses).) Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - Azure CLI authentication (az login). -- Environment variables configured for AzureOpenAIResponsesClient. +- Environment variables configured for FoundryChatClient. """ CHECKPOINT_DIR = Path(__file__).parent / "tmp" / "handoff_checkpoints" @@ -60,10 +60,11 @@ def submit_refund(refund_description: str, amount: str, order_id: str) -> str: return f"refund recorded for order {order_id} (amount: {amount}) with details: {refund_description}" -def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Agent]: +def create_agents(client: FoundryChatClient) -> tuple[Agent, Agent, Agent]: """Create a simple handoff scenario: triage, refund, and order specialists.""" - triage = client.as_agent( + triage = Agent( + client=client, name="triage_agent", instructions=( "You are a customer service triage agent. Listen to customer issues and determine " @@ -72,7 +73,8 @@ def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Age ), ) - refund = client.as_agent( + refund = Agent( + client=client, name="refund_agent", instructions=( "You are a refund specialist. Help customers with refund requests. " @@ -83,7 +85,8 @@ def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Age tools=[submit_refund], ) - order = client.as_agent( + order = Agent( + client=client, name="order_agent", instructions=( "You are an order tracking specialist. Help customers track their orders. " @@ -97,9 +100,9 @@ def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Age def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow: """Build the handoff workflow with checkpointing enabled.""" - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) triage, refund, order = create_agents(client) diff --git a/python/samples/03-workflows/orchestrations/magentic.py b/python/samples/03-workflows/orchestrations/magentic.py index b412fd0b9d..4988bed22e 100644 --- a/python/samples/03-workflows/orchestrations/magentic.py +++ b/python/samples/03-workflows/orchestrations/magentic.py @@ -12,7 +12,7 @@ Message, WorkflowEvent, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import GroupChatRequestSentEvent, MagenticBuilder, MagenticProgressLedger from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -42,8 +42,8 @@ events, and prints the final answer. The workflow completes when idle. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. """ @@ -52,9 +52,9 @@ async def main() -> None: - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) diff --git a/python/samples/03-workflows/orchestrations/magentic_checkpoint.py b/python/samples/03-workflows/orchestrations/magentic_checkpoint.py index ab3da11c1d..f05d22f807 100644 --- a/python/samples/03-workflows/orchestrations/magentic_checkpoint.py +++ b/python/samples/03-workflows/orchestrations/magentic_checkpoint.py @@ -15,7 +15,7 @@ WorkflowEvent, WorkflowRunState, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -39,8 +39,8 @@ `responses` mapping so we can inject the stored human reply during restoration. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. """ @@ -64,9 +64,9 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage): name="ResearcherAgent", description="Collects background facts and references for the project.", instructions=("You are the research lead. Gather crisp bullet points the team should know."), - client=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ), ) @@ -75,9 +75,9 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage): name="WriterAgent", description="Synthesizes the final brief for stakeholders.", instructions=("You convert the research notes into a structured brief with milestones and risks."), - client=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ), ) @@ -87,9 +87,9 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage): name="MagenticManager", description="Orchestrator that coordinates the research and writing workflow", instructions="You coordinate a team to complete complex tasks efficiently.", - client=AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ), ) diff --git a/python/samples/03-workflows/orchestrations/magentic_human_plan_review.py b/python/samples/03-workflows/orchestrations/magentic_human_plan_review.py index 61f1cd412d..a80dd01a35 100644 --- a/python/samples/03-workflows/orchestrations/magentic_human_plan_review.py +++ b/python/samples/03-workflows/orchestrations/magentic_human_plan_review.py @@ -12,7 +12,7 @@ Message, WorkflowEvent, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest, MagenticPlanReviewResponse from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -37,8 +37,8 @@ - revise(feedback): Provide textual feedback to modify the plan Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. """ @@ -100,9 +100,9 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) diff --git a/python/samples/03-workflows/orchestrations/sequential_agents.py b/python/samples/03-workflows/orchestrations/sequential_agents.py index 916ecbee9c..b7da904bc6 100644 --- a/python/samples/03-workflows/orchestrations/sequential_agents.py +++ b/python/samples/03-workflows/orchestrations/sequential_agents.py @@ -4,8 +4,8 @@ import os from typing import cast -from agent_framework import Message -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, Message +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -29,26 +29,26 @@ You can safely ignore them when focusing on agent progress. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. """ async def main() -> None: # 1) Create agents - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - writer = client.as_agent( + writer = Agent(client=client, instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), name="writer", ) - reviewer = client.as_agent( + reviewer = Agent(client=client, instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), name="reviewer", ) diff --git a/python/samples/03-workflows/orchestrations/sequential_custom_executors.py b/python/samples/03-workflows/orchestrations/sequential_custom_executors.py index b46971cffe..a703ee9c51 100644 --- a/python/samples/03-workflows/orchestrations/sequential_custom_executors.py +++ b/python/samples/03-workflows/orchestrations/sequential_custom_executors.py @@ -5,13 +5,14 @@ from typing import Any from agent_framework import ( + Agent, AgentExecutorResponse, Executor, Message, WorkflowContext, handler, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -33,8 +34,8 @@ - Emit the updated conversation via ctx.send_message([...]) Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. """ @@ -65,12 +66,13 @@ async def summarize(self, agent_response: AgentExecutorResponse, ctx: WorkflowCo async def main() -> None: # 1) Create a content agent - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - content = client.as_agent( + content = Agent( + client=client, instructions="Produce a concise paragraph answering the user's request.", name="content", ) diff --git a/python/samples/03-workflows/parallelism/fan_out_fan_in_edges.py b/python/samples/03-workflows/parallelism/fan_out_fan_in_edges.py index 0e45e70ada..aa811b9932 100644 --- a/python/samples/03-workflows/parallelism/fan_out_fan_in_edges.py +++ b/python/samples/03-workflows/parallelism/fan_out_fan_in_edges.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from agent_framework import ( + Agent, AgentExecutor, # Wraps a ChatAgent as an Executor for use in workflows AgentExecutorRequest, # The message bundle sent to an AgentExecutor AgentExecutorResponse, # The structured result returned by an AgentExecutor @@ -15,7 +16,7 @@ WorkflowContext, # Per run context and event bus handler, # Decorator to mark an Executor method as invokable ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential # Uses your az CLI login for credentials from dotenv import load_dotenv from typing_extensions import Never @@ -35,9 +36,9 @@ - Fan in by collecting a list of AgentExecutorResponse objects and reducing them to a single result. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - Familiarity with WorkflowBuilder, executors, edges, events, and streaming runs. -- Azure OpenAI access configured for AzureOpenAIResponsesClient. Log in with Azure CLI and set any required environment variables. +- Azure OpenAI access configured for FoundryChatClient. Log in with Azure CLI and set any required environment variables. - Comfort reading AgentExecutorResponse.agent_response.text for assistant output aggregation. """ @@ -114,11 +115,12 @@ async def main() -> None: aggregator = AggregateInsights(id="aggregator") researcher = AgentExecutor( - AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -127,11 +129,12 @@ async def main() -> None: ) ) marketer = AgentExecutor( - AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -140,11 +143,12 @@ async def main() -> None: ) ) legal = AgentExecutor( - AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." diff --git a/python/samples/03-workflows/state-management/state_with_agents.py b/python/samples/03-workflows/state-management/state_with_agents.py index ad2fb7112d..7f7a6f8ac7 100644 --- a/python/samples/03-workflows/state-management/state_with_agents.py +++ b/python/samples/03-workflows/state-management/state_with_agents.py @@ -16,7 +16,7 @@ WorkflowContext, executor, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from pydantic import BaseModel @@ -39,8 +39,8 @@ - Compose agent backed executors with function style executors and yield the final output when the workflow completes. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Familiarity with WorkflowBuilder, executors, conditional edges, and streaming runs. """ @@ -162,11 +162,12 @@ async def handle_spam(detection: DetectionResult, ctx: WorkflowContext[Never, st def create_spam_detection_agent() -> Agent: """Creates a spam detection agent.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You are a spam detection assistant that identifies spam emails. " "Always return JSON with fields is_spam (bool) and reason (string)." @@ -179,11 +180,12 @@ def create_spam_detection_agent() -> Agent: def create_email_assistant_agent() -> Agent: """Creates an email assistant agent.""" - return AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + return Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You are an email assistant that helps users draft responses to emails with professionalism. " "Return JSON with a single field 'response' containing the drafted reply." diff --git a/python/samples/03-workflows/state-management/workflow_kwargs.py b/python/samples/03-workflows/state-management/workflow_kwargs.py index 12ed57b628..aaff1b07c1 100644 --- a/python/samples/03-workflows/state-management/workflow_kwargs.py +++ b/python/samples/03-workflows/state-management/workflow_kwargs.py @@ -5,8 +5,8 @@ import os from typing import Annotated, Any, cast -from agent_framework import Message, tool -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, Message, tool +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -28,7 +28,7 @@ - Works with Sequential, Concurrent, GroupChat, Handoff, and Magentic patterns Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - Environment variables configured """ @@ -81,14 +81,14 @@ async def main() -> None: print("=" * 70) # Create chat client - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) # Create agent with tools that use kwargs - agent = client.as_agent( + agent = Agent(client=client, name="assistant", instructions=( "You are a helpful assistant. Use the available tools to help users. " diff --git a/python/samples/03-workflows/tool-approval/concurrent_builder_tool_approval.py b/python/samples/03-workflows/tool-approval/concurrent_builder_tool_approval.py index c6a83c93a6..2f5b660106 100644 --- a/python/samples/03-workflows/tool-approval/concurrent_builder_tool_approval.py +++ b/python/samples/03-workflows/tool-approval/concurrent_builder_tool_approval.py @@ -6,12 +6,13 @@ from typing import Annotated from agent_framework import ( + Agent, Content, Message, WorkflowEvent, tool, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import ConcurrentBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -44,7 +45,7 @@ - Understanding that approval pauses only the agent that triggered it, not all agents. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - OpenAI or Azure OpenAI configured with the required environment variables. - Basic familiarity with ConcurrentBuilder and streaming workflow events. """ @@ -133,13 +134,14 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: # 3. Create two agents focused on different stocks but with the same tool sets - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - microsoft_agent = client.as_agent( + microsoft_agent = Agent( + client=client, name="MicrosoftAgent", instructions=( "You are a personal trading assistant focused on Microsoft (MSFT). " @@ -148,7 +150,8 @@ async def main() -> None: tools=[get_stock_price, get_market_sentiment, get_portfolio_balance, execute_trade], ) - google_agent = client.as_agent( + google_agent = Agent( + client=client, name="GoogleAgent", instructions=( "You are a personal trading assistant focused on Google (GOOGL). " diff --git a/python/samples/03-workflows/tool-approval/group_chat_builder_tool_approval.py b/python/samples/03-workflows/tool-approval/group_chat_builder_tool_approval.py index 7f384bb4cd..efa1151202 100644 --- a/python/samples/03-workflows/tool-approval/group_chat_builder_tool_approval.py +++ b/python/samples/03-workflows/tool-approval/group_chat_builder_tool_approval.py @@ -6,12 +6,13 @@ from typing import Annotated, cast from agent_framework import ( + Agent, Content, Message, WorkflowEvent, tool, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import GroupChatBuilder, GroupChatState from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -43,7 +44,7 @@ - Multi-round group chat with tool approval interruption and resumption. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - OpenAI or Azure OpenAI configured with the required environment variables. - Basic familiarity with GroupChatBuilder and streaming workflow events. """ @@ -133,13 +134,14 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: # 3. Create specialized agents - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - qa_engineer = client.as_agent( + qa_engineer = Agent( + client=client, name="QAEngineer", instructions=( "You are a QA engineer responsible for running tests before deployment. " @@ -148,7 +150,8 @@ async def main() -> None: tools=[run_tests], ) - devops_engineer = client.as_agent( + devops_engineer = Agent( + client=client, name="DevOpsEngineer", instructions=( "You are a DevOps engineer responsible for deployments. First check staging " diff --git a/python/samples/03-workflows/tool-approval/sequential_builder_tool_approval.py b/python/samples/03-workflows/tool-approval/sequential_builder_tool_approval.py index c3ad0cf011..73be7dbabb 100644 --- a/python/samples/03-workflows/tool-approval/sequential_builder_tool_approval.py +++ b/python/samples/03-workflows/tool-approval/sequential_builder_tool_approval.py @@ -6,12 +6,13 @@ from typing import Annotated, cast from agent_framework import ( + Agent, Content, Message, WorkflowEvent, tool, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -44,7 +45,7 @@ - Resuming workflow execution after approval via run(responses=..., stream=True). Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - OpenAI or Azure OpenAI configured with the required environment variables. - Basic familiarity with SequentialBuilder and streaming workflow events. """ @@ -106,12 +107,13 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: # 2. Create the agent with tools (approval mode is set per-tool via decorator) - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - database_agent = client.as_agent( + database_agent = Agent( + client=client, name="DatabaseAgent", instructions=( "You are a database assistant. You can view the database schema and execute " diff --git a/python/samples/03-workflows/visualization/concurrent_with_visualization.py b/python/samples/03-workflows/visualization/concurrent_with_visualization.py index 2786e792a2..70b4188bea 100644 --- a/python/samples/03-workflows/visualization/concurrent_with_visualization.py +++ b/python/samples/03-workflows/visualization/concurrent_with_visualization.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from agent_framework import ( + Agent, AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, @@ -15,7 +16,7 @@ WorkflowViz, handler, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv from typing_extensions import Never @@ -32,8 +33,8 @@ - Visualization: generate Mermaid and GraphViz representations via `WorkflowViz` and optionally export SVG. Prerequisites: -- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. -- Azure AI/ Azure OpenAI for `AzureOpenAIResponsesClient` agents. +- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. +- Azure AI/ Azure OpenAI for `FoundryChatClient` agents. - Authentication via `azure-identity` — uses `AzureCliCredential()` (run `az login`). - For visualization export: `pip install graphviz>=0.20.0` and install GraphViz binaries. """ @@ -96,11 +97,12 @@ async def main() -> None: # Create agent instances researcher = AgentExecutor( - AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -110,11 +112,12 @@ async def main() -> None: ) marketer = AgentExecutor( - AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -124,11 +127,12 @@ async def main() -> None: ) legal = AgentExecutor( - AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), - ).as_agent( + ), instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." diff --git a/python/samples/04-hosting/a2a/a2a_server.py b/python/samples/04-hosting/a2a/a2a_server.py index d797bef95d..57be5db769 100644 --- a/python/samples/04-hosting/a2a/a2a_server.py +++ b/python/samples/04-hosting/a2a/a2a_server.py @@ -10,7 +10,7 @@ from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore from agent_definitions import AGENT_CARD_FACTORIES, AGENT_FACTORIES from agent_executor import AgentFrameworkExecutor -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -35,8 +35,8 @@ uv run python a2a_server.py --agent-type logistics --port 5002 Environment variables: - AZURE_AI_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint - AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME — Model deployment name (e.g. gpt-4o) + FOUNDRY_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint + FOUNDRY_MODEL — Model deployment name (e.g. gpt-4o) """ @@ -66,21 +66,21 @@ def main() -> None: args = parse_args() # Validate environment - project_endpoint = os.getenv("AZURE_AI_PROJECT_ENDPOINT") - deployment_name = os.getenv("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME") + project_endpoint = os.getenv("FOUNDRY_PROJECT_ENDPOINT") + deployment_name = os.getenv("FOUNDRY_MODEL") if not project_endpoint: - print("Error: AZURE_AI_PROJECT_ENDPOINT environment variable is not set.") + print("Error: FOUNDRY_PROJECT_ENDPOINT environment variable is not set.") sys.exit(1) if not deployment_name: - print("Error: AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME environment variable is not set.") + print("Error: FOUNDRY_MODEL environment variable is not set.") sys.exit(1) # Create the LLM client credential = AzureCliCredential() - client = AzureOpenAIResponsesClient( + client = FoundryChatClient( project_endpoint=project_endpoint, - deployment_name=deployment_name, + model=deployment_name, credential=credential, ) diff --git a/python/samples/04-hosting/a2a/agent_definitions.py b/python/samples/04-hosting/a2a/agent_definitions.py index b0e87e485f..08e2edaced 100644 --- a/python/samples/04-hosting/a2a/agent_definitions.py +++ b/python/samples/04-hosting/a2a/agent_definitions.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from agent_framework import Agent - from agent_framework.azure import AzureOpenAIResponsesClient + from agent_framework.azure import FoundryChatClient # --------------------------------------------------------------------------- @@ -54,26 +54,26 @@ # --------------------------------------------------------------------------- -def create_invoice_agent(client: AzureOpenAIResponsesClient) -> Agent: +def create_invoice_agent(client: FoundryChatClient) -> Agent: """Create an invoice agent backed by the given client with query tools.""" - return client.as_agent( + return Agent(client=client, name="InvoiceAgent", instructions=INVOICE_INSTRUCTIONS, tools=[query_invoices, query_by_transaction_id, query_by_invoice_id], ) -def create_policy_agent(client: AzureOpenAIResponsesClient) -> Agent: +def create_policy_agent(client: FoundryChatClient) -> Agent: """Create a policy agent backed by the given client.""" - return client.as_agent( + return Agent(client=client, name="PolicyAgent", instructions=POLICY_INSTRUCTIONS, ) -def create_logistics_agent(client: AzureOpenAIResponsesClient) -> Agent: +def create_logistics_agent(client: FoundryChatClient) -> Agent: """Create a logistics agent backed by the given client.""" - return client.as_agent( + return Agent(client=client, name="LogisticsAgent", instructions=LOGISTICS_INSTRUCTIONS, ) diff --git a/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py b/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py index 03e4a4d20e..a7ed6a2e8e 100644 --- a/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py +++ b/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py @@ -1,16 +1,17 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """Host a single Azure OpenAI-powered agent inside Azure Functions. Components used in this sample: -- AzureOpenAIChatClient to call the Azure OpenAI chat deployment. +- FoundryChatClient to call the Azure OpenAI chat deployment. - AgentFunctionApp to expose HTTP endpoints via the Durable Functions extension. -Prerequisites: set `AZURE_OPENAI_ENDPOINT` and `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME` (plus `AZURE_OPENAI_API_KEY` or Azure CLI authentication) before starting the Functions host.""" +Prerequisites: set `AZURE_OPENAI_ENDPOINT` and `FOUNDRY_MODEL` (plus `AZURE_OPENAI_API_KEY` or Azure CLI authentication) before starting the Functions host.""" from typing import Any -from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient +from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -21,7 +22,8 @@ # 1. Instantiate the agent with the chosen deployment and instructions. def _create_agent() -> Any: """Create the Joker agent.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name="Joker", instructions="You are good at telling jokes.", ) diff --git a/python/samples/04-hosting/azure_functions/02_multi_agent/function_app.py b/python/samples/04-hosting/azure_functions/02_multi_agent/function_app.py index 2e805e43b2..ec3875b9c0 100644 --- a/python/samples/04-hosting/azure_functions/02_multi_agent/function_app.py +++ b/python/samples/04-hosting/azure_functions/02_multi_agent/function_app.py @@ -3,18 +3,18 @@ """Host multiple Azure OpenAI agents inside a single Azure Functions app. Components used in this sample: -- AzureOpenAIChatClient to create agents bound to a shared Azure OpenAI deployment. +- FoundryChatClient to create agents bound to a shared Azure OpenAI deployment. - AgentFunctionApp to register multiple agents and expose dedicated HTTP endpoints. - Custom tool functions to demonstrate tool invocation from different agents. -Prerequisites: set `AZURE_OPENAI_ENDPOINT` and `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`, plus either +Prerequisites: set `AZURE_OPENAI_ENDPOINT` and `FOUNDRY_MODEL`, plus either `AZURE_OPENAI_API_KEY` or authenticate with Azure CLI before starting the Functions host.""" import logging from typing import Any -from agent_framework import tool -from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient +from agent_framework import Agent, tool +from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -59,15 +59,15 @@ def calculate_tip(bill_amount: float, tip_percentage: float = 15.0) -> dict[str, # 1. Create multiple agents, each with its own instruction set and tools. -client = AzureOpenAIChatClient(credential=AzureCliCredential()) +client = FoundryChatClient(credential=AzureCliCredential()) -weather_agent = client.as_agent( +weather_agent = Agent(client=client, name="WeatherAgent", instructions="You are a helpful weather assistant. Provide current weather information.", tools=[get_weather], ) -math_agent = client.as_agent( +math_agent = Agent(client=client, name="MathAgent", instructions="You are a helpful math assistant. Help users with calculations like tip calculations.", tools=[calculate_tip], diff --git a/python/samples/04-hosting/azure_functions/03_reliable_streaming/function_app.py b/python/samples/04-hosting/azure_functions/03_reliable_streaming/function_app.py index 61eda7d6c0..1ee9e85d3e 100644 --- a/python/samples/04-hosting/azure_functions/03_reliable_streaming/function_app.py +++ b/python/samples/04-hosting/azure_functions/03_reliable_streaming/function_app.py @@ -5,12 +5,12 @@ This sample demonstrates how to implement reliable streaming for durable agents using Redis Streams. Components used in this sample: -- AzureOpenAIChatClient to create the travel planner agent with tools. +- FoundryChatClient to create the travel planner agent with tools. - AgentFunctionApp with a Redis-based callback for persistent streaming. - Custom HTTP endpoint to resume streaming from any point using cursor-based pagination. Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL - Redis running (docker run -d --name redis -p 6379:6379 redis:latest) - DTS and Azurite running (see parent README) """ @@ -26,7 +26,7 @@ AgentCallbackContext, AgentFunctionApp, AgentResponseCallbackProtocol, - AzureOpenAIChatClient, + FoundryChatClient, ) from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -155,7 +155,8 @@ async def on_agent_response(self, response, context: AgentCallbackContext) -> No # Create the travel planner agent def create_travel_agent(): """Create the TravelPlanner agent with tools.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name="TravelPlanner", instructions="""You are an expert travel planner who creates detailed, personalized travel itineraries. When asked to plan a trip, you should: diff --git a/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py b/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py index 6418cd7180..b7eaaf40b8 100644 --- a/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py +++ b/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """Chain two runs of a single agent inside a Durable Functions orchestration. Components used in this sample: -- AzureOpenAIChatClient to construct the writer agent hosted by Agent Framework. +- FoundryChatClient to construct the writer agent hosted by Agent Framework. - AgentFunctionApp to surface HTTP and orchestration triggers via the Azure Functions extension. - Durable Functions orchestration to run sequential agent invocations on the same conversation session. -Prerequisites: configure `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`, and either +Prerequisites: configure `AZURE_OPENAI_ENDPOINT`, `FOUNDRY_MODEL`, and either `AZURE_OPENAI_API_KEY` or authenticate with Azure CLI before starting the Functions host.""" import json @@ -16,7 +17,7 @@ from typing import Any import azure.functions as func -from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient +from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -38,7 +39,8 @@ def _create_writer_agent() -> Any: "when given an improved sentence you polish it further." ) - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name=WRITER_AGENT_NAME, instructions=instructions, ) diff --git a/python/samples/04-hosting/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py b/python/samples/04-hosting/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py index 3a64fa545a..5f0e367d6e 100644 --- a/python/samples/04-hosting/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py +++ b/python/samples/04-hosting/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py @@ -3,11 +3,11 @@ """Fan out concurrent runs across two agents inside a Durable Functions orchestration. Components used in this sample: -- AzureOpenAIChatClient to create domain-specific agents hosted by Agent Framework. +- FoundryChatClient to create domain-specific agents hosted by Agent Framework. - AgentFunctionApp to expose orchestration and HTTP triggers. - Durable Functions orchestration that executes agent calls in parallel and aggregates results. -Prerequisites: configure `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`, and either +Prerequisites: configure `AZURE_OPENAI_ENDPOINT`, `FOUNDRY_MODEL`, and either `AZURE_OPENAI_API_KEY` or authenticate with Azure CLI before starting the Functions host.""" import json @@ -16,8 +16,8 @@ from typing import Any, cast import azure.functions as func -from agent_framework import AgentResponse -from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient +from agent_framework import Agent, AgentResponse +from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -34,14 +34,14 @@ # 2. Instantiate both agents that the orchestration will run concurrently. def _create_agents() -> list[Any]: - client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = FoundryChatClient(credential=AzureCliCredential()) - physicist = client.as_agent( + physicist = Agent(client=client, name=PHYSICIST_AGENT_NAME, instructions="You are an expert in physics. You answer questions from a physics perspective.", ) - chemist = client.as_agent( + chemist = Agent(client=client, name=CHEMIST_AGENT_NAME, instructions="You are an expert in chemistry. You answer questions from a chemistry perspective.", ) diff --git a/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py b/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py index 64a071ca99..2bda1e0463 100644 --- a/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py +++ b/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """Route email requests through conditional orchestration with two agents. Components used in this sample: -- AzureOpenAIChatClient agents for spam detection and email drafting. +- FoundryChatClient agents for spam detection and email drafting. - AgentFunctionApp with Durable orchestration, activity, and HTTP triggers. - Pydantic models that validate payloads and agent JSON responses. -Prerequisites: set `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`, +Prerequisites: set `AZURE_OPENAI_ENDPOINT`, `FOUNDRY_MODEL`, and either `AZURE_OPENAI_API_KEY` or sign in with Azure CLI before running the Functions host.""" @@ -17,7 +18,7 @@ from typing import Any import azure.functions as func -from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient +from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -49,14 +50,14 @@ class EmailPayload(BaseModel): # 2. Instantiate both agents so they can be registered with AgentFunctionApp. def _create_agents() -> list[Any]: - client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = FoundryChatClient(credential=AzureCliCredential()) - spam_agent = client.as_agent( + spam_agent = Agent(client=client, name=SPAM_AGENT_NAME, instructions="You are a spam detection assistant that identifies spam emails.", ) - email_agent = client.as_agent( + email_agent = Agent(client=client, name=EMAIL_AGENT_NAME, instructions="You are an email assistant that helps users draft responses to emails with professionalism.", ) diff --git a/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py b/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py index ecdc5ca1c5..7f16213d91 100644 --- a/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py +++ b/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """Iterate on generated content with a human-in-the-loop Durable orchestration. Components used in this sample: -- AzureOpenAIChatClient for a single writer agent that emits structured JSON. +- FoundryChatClient for a single writer agent that emits structured JSON. - AgentFunctionApp with Durable orchestration, HTTP triggers, and activity triggers. - External events that pause the workflow until a human decision arrives or times out. -Prerequisites: configure `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`, and +Prerequisites: configure `AZURE_OPENAI_ENDPOINT`, `FOUNDRY_MODEL`, and either `AZURE_OPENAI_API_KEY` or sign in with Azure CLI before running `func start`.""" import json @@ -17,7 +18,7 @@ from typing import Any import azure.functions as func -from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient +from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -57,7 +58,8 @@ def _create_writer_agent() -> Any: "Return your response as JSON with 'title' and 'content' fields." ) - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name=WRITER_AGENT_NAME, instructions=instructions, ) diff --git a/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py b/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py index a4580fa23c..91c18e451a 100644 --- a/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py +++ b/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """ Example showing how to configure AI agents with different trigger configurations. @@ -19,12 +20,12 @@ Required environment variables: - AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint -- AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: Your Azure OpenAI deployment name +- FOUNDRY_MODEL: Your Azure OpenAI deployment name Authentication uses AzureCliCredential (Azure Identity). """ -from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient +from agent_framework.azure import AgentFunctionApp, FoundryChatClient from dotenv import load_dotenv # Load environment variables from .env file @@ -32,23 +33,23 @@ # Create Azure OpenAI Chat Client # This uses AzureCliCredential for authentication (requires 'az login') -client = AzureOpenAIChatClient() +client = FoundryChatClient() # Define three AI agents with different roles # Agent 1: Joker - HTTP trigger only (default) -agent1 = client.as_agent( +agent1 = Agent(client=client, name="Joker", instructions="You are good at telling jokes.", ) # Agent 2: StockAdvisor - MCP tool trigger only -agent2 = client.as_agent( +agent2 = Agent(client=client, name="StockAdvisor", instructions="Check stock prices.", ) # Agent 3: PlantAdvisor - Both HTTP and MCP tool triggers -agent3 = client.as_agent( +agent3 = Agent(client=client, name="PlantAdvisor", instructions="Recommend plants.", description="Get plant recommendations.", diff --git a/python/samples/04-hosting/azure_functions/09_workflow_shared_state/function_app.py b/python/samples/04-hosting/azure_functions/09_workflow_shared_state/function_app.py index 7b2317c58e..cc92114fd3 100644 --- a/python/samples/04-hosting/azure_functions/09_workflow_shared_state/function_app.py +++ b/python/samples/04-hosting/azure_functions/09_workflow_shared_state/function_app.py @@ -13,7 +13,7 @@ - Compose agent backed executors with function style executors and yield the final output when the workflow completes. Prerequisites: -- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables. +- Azure OpenAI configured for FoundryChatClient with required environment variables. - Authentication via azure-identity. Use DefaultAzureCredential and run az login before executing the sample. - Familiarity with WorkflowBuilder, executors, conditional edges, and streaming runs. """ @@ -25,6 +25,7 @@ from uuid import uuid4 from agent_framework import ( + Agent, AgentExecutorRequest, AgentExecutorResponse, Message, @@ -33,7 +34,7 @@ WorkflowContext, executor, ) -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from agent_framework_azurefunctions import AgentFunctionApp from azure.identity import AzureCliCredential from pydantic import BaseModel, ValidationError @@ -43,7 +44,7 @@ # Environment variable names AZURE_OPENAI_ENDPOINT_ENV = "AZURE_OPENAI_ENDPOINT" -AZURE_OPENAI_DEPLOYMENT_ENV = "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" +AZURE_OPENAI_DEPLOYMENT_ENV = "FOUNDRY_MODEL" AZURE_OPENAI_API_KEY_ENV = "AZURE_OPENAI_API_KEY" EMAIL_STATE_PREFIX = "email:" @@ -198,9 +199,10 @@ def _build_client_kwargs() -> dict[str, Any]: def _create_workflow() -> Workflow: """Create the email classification workflow with conditional routing.""" client_kwargs = _build_client_kwargs() - chat_client = AzureOpenAIChatClient(**client_kwargs) + chat_client = FoundryChatClient(**client_kwargs) - spam_detection_agent = chat_client.as_agent( + spam_detection_agent = Agent( + client=chat_client, instructions=( "You are a spam detection assistant that identifies spam emails. " "Always return JSON with fields is_spam (bool) and reason (string)." @@ -209,7 +211,8 @@ def _create_workflow() -> Workflow: name="spam_detection_agent", ) - email_assistant_agent = chat_client.as_agent( + email_assistant_agent = Agent( + client=chat_client, instructions=( "You are an email assistant that helps users draft responses to emails with professionalism. " "Return JSON with a single field 'response' containing the drafted reply." diff --git a/python/samples/04-hosting/azure_functions/10_workflow_no_shared_state/function_app.py b/python/samples/04-hosting/azure_functions/10_workflow_no_shared_state/function_app.py index 831d860806..d8bc677c9a 100644 --- a/python/samples/04-hosting/azure_functions/10_workflow_no_shared_state/function_app.py +++ b/python/samples/04-hosting/azure_functions/10_workflow_no_shared_state/function_app.py @@ -22,6 +22,7 @@ from typing import Any from agent_framework import ( + Agent, AgentExecutorResponse, Case, Default, @@ -31,7 +32,7 @@ WorkflowContext, handler, ) -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from agent_framework_azurefunctions import AgentFunctionApp from azure.identity import AzureCliCredential from pydantic import BaseModel, ValidationError @@ -40,7 +41,7 @@ logger = logging.getLogger(__name__) AZURE_OPENAI_ENDPOINT_ENV = "AZURE_OPENAI_ENDPOINT" -AZURE_OPENAI_DEPLOYMENT_ENV = "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" +AZURE_OPENAI_DEPLOYMENT_ENV = "FOUNDRY_MODEL" AZURE_OPENAI_API_KEY_ENV = "AZURE_OPENAI_API_KEY" SPAM_AGENT_NAME = "SpamDetectionAgent" EMAIL_AGENT_NAME = "EmailAssistantAgent" @@ -165,15 +166,17 @@ def is_spam_detected(message: Any) -> bool: def _create_workflow() -> Workflow: """Create the workflow definition.""" client_kwargs = _build_client_kwargs() - chat_client = AzureOpenAIChatClient(**client_kwargs) + chat_client = FoundryChatClient(**client_kwargs) - spam_agent = chat_client.as_agent( + spam_agent = Agent( + client=chat_client, name=SPAM_AGENT_NAME, instructions=SPAM_DETECTION_INSTRUCTIONS, default_options={"response_format": SpamDetectionResult}, ) - email_agent = chat_client.as_agent( + email_agent = Agent( + client=chat_client, name=EMAIL_AGENT_NAME, instructions=EMAIL_ASSISTANT_INSTRUCTIONS, default_options={"response_format": EmailResponse}, diff --git a/python/samples/04-hosting/azure_functions/11_workflow_parallel/function_app.py b/python/samples/04-hosting/azure_functions/11_workflow_parallel/function_app.py index d9d3ff6324..cf600a851b 100644 --- a/python/samples/04-hosting/azure_functions/11_workflow_parallel/function_app.py +++ b/python/samples/04-hosting/azure_functions/11_workflow_parallel/function_app.py @@ -28,6 +28,7 @@ from typing import Any from agent_framework import ( + Agent, AgentExecutorResponse, Executor, Workflow, @@ -36,7 +37,7 @@ executor, handler, ) -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from agent_framework_azurefunctions import AgentFunctionApp from azure.identity import AzureCliCredential from pydantic import BaseModel @@ -45,7 +46,7 @@ logger = logging.getLogger(__name__) AZURE_OPENAI_ENDPOINT_ENV = "AZURE_OPENAI_ENDPOINT" -AZURE_OPENAI_DEPLOYMENT_ENV = "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" +AZURE_OPENAI_DEPLOYMENT_ENV = "FOUNDRY_MODEL" AZURE_OPENAI_API_KEY_ENV = "AZURE_OPENAI_API_KEY" # Agent names @@ -382,10 +383,11 @@ def _create_workflow() -> Workflow: └──> final_report """ client_kwargs = _build_client_kwargs() - chat_client = AzureOpenAIChatClient(**client_kwargs) + chat_client = FoundryChatClient(**client_kwargs) # Create agents for parallel analysis - sentiment_agent = chat_client.as_agent( + sentiment_agent = Agent( + client=chat_client, name=SENTIMENT_AGENT_NAME, instructions=( "You are a sentiment analysis expert. Analyze the sentiment of the given text. " @@ -395,7 +397,8 @@ def _create_workflow() -> Workflow: default_options={"response_format": SentimentResult}, ) - keyword_agent = chat_client.as_agent( + keyword_agent = Agent( + client=chat_client, name=KEYWORD_AGENT_NAME, instructions=( "You are a keyword extraction expert. Extract important keywords and categories " @@ -406,7 +409,8 @@ def _create_workflow() -> Workflow: ) # Create summary agent for Pattern 3 (mixed parallel) - summary_agent = chat_client.as_agent( + summary_agent = Agent( + client=chat_client, name=SUMMARY_AGENT_NAME, instructions=( "You are a summarization expert. Given analysis results (sentiment and keywords), " diff --git a/python/samples/04-hosting/azure_functions/12_workflow_hitl/function_app.py b/python/samples/04-hosting/azure_functions/12_workflow_hitl/function_app.py index 11d11b4a92..c951d58aef 100644 --- a/python/samples/04-hosting/azure_functions/12_workflow_hitl/function_app.py +++ b/python/samples/04-hosting/azure_functions/12_workflow_hitl/function_app.py @@ -30,6 +30,7 @@ from typing import Any from agent_framework import ( + Agent, AgentExecutorRequest, AgentExecutorResponse, Executor, @@ -40,7 +41,7 @@ handler, response_handler, ) -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from agent_framework_azurefunctions import AgentFunctionApp from azure.identity import AzureCliCredential from pydantic import BaseModel, ValidationError @@ -50,7 +51,7 @@ # Environment variable names AZURE_OPENAI_ENDPOINT_ENV = "AZURE_OPENAI_ENDPOINT" -AZURE_OPENAI_DEPLOYMENT_ENV = "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" +AZURE_OPENAI_DEPLOYMENT_ENV = "FOUNDRY_MODEL" AZURE_OPENAI_API_KEY_ENV = "AZURE_OPENAI_API_KEY" # Agent names @@ -379,10 +380,11 @@ async def route_input( def _create_workflow() -> Workflow: """Create the content moderation workflow with HITL.""" client_kwargs = _build_client_kwargs() - chat_client = AzureOpenAIChatClient(**client_kwargs) + chat_client = FoundryChatClient(**client_kwargs) # Create the content analysis agent - content_analyzer_agent = chat_client.as_agent( + content_analyzer_agent = Agent( + client=chat_client, name=CONTENT_ANALYZER_AGENT_NAME, instructions=CONTENT_ANALYZER_INSTRUCTIONS, default_options={"response_format": ContentAnalysisResult}, diff --git a/python/samples/04-hosting/durabletask/01_single_agent/client.py b/python/samples/04-hosting/durabletask/01_single_agent/client.py index 917a53a74a..ca8e33d504 100644 --- a/python/samples/04-hosting/durabletask/01_single_agent/client.py +++ b/python/samples/04-hosting/durabletask/01_single_agent/client.py @@ -7,7 +7,7 @@ Prerequisites: - The worker must be running with the agent registered -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running """ @@ -17,7 +17,7 @@ import os from agent_framework.azure import DurableAIAgentClient -from azure.identity import DefaultAzureCredential +from azure.identity import AzureCliCredential from dotenv import load_dotenv from durabletask.azuremanaged.client import DurableTaskSchedulerClient @@ -48,7 +48,7 @@ def get_client( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() dts_client = DurableTaskSchedulerClient( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/01_single_agent/sample.py b/python/samples/04-hosting/durabletask/01_single_agent/sample.py index 22d22927fd..8adb31e40d 100644 --- a/python/samples/04-hosting/durabletask/01_single_agent/sample.py +++ b/python/samples/04-hosting/durabletask/01_single_agent/sample.py @@ -1,20 +1,16 @@ # Copyright (c) Microsoft. All rights reserved. """Single Agent Sample - Durable Task Integration (Combined Worker + Client) - This sample demonstrates running both the worker and client in a single process. The worker is started first to register the agent, then client operations are performed against the running worker. - Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running (e.g., using Docker) - To run this sample: python sample.py """ - import logging # Import helper functions from worker and client modules @@ -30,27 +26,21 @@ def main(): """Main entry point - runs both worker and client in single process.""" logger.debug("Starting Durable Task Agent Sample (Combined Worker + Client)...") - silent_handler = logging.NullHandler() - # Create and start the worker using helper function and context manager with get_worker(log_handler=silent_handler) as dts_worker: # Register agents using helper function setup_worker(dts_worker) - # Start the worker dts_worker.start() logger.debug("Worker started and listening for requests...") - # Create the client using helper function agent_client = get_client(log_handler=silent_handler) - try: # Run client interactions using helper function run_client(agent_client) except Exception as e: logger.exception(f"Error during agent interaction: {e}") - logger.debug("Sample completed. Worker shutting down...") diff --git a/python/samples/04-hosting/durabletask/01_single_agent/worker.py b/python/samples/04-hosting/durabletask/01_single_agent/worker.py index 535b5d6fb2..a660c0ac6f 100644 --- a/python/samples/04-hosting/durabletask/01_single_agent/worker.py +++ b/python/samples/04-hosting/durabletask/01_single_agent/worker.py @@ -6,7 +6,7 @@ The worker should run as a background service, processing incoming agent requests. Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Start a Durable Task Scheduler (e.g., using Docker) """ @@ -16,8 +16,8 @@ import os from agent_framework import Agent -from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentWorker -from azure.identity import AzureCliCredential, DefaultAzureCredential +from agent_framework.azure import DurableAIAgentWorker, FoundryChatClient +from azure.identity import AzureCliCredential from dotenv import load_dotenv from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker @@ -35,7 +35,8 @@ def create_joker_agent() -> Agent: Returns: Agent: The configured Joker agent """ - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name="Joker", instructions="You are good at telling jokes.", ) @@ -60,7 +61,7 @@ def get_worker( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerWorker( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/02_multi_agent/client.py b/python/samples/04-hosting/durabletask/02_multi_agent/client.py index df8c9c8e1a..6bd2f20168 100644 --- a/python/samples/04-hosting/durabletask/02_multi_agent/client.py +++ b/python/samples/04-hosting/durabletask/02_multi_agent/client.py @@ -8,7 +8,7 @@ Prerequisites: - The worker must be running with both agents registered -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running """ @@ -18,7 +18,7 @@ import os from agent_framework.azure import DurableAIAgentClient -from azure.identity import DefaultAzureCredential +from azure.identity import AzureCliCredential from dotenv import load_dotenv from durabletask.azuremanaged.client import DurableTaskSchedulerClient @@ -49,7 +49,7 @@ def get_client( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() dts_client = DurableTaskSchedulerClient( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/02_multi_agent/sample.py b/python/samples/04-hosting/durabletask/02_multi_agent/sample.py index 6357c145a2..8f2decaba8 100644 --- a/python/samples/04-hosting/durabletask/02_multi_agent/sample.py +++ b/python/samples/04-hosting/durabletask/02_multi_agent/sample.py @@ -1,20 +1,16 @@ # Copyright (c) Microsoft. All rights reserved. """Multi-Agent Sample - Durable Task Integration (Combined Worker + Client) - This sample demonstrates running both the worker and client in a single process for multiple agents with different tools. The worker registers two agents (WeatherAgent and MathAgent), each with their own specialized capabilities. - Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running (e.g., using Docker) - To run this sample: python sample.py """ - import logging # Import helper functions from worker and client modules @@ -30,26 +26,21 @@ def main(): """Main entry point - runs both worker and client in single process.""" logger.debug("Starting Durable Task Multi-Agent Sample (Combined Worker + Client)...") - silent_handler = logging.NullHandler() # Create and start the worker using helper function and context manager with get_worker(log_handler=silent_handler) as dts_worker: # Register agents using helper function setup_worker(dts_worker) - # Start the worker dts_worker.start() logger.debug("Worker started and listening for requests...") - # Create the client using helper function agent_client = get_client(log_handler=silent_handler) - try: # Run client interactions using helper function run_client(agent_client) except Exception as e: logger.exception(f"Error during agent interaction: {e}") - logger.debug("Sample completed. Worker shutting down...") diff --git a/python/samples/04-hosting/durabletask/02_multi_agent/worker.py b/python/samples/04-hosting/durabletask/02_multi_agent/worker.py index 1b7dc91c1a..50640e0621 100644 --- a/python/samples/04-hosting/durabletask/02_multi_agent/worker.py +++ b/python/samples/04-hosting/durabletask/02_multi_agent/worker.py @@ -7,7 +7,7 @@ with different capabilities in a single worker process. Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Start a Durable Task Scheduler (e.g., using Docker) """ @@ -17,9 +17,9 @@ import os from typing import Any -from agent_framework import tool -from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentWorker -from azure.identity import AzureCliCredential, DefaultAzureCredential +from agent_framework import Agent, tool +from agent_framework.azure import DurableAIAgentWorker, FoundryChatClient +from azure.identity import AzureCliCredential from dotenv import load_dotenv from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker @@ -71,7 +71,8 @@ def create_weather_agent(): Returns: Agent: The configured Weather agent with weather tool """ - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name=WEATHER_AGENT_NAME, instructions="You are a helpful weather assistant. Provide current weather information.", tools=[get_weather], @@ -84,7 +85,8 @@ def create_math_agent(): Returns: Agent: The configured Math agent with calculation tools """ - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name=MATH_AGENT_NAME, instructions="You are a helpful math assistant. Help users with calculations like tip calculations.", tools=[calculate_tip], @@ -110,7 +112,7 @@ def get_worker( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerWorker( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/03_single_agent_streaming/client.py b/python/samples/04-hosting/durabletask/03_single_agent_streaming/client.py index 883ebeb483..8451381222 100644 --- a/python/samples/04-hosting/durabletask/03_single_agent_streaming/client.py +++ b/python/samples/04-hosting/durabletask/03_single_agent_streaming/client.py @@ -9,7 +9,7 @@ Prerequisites: - The worker must be running with the TravelPlanner agent registered -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL - Redis must be running - Durable Task Scheduler must be running """ @@ -21,7 +21,7 @@ import redis.asyncio as aioredis from agent_framework.azure import DurableAIAgentClient -from azure.identity import DefaultAzureCredential +from azure.identity import AzureCliCredential from dotenv import load_dotenv from durabletask.azuremanaged.client import DurableTaskSchedulerClient from redis_stream_response_handler import RedisStreamResponseHandler @@ -76,7 +76,7 @@ def get_client( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() dts_client = DurableTaskSchedulerClient( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/03_single_agent_streaming/sample.py b/python/samples/04-hosting/durabletask/03_single_agent_streaming/sample.py index 800f3597c5..6ea11ad15e 100644 --- a/python/samples/04-hosting/durabletask/03_single_agent_streaming/sample.py +++ b/python/samples/04-hosting/durabletask/03_single_agent_streaming/sample.py @@ -1,23 +1,18 @@ # Copyright (c) Microsoft. All rights reserved. """Single Agent Streaming Sample - Durable Task Integration (Combined Worker + Client) - This sample demonstrates running both the worker and client in a single process with reliable Redis-based streaming for agent responses. - The worker is started first to register the TravelPlanner agent with Redis streaming callback, then client operations are performed against the running worker. - Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running (e.g., using Docker) - Redis must be running (e.g., docker run -d --name redis -p 6379:6379 redis:latest) - To run this sample: python sample.py """ - import logging # Import helper functions from worker and client modules @@ -33,27 +28,21 @@ def main(): """Main entry point - runs both worker and client in single process.""" logger.debug("Starting Durable Task Agent Sample with Redis Streaming...") - silent_handler = logging.NullHandler() - # Create and start the worker using helper function and context manager with get_worker(log_handler=silent_handler) as dts_worker: # Register agents and callbacks using helper function setup_worker(dts_worker) - # Start the worker dts_worker.start() logger.debug("Worker started and listening for requests...") - # Create the client using helper function agent_client = get_client(log_handler=silent_handler) - try: # Run client interactions using helper function run_client(agent_client) except Exception as e: logger.exception(f"Error during agent interaction: {e}") - logger.debug("Sample completed. Worker shutting down...") diff --git a/python/samples/04-hosting/durabletask/03_single_agent_streaming/worker.py b/python/samples/04-hosting/durabletask/03_single_agent_streaming/worker.py index 983156147a..b98ed45bdc 100644 --- a/python/samples/04-hosting/durabletask/03_single_agent_streaming/worker.py +++ b/python/samples/04-hosting/durabletask/03_single_agent_streaming/worker.py @@ -6,7 +6,7 @@ and uses RedisStreamCallback to persist streaming responses to Redis for reliable delivery. Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Start a Durable Task Scheduler (e.g., using Docker) - Start Redis (e.g., docker run -d --name redis -p 6379:6379 redis:latest) @@ -22,10 +22,10 @@ from agent_framework.azure import ( AgentCallbackContext, AgentResponseCallbackProtocol, - AzureOpenAIChatClient, DurableAIAgentWorker, + FoundryChatClient, ) -from azure.identity import AzureCliCredential, DefaultAzureCredential +from azure.identity import AzureCliCredential from dotenv import load_dotenv from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker from redis_stream_response_handler import RedisStreamResponseHandler @@ -153,7 +153,8 @@ def create_travel_agent() -> "Agent": Returns: Agent: The configured TravelPlanner agent with travel planning tools. """ - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name="TravelPlanner", instructions="""You are an expert travel planner who creates detailed, personalized travel itineraries. When asked to plan a trip, you should: @@ -191,7 +192,7 @@ def get_worker( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerWorker( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/client.py b/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/client.py index 573683fca1..24e2dbf711 100644 --- a/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/client.py +++ b/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/client.py @@ -8,7 +8,7 @@ Prerequisites: - The worker must be running with the writer agent and orchestration registered -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running """ @@ -18,7 +18,7 @@ import logging import os -from azure.identity import DefaultAzureCredential +from azure.identity import AzureCliCredential from durabletask.azuremanaged.client import DurableTaskSchedulerClient # Configure logging @@ -45,7 +45,7 @@ def get_client( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerClient( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/sample.py b/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/sample.py index 44b20c2265..c1d3749964 100644 --- a/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/sample.py +++ b/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/sample.py @@ -1,26 +1,21 @@ # Copyright (c) Microsoft. All rights reserved. """Single Agent Orchestration Chaining Sample - Durable Task Integration - This sample demonstrates chaining two invocations of the same agent inside a Durable Task orchestration while preserving the conversation state between runs. The orchestration runs the writer agent sequentially on a shared thread to refine text iteratively. - Components used: -- AzureOpenAIChatClient to construct the writer agent +- FoundryChatClient to construct the writer agent - DurableTaskSchedulerWorker and DurableAIAgentWorker for agent hosting - DurableTaskSchedulerClient and orchestration for sequential agent invocations - Thread management to maintain conversation context across invocations - Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running (e.g., using Docker emulator) - To run this sample: python sample.py """ - import logging # Import helper functions from worker and client modules @@ -36,22 +31,17 @@ def main(): """Main entry point - runs both worker and client in single process.""" logger.debug("Starting Single Agent Orchestration Chaining Sample...") - silent_handler = logging.NullHandler() # Create and start the worker using helper function and context manager with get_worker(log_handler=silent_handler) as dts_worker: # Register agents and orchestrations using helper function setup_worker(dts_worker) - # Start the worker dts_worker.start() logger.debug("Worker started and listening for requests...") - # Create the client using helper function client = get_client(log_handler=silent_handler) - logger.debug("CLIENT: Starting orchestration...") - # Run the client in the same process try: run_client(client) @@ -61,7 +51,6 @@ def main(): logger.exception(f"Error during orchestration: {e}") finally: logger.debug("Worker stopping...") - logger.debug("") logger.debug("Sample completed") diff --git a/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/worker.py b/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/worker.py index 86a3b259f7..54935d2bf3 100644 --- a/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/worker.py +++ b/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/worker.py @@ -7,7 +7,7 @@ preserving conversation context between invocations. Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Start a Durable Task Scheduler (e.g., using Docker) """ @@ -18,8 +18,8 @@ from collections.abc import Generator from agent_framework import Agent, AgentResponse -from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker -from azure.identity import AzureCliCredential, DefaultAzureCredential +from agent_framework.azure import DurableAIAgentOrchestrationContext, DurableAIAgentWorker, FoundryChatClient +from azure.identity import AzureCliCredential from dotenv import load_dotenv from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker from durabletask.task import OrchestrationContext, Task @@ -49,7 +49,8 @@ def create_writer_agent() -> "Agent": "when given an improved sentence you polish it further." ) - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name=WRITER_AGENT_NAME, instructions=instructions, ) @@ -139,7 +140,7 @@ def get_worker( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerWorker( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/client.py b/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/client.py index 473a176def..7e83383fa1 100644 --- a/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/client.py +++ b/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/client.py @@ -8,7 +8,7 @@ Prerequisites: - The worker must be running with both agents and orchestration registered -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running """ @@ -18,7 +18,7 @@ import logging import os -from azure.identity import DefaultAzureCredential +from azure.identity import AzureCliCredential from durabletask.azuremanaged.client import DurableTaskSchedulerClient # Configure logging @@ -45,7 +45,7 @@ def get_client( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerClient( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/sample.py b/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/sample.py index 808a45e6ea..4653a98a7b 100644 --- a/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/sample.py +++ b/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/sample.py @@ -1,23 +1,18 @@ # Copyright (c) Microsoft. All rights reserved. """Multi-Agent Orchestration Sample - Durable Task Integration (Combined Worker + Client) - This sample demonstrates running both the worker and client in a single process for concurrent multi-agent orchestration. The worker registers two domain-specific agents (physicist and chemist) and an orchestration function that runs them in parallel. - The orchestration uses OrchestrationAgentExecutor to execute agents concurrently and aggregate their responses. - Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running (e.g., using Docker) - To run this sample: python sample.py """ - import logging # Import helper functions from worker and client modules @@ -33,30 +28,24 @@ def main(): """Main entry point - runs both worker and client in single process.""" logger.debug("Starting Durable Task Multi-Agent Orchestration Sample (Combined Worker + Client)...") - silent_handler = logging.NullHandler() # Create and start the worker using helper function and context manager with get_worker(log_handler=silent_handler) as dts_worker: # Register agents and orchestrations using helper function setup_worker(dts_worker) - # Start the worker dts_worker.start() logger.debug("Worker started and listening for requests...") - # Create the client using helper function client = get_client(log_handler=silent_handler) - # Define the prompt prompt = "What is temperature?" logger.debug("CLIENT: Starting orchestration...") - try: # Run the client to start the orchestration run_client(client, prompt) except Exception as e: logger.exception(f"Error during sample execution: {e}") - logger.debug("Sample completed. Worker shutting down...") diff --git a/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/worker.py b/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/worker.py index 18ede13ead..6f40311a75 100644 --- a/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/worker.py +++ b/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/worker.py @@ -7,7 +7,7 @@ to execute agents in parallel and aggregate their responses. Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Start a Durable Task Scheduler (e.g., using Docker) """ @@ -19,8 +19,8 @@ from typing import Any from agent_framework import Agent, AgentResponse -from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker -from azure.identity import AzureCliCredential, DefaultAzureCredential +from agent_framework.azure import DurableAIAgentOrchestrationContext, DurableAIAgentWorker, FoundryChatClient +from azure.identity import AzureCliCredential from dotenv import load_dotenv from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker from durabletask.task import OrchestrationContext, Task, when_all @@ -43,7 +43,8 @@ def create_physicist_agent() -> "Agent": Returns: Agent: The configured Physicist agent """ - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name=PHYSICIST_AGENT_NAME, instructions="You are an expert in physics. You answer questions from a physics perspective.", ) @@ -55,7 +56,8 @@ def create_chemist_agent() -> "Agent": Returns: Agent: The configured Chemist agent """ - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name=CHEMIST_AGENT_NAME, instructions="You are an expert in chemistry. You answer questions from a chemistry perspective.", ) @@ -138,7 +140,7 @@ def get_worker( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerWorker( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/client.py b/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/client.py index 0202763e74..7077671152 100644 --- a/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/client.py +++ b/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/client.py @@ -7,7 +7,7 @@ Prerequisites: - The worker must be running with both agents, orchestration, and activities registered -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running """ @@ -16,7 +16,7 @@ import logging import os -from azure.identity import DefaultAzureCredential +from azure.identity import AzureCliCredential from durabletask.azuremanaged.client import DurableTaskSchedulerClient # Configure logging @@ -43,7 +43,7 @@ def get_client( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerClient( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/sample.py b/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/sample.py index 930c3b1f9d..e426a32534 100644 --- a/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/sample.py +++ b/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/sample.py @@ -10,7 +10,7 @@ activity functions to handle spam or send legitimate email responses. Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running (e.g., using Docker) diff --git a/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/worker.py b/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/worker.py index 28a3d54749..c95ee4f997 100644 --- a/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/worker.py +++ b/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/worker.py @@ -7,7 +7,7 @@ handle side effects (spam handling and email sending). Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Start a Durable Task Scheduler (e.g., using Docker) """ @@ -19,8 +19,8 @@ from typing import Any, cast from agent_framework import Agent, AgentResponse -from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker -from azure.identity import AzureCliCredential, DefaultAzureCredential +from agent_framework.azure import DurableAIAgentOrchestrationContext, DurableAIAgentWorker, FoundryChatClient +from azure.identity import AzureCliCredential from dotenv import load_dotenv from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker from durabletask.task import ActivityContext, OrchestrationContext, Task @@ -64,7 +64,8 @@ def create_spam_agent() -> "Agent": Returns: Agent: The configured Spam Detection agent """ - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name=SPAM_AGENT_NAME, instructions="You are a spam detection assistant that identifies spam emails.", ) @@ -76,7 +77,8 @@ def create_email_agent() -> "Agent": Returns: Agent: The configured Email Assistant agent """ - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name=EMAIL_AGENT_NAME, instructions="You are an email assistant that helps users draft responses to emails with professionalism.", ) @@ -220,7 +222,7 @@ def get_worker( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerWorker( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/client.py b/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/client.py index cf29e31f65..c49c00d826 100644 --- a/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/client.py +++ b/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/client.py @@ -7,7 +7,7 @@ Prerequisites: - The worker must be running with the agent, orchestration, and activities registered -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running """ @@ -18,7 +18,7 @@ import os import time -from azure.identity import DefaultAzureCredential +from azure.identity import AzureCliCredential from durabletask.azuremanaged.client import DurableTaskSchedulerClient from durabletask.client import OrchestrationState @@ -49,7 +49,7 @@ def get_client( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerClient( host_address=endpoint_url, diff --git a/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/sample.py b/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/sample.py index d90d6c1aea..ae00a7cb67 100644 --- a/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/sample.py +++ b/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/sample.py @@ -1,23 +1,19 @@ # Copyright (c) Microsoft. All rights reserved. """Human-in-the-Loop Orchestration Sample - Durable Task Integration - This sample demonstrates the HITL pattern with a WriterAgent that generates content and waits for human approval. The orchestration handles: - External event waiting (approval/rejection) - Timeout handling - Iterative refinement based on feedback - Activity functions for notifications and publishing - Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Durable Task Scheduler must be running (e.g., using Docker) - To run this sample: python sample.py """ - import logging # Import helper functions from worker and client modules @@ -32,28 +28,21 @@ def main(): """Main entry point - runs both worker and client in single process.""" logger.debug("Starting Durable Task HITL Content Generation Sample (Combined Worker + Client)...") - silent_handler = logging.NullHandler() # Create and start the worker using helper function and context manager with get_worker(log_handler=silent_handler) as dts_worker: # Register agent, orchestration, and activities using helper function setup_worker(dts_worker) - # Start the worker dts_worker.start() logger.debug("Worker started and listening for requests...") - # Create the client using helper function client = get_client(log_handler=silent_handler) - try: logger.debug("CLIENT: Starting orchestration tests...") - run_interactive_client(client) - except Exception as e: logger.exception(f"Error during sample execution: {e}") - logger.debug("Sample completed. Worker shutting down...") diff --git a/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/worker.py b/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/worker.py index fed74874f0..4d5fb85e21 100644 --- a/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/worker.py +++ b/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/worker.py @@ -7,7 +7,7 @@ (human approval/rejection) with timeout handling, and iterates based on feedback. Prerequisites: -- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME +- Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (plus AZURE_OPENAI_API_KEY or Azure CLI authentication) - Start a Durable Task Scheduler (e.g., using Docker) """ @@ -20,8 +20,8 @@ from typing import Any, cast from agent_framework import Agent, AgentResponse -from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker -from azure.identity import AzureCliCredential, DefaultAzureCredential +from agent_framework.azure import DurableAIAgentOrchestrationContext, DurableAIAgentWorker, FoundryChatClient +from azure.identity import AzureCliCredential from dotenv import load_dotenv from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker from durabletask.task import ActivityContext, OrchestrationContext, Task, when_any # type: ignore @@ -74,7 +74,8 @@ def create_writer_agent() -> "Agent": "Limit response to 300 words or less." ) - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + _client = FoundryChatClient(credential=AzureCliCredential()) + return Agent(client=_client, name=WRITER_AGENT_NAME, instructions=instructions, ) @@ -298,7 +299,7 @@ def get_worker( logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() + credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential() return DurableTaskSchedulerWorker( host_address=endpoint_url, diff --git a/python/samples/05-end-to-end/chatkit-integration/app.py b/python/samples/05-end-to-end/chatkit-integration/app.py index d47ecc6160..e165456ee7 100644 --- a/python/samples/05-end-to-end/chatkit-integration/app.py +++ b/python/samples/05-end-to-end/chatkit-integration/app.py @@ -29,7 +29,7 @@ # Agent Framework imports from agent_framework import Agent, AgentResponseUpdate, FunctionResultContent, Message, Role, tool -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient # Agent Framework ChatKit integration from agent_framework_chatkit import ThreadItemConverter, stream_agent_response @@ -222,7 +222,7 @@ def __init__(self, data_store: SQLiteStore, attachment_store: FileBasedAttachmen # For authentication, run `az login` command in terminal try: self.weather_agent = Agent( - client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=FoundryChatClient(credential=AzureCliCredential()), instructions=( "You are a helpful weather assistant with image analysis capabilities. " "You can provide weather information for any location, tell the current time, " diff --git a/python/samples/05-end-to-end/evaluation/red_teaming/red_team_agent_sample.py b/python/samples/05-end-to-end/evaluation/red_teaming/red_team_agent_sample.py index a63912c615..705f8ce581 100644 --- a/python/samples/05-end-to-end/evaluation/red_teaming/red_team_agent_sample.py +++ b/python/samples/05-end-to-end/evaluation/red_teaming/red_team_agent_sample.py @@ -15,8 +15,8 @@ import os from typing import Any -from agent_framework import Message -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework import Agent, Message +from agent_framework.azure import FoundryChatClient from azure.ai.evaluation.red_team import AttackStrategy, RedTeam, RiskCategory from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -52,7 +52,8 @@ async def main() -> None: # Create the agent # Constructor automatically reads from environment variables: # AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT_NAME, AZURE_OPENAI_API_KEY - agent = AzureOpenAIChatClient(credential=credential).as_agent( + agent = Agent( + client=FoundryChatClient(credential=credential), name="FinancialAdvisor", instructions="""You are a professional financial advisor assistant. @@ -98,7 +99,7 @@ async def agent_callback( # Create RedTeam instance red_team = RedTeam( - azure_ai_project=os.environ["AZURE_AI_PROJECT_ENDPOINT"], + azure_ai_project=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=credential, risk_categories=[ RiskCategory.Violence, diff --git a/python/samples/05-end-to-end/evaluation/self_reflection/self_reflection.py b/python/samples/05-end-to-end/evaluation/self_reflection/self_reflection.py index d554531e35..1da6861112 100644 --- a/python/samples/05-end-to-end/evaluation/self_reflection/self_reflection.py +++ b/python/samples/05-end-to-end/evaluation/self_reflection/self_reflection.py @@ -20,7 +20,7 @@ import openai import pandas as pd from agent_framework import Agent, Message -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.ai.projects import AIProjectClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -87,7 +87,7 @@ def create_openai_client(): - endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] credential = AzureCliCredential() project_client = AIProjectClient(endpoint=endpoint, credential=credential) return project_client.get_openai_client() @@ -97,7 +97,7 @@ def create_async_project_client(): from azure.ai.projects.aio import AIProjectClient as AsyncAIProjectClient from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential - return AsyncAIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=AsyncAzureCliCredential()) + return AsyncAIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=AsyncAzureCliCredential()) def create_eval(client: openai.OpenAI, judge_model: str) -> openai.types.EvalCreateResponse: @@ -321,9 +321,9 @@ async def run_self_reflection_batch( load_dotenv(override=True) # Create agent, it loads environment variables AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT automatically - responses_client = AzureOpenAIResponsesClient( + responses_client = FoundryChatClient( project_client=project_client, - deployment_name=agent_model, + model=agent_model, ) # Load input data @@ -368,7 +368,7 @@ async def run_self_reflection_batch( try: result = await execute_query_with_self_reflection( client=client, - agent=responses_client.as_agent(instructions=row["system_instruction"]), + agent=Agent(client=responses_client, instructions=row["system_instruction"]), eval_object=eval_object, full_user_query=row["full_prompt"], context=row["context_document"], diff --git a/python/samples/05-end-to-end/hosted_agents/agent_with_hosted_mcp/main.py b/python/samples/05-end-to-end/hosted_agents/agent_with_hosted_mcp/main.py index 53ee10e6bf..e7916dd0be 100644 --- a/python/samples/05-end-to-end/hosted_agents/agent_with_hosted_mcp/main.py +++ b/python/samples/05-end-to-end/hosted_agents/agent_with_hosted_mcp/main.py @@ -1,8 +1,9 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework import Agent +from agent_framework.azure import FoundryChatClient from azure.ai.agentserver.agentframework import from_agent_framework # pyright: ignore[reportUnknownVariableType] -from azure.identity import DefaultAzureCredential +from azure.identity import AzureCliCredential from dotenv import load_dotenv # Load environment variables from .env file @@ -16,14 +17,13 @@ def main(): "server_label": "Microsoft_Learn_MCP", "server_url": "https://learn.microsoft.com/api/mcp", } - # Create an Agent using the Azure OpenAI Chat Client with a MCP Tool that connects to Microsoft Learn MCP - agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent( + agent = Agent( + client=FoundryChatClient(credential=AzureCliCredential()), name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", tools=mcp_tool, ) - # Run the agent as a hosted agent from_agent_framework(agent).run() diff --git a/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py b/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py index 4c60902dc2..6ee26b6157 100644 --- a/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py +++ b/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +from agent_framework import Agent """ Seattle Hotel Agent - A simple agent with a tool to find hotels in Seattle. @@ -11,7 +12,7 @@ from datetime import datetime from typing import Annotated -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.ai.agentserver.agentframework import from_agent_framework from azure.identity.aio import AzureCliCredential, ManagedIdentityCredential from dotenv import load_dotenv @@ -127,12 +128,12 @@ def get_credential(): async def main(): """Main function to run the agent as a web server.""" async with get_credential() as credential: - client = AzureOpenAIResponsesClient( + client = FoundryChatClient( project_endpoint=PROJECT_ENDPOINT, - deployment_name=MODEL_DEPLOYMENT_NAME, + model=MODEL_DEPLOYMENT_NAME, credential=credential, ) - agent = client.as_agent( + agent = Agent(client=client, name="SeattleHotelAgent", instructions="""You are a helpful travel assistant specializing in finding hotels in Seattle, Washington. diff --git a/python/samples/05-end-to-end/hosted_agents/agent_with_text_search_rag/main.py b/python/samples/05-end-to-end/hosted_agents/agent_with_text_search_rag/main.py index 083da0d880..e6834b6b31 100644 --- a/python/samples/05-end-to-end/hosted_agents/agent_with_text_search_rag/main.py +++ b/python/samples/05-end-to-end/hosted_agents/agent_with_text_search_rag/main.py @@ -5,8 +5,8 @@ from dataclasses import dataclass from typing import Any -from agent_framework import AgentSession, BaseContextProvider, Message, SessionContext -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework import Agent, AgentSession, BaseContextProvider, Message, SessionContext +from agent_framework.azure import FoundryChatClient from azure.ai.agentserver.agentframework import from_agent_framework # pyright: ignore[reportUnknownVariableType] from azure.identity import DefaultAzureCredential from dotenv import load_dotenv @@ -105,7 +105,8 @@ async def before_run( def main(): # Create an Agent using the Azure OpenAI Chat Client - agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent( + agent = Agent( + client=FoundryChatClient(credential=DefaultAzureCredential()), name="SupportSpecialist", instructions=( "You are a helpful support specialist for Contoso Outdoors. " diff --git a/python/samples/05-end-to-end/hosted_agents/agents_in_workflow/main.py b/python/samples/05-end-to-end/hosted_agents/agents_in_workflow/main.py index 4afa83cd07..66ad933216 100644 --- a/python/samples/05-end-to-end/hosted_agents/agents_in_workflow/main.py +++ b/python/samples/05-end-to-end/hosted_agents/agents_in_workflow/main.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework import Agent +from agent_framework.azure import FoundryChatClient from agent_framework_orchestrations import ConcurrentBuilder from azure.ai.agentserver.agentframework import from_agent_framework from azure.identity import DefaultAzureCredential # pyright: ignore[reportUnknownVariableType] @@ -12,21 +13,24 @@ def main(): # Create agents - researcher = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent( + researcher = Agent( + client=FoundryChatClient(credential=DefaultAzureCredential()), instructions=( "You're an expert market and product researcher. " "Given a prompt, provide concise, factual insights, opportunities, and risks." ), name="researcher", ) - marketer = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent( + marketer = Agent( + client=FoundryChatClient(credential=DefaultAzureCredential()), instructions=( "You're a creative marketing strategist. " "Craft compelling value propositions and target messaging aligned to the prompt." ), name="marketer", ) - legal = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent( + legal = Agent( + client=FoundryChatClient(credential=DefaultAzureCredential()), instructions=( "You're a cautious legal/compliance reviewer. " "Highlight constraints, disclaimers, and policy concerns based on the prompt." @@ -38,7 +42,7 @@ def main(): workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build() # Convert the workflow to an agent - workflow_agent = workflow.as_agent() + workflow_agent = Agent(client=workflow) # Run the agent as a hosted agent from_agent_framework(workflow_agent).run() diff --git a/python/samples/05-end-to-end/hosted_agents/writer_reviewer_agents_in_workflow/main.py b/python/samples/05-end-to-end/hosted_agents/writer_reviewer_agents_in_workflow/main.py index af2c049808..13ff15b219 100644 --- a/python/samples/05-end-to-end/hosted_agents/writer_reviewer_agents_in_workflow/main.py +++ b/python/samples/05-end-to-end/hosted_agents/writer_reviewer_agents_in_workflow/main.py @@ -4,8 +4,8 @@ import os from contextlib import asynccontextmanager -from agent_framework import WorkflowBuilder -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent, WorkflowBuilder +from agent_framework.azure import FoundryChatClient from azure.ai.agentserver.agentframework import from_agent_framework from azure.identity.aio import AzureCliCredential, ManagedIdentityCredential from dotenv import load_dotenv @@ -34,16 +34,16 @@ def get_credential(): @asynccontextmanager async def create_agents(): async with get_credential() as credential: - client = AzureOpenAIResponsesClient( + client = FoundryChatClient( project_endpoint=PROJECT_ENDPOINT, - deployment_name=MODEL_DEPLOYMENT_NAME, + model=MODEL_DEPLOYMENT_NAME, credential=credential, ) - writer = client.as_agent( + writer = Agent(client=client, name="Writer", instructions="You are an excellent content writer. You create new content and edit contents based on the feedback.", ) - reviewer = client.as_agent( + reviewer = Agent(client=client, name="Reviewer", instructions="You are an excellent content reviewer. Provide actionable feedback to the writer about the provided content in the most concise manner possible.", ) @@ -52,7 +52,7 @@ async def create_agents(): def create_workflow(writer, reviewer): workflow = WorkflowBuilder(start_executor=writer).add_edge(writer, reviewer).build() - return workflow.as_agent() + return Agent(client=workflow,) async def main() -> None: diff --git a/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py b/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py index 8cd66d3dc1..5a5856fdd6 100644 --- a/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py +++ b/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py @@ -19,7 +19,7 @@ from typing import Annotated from agent_framework import Agent, tool -from agent_framework.openai import OpenAIChatClient +from agent_framework.azure import FoundryChatClient from aiohttp import web from aiohttp.web_middlewares import middleware from dotenv import load_dotenv @@ -101,7 +101,8 @@ def get_weather( def build_agent() -> Agent: """Create and return the chat agent instance with weather tool registered.""" - return OpenAIChatClient().as_agent( + _client = FoundryChatClient() + return Agent(client=_client, name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather ) diff --git a/python/samples/05-end-to-end/purview_agent/sample_purview_agent.py b/python/samples/05-end-to-end/purview_agent/sample_purview_agent.py index 2e98f05b10..da0bde80a7 100644 --- a/python/samples/05-end-to-end/purview_agent/sample_purview_agent.py +++ b/python/samples/05-end-to-end/purview_agent/sample_purview_agent.py @@ -26,7 +26,7 @@ from typing import Any from agent_framework import Agent, AgentResponse, Message -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from agent_framework.microsoft import ( PurviewChatPolicyMiddleware, PurviewPolicyMiddleware, @@ -145,7 +145,7 @@ async def run_with_agent_middleware() -> None: deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") - client = AzureOpenAIChatClient(deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential()) + client = FoundryChatClient(model=deployment, endpoint=endpoint, credential=AzureCliCredential()) purview_agent_middleware = PurviewPolicyMiddleware( build_credential(), @@ -182,8 +182,8 @@ async def run_with_chat_middleware() -> None: deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", default="gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") - client = AzureOpenAIChatClient( - deployment_name=deployment, + client = FoundryChatClient( + model=deployment, endpoint=endpoint, credential=AzureCliCredential(), middleware=[ @@ -231,7 +231,7 @@ async def run_with_custom_cache_provider() -> None: deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") - client = AzureOpenAIChatClient(deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential()) + client = FoundryChatClient(model=deployment, endpoint=endpoint, credential=AzureCliCredential()) custom_cache = SimpleDictCacheProvider() @@ -271,7 +271,7 @@ async def run_with_custom_cache_provider() -> None: deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") - client = AzureOpenAIChatClient(deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential()) + client = FoundryChatClient(model=deployment, endpoint=endpoint, credential=AzureCliCredential()) # No cache_provider specified - uses default InMemoryCacheProvider purview_agent_middleware = PurviewPolicyMiddleware( diff --git a/python/samples/05-end-to-end/workflow_evaluation/create_workflow.py b/python/samples/05-end-to-end/workflow_evaluation/create_workflow.py index 12a4286de0..be794ce68d 100644 --- a/python/samples/05-end-to-end/workflow_evaluation/create_workflow.py +++ b/python/samples/05-end-to-end/workflow_evaluation/create_workflow.py @@ -46,6 +46,7 @@ validate_payment_method, ) from agent_framework import ( + Agent, AgentExecutorResponse, AgentResponseUpdate, Executor, @@ -56,7 +57,7 @@ executor, handler, ) -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from azure.ai.projects.aio import AIProjectClient from azure.identity.aio import DefaultAzureCredential from dotenv import load_dotenv @@ -74,9 +75,10 @@ async def start_executor(input: str, ctx: WorkflowContext[list[Message]]) -> Non class ResearchLead(Executor): """Aggregates and summarizes travel planning findings from all specialized agents.""" - def __init__(self, client: AzureOpenAIResponsesClient, id: str = "travel-planning-coordinator"): + def __init__(self, client: FoundryChatClient, id: str = "travel-planning-coordinator"): # Use default_options to persist conversation history for evaluation. - self.agent = client.as_agent( + self.agent = Agent( + client=client, id="travel-planning-coordinator", instructions=( "You are the final coordinator. You will receive responses from multiple agents: " @@ -143,13 +145,13 @@ def _extract_agent_findings(self, responses: list[AgentExecutorResponse]) -> lis async def run_workflow_with_response_tracking( - query: str, client: AzureOpenAIResponsesClient | None = None, deployment_name: str | None = None + query: str, client: FoundryChatClient | None = None, deployment_name: str | None = None ) -> dict: """Run multi-agent workflow and track conversation IDs, response IDs, and interaction sequence. Args: query: The user query to process through the multi-agent workflow - client: Optional AzureOpenAIResponsesClient instance + client: Optional FoundryChatClient instance deployment_name: Optional model deployment name for the workflow agents Returns: @@ -159,12 +161,12 @@ async def run_workflow_with_response_tracking( try: async with DefaultAzureCredential() as credential: project_client = AIProjectClient( - endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], + endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=credential, ) async with project_client: - client = AzureOpenAIResponsesClient(project_client=project_client, deployment_name=deployment_name) + client = FoundryChatClient(project_client=project_client, model=deployment_name) return await _run_workflow_with_client(query, client) except Exception as e: print(f"Error during workflow execution: {e}") @@ -173,7 +175,7 @@ async def run_workflow_with_response_tracking( return await _run_workflow_with_client(query, client) -async def _run_workflow_with_client(query: str, client: AzureOpenAIResponsesClient) -> dict: +async def _run_workflow_with_client(query: str, client: FoundryChatClient) -> dict: """Execute workflow with given client and track all interactions.""" # Initialize tracking variables - use lists to track multiple responses per agent @@ -205,16 +207,17 @@ def track_ids(event: WorkflowEvent) -> WorkflowEvent: } -async def _create_workflow(client: AzureOpenAIResponsesClient): +async def _create_workflow(client: FoundryChatClient): """Create the multi-agent travel planning workflow with specialized agents. - Uses a single shared AzureOpenAIResponsesClient for all agents. + Uses a single shared FoundryChatClient for all agents. """ final_coordinator = ResearchLead(client=client, id="final-coordinator") # Agent 1: Travel Request Handler (initial coordinator) - travel_request_handler = client.as_agent( + travel_request_handler = Agent( + client=client, id="travel-request-handler", instructions=( "You receive user travel queries and relay them to specialized agents. Extract key information: destination, dates, budget, and preferences. Pass this information forward clearly to the next agents." @@ -223,7 +226,8 @@ async def _create_workflow(client: AzureOpenAIResponsesClient): ) # Agent 2: Hotel Search Executor - hotel_search_agent = client.as_agent( + hotel_search_agent = Agent( + client=client, id="hotel-search-agent", instructions=( "You are a hotel search specialist. Your task is ONLY to search for and provide hotel information. Use search_hotels to find options, get_hotel_details for specifics, and check_availability to verify rooms. Output format: List hotel names, prices per night, total cost for the stay, locations, ratings, amenities, and addresses. IMPORTANT: Only provide hotel information without additional commentary." @@ -233,7 +237,8 @@ async def _create_workflow(client: AzureOpenAIResponsesClient): ) # Agent 3: Flight Search Executor - flight_search_agent = client.as_agent( + flight_search_agent = Agent( + client=client, id="flight-search-agent", instructions=( "You are a flight search specialist. Your task is ONLY to search for and provide flight information. Use search_flights to find options, get_flight_details for specifics, and check_availability for seats. Output format: List flight numbers, airlines, departure/arrival times, prices, durations, and cabin class. IMPORTANT: Only provide flight information without additional commentary." @@ -243,7 +248,8 @@ async def _create_workflow(client: AzureOpenAIResponsesClient): ) # Agent 4: Activity Search Executor - activity_search_agent = client.as_agent( + activity_search_agent = Agent( + client=client, id="activity-search-agent", instructions=( "You are an activities specialist. Your task is ONLY to search for and provide activity information. Use search_activities to find options for activities. Output format: List activity names, descriptions, prices, durations, ratings, and categories. IMPORTANT: Only provide activity information without additional commentary." @@ -253,7 +259,8 @@ async def _create_workflow(client: AzureOpenAIResponsesClient): ) # Agent 5: Booking Confirmation Executor - booking_confirmation_agent = client.as_agent( + booking_confirmation_agent = Agent( + client=client, id="booking-confirmation-agent", instructions=( "You confirm bookings. Use check_hotel_availability and check_flight_availability to verify slots, then confirm_booking to finalize. Provide ONLY: confirmation numbers, booking references, and confirmation status." @@ -263,7 +270,8 @@ async def _create_workflow(client: AzureOpenAIResponsesClient): ) # Agent 6: Booking Payment Executor - booking_payment_agent = client.as_agent( + booking_payment_agent = Agent( + client=client, id="booking-payment-agent", instructions=( "You process payments. Use validate_payment_method to verify payment, then process_payment to complete transactions. Provide ONLY: payment confirmation status, transaction IDs, and payment amounts." @@ -273,7 +281,8 @@ async def _create_workflow(client: AzureOpenAIResponsesClient): ) # Agent 7: Booking Information Aggregation Executor - booking_info_aggregation_agent = client.as_agent( + booking_info_aggregation_agent = Agent( + client=client, id="booking-info-aggregation-agent", instructions=( "You aggregate hotel and flight search results. Receive options from search agents and organize them. Provide: top 2-3 hotel options with prices and top 2-3 flight options with prices in a structured format." @@ -356,7 +365,7 @@ async def create_and_run_workflow(deployment_name: str | None = None): query = example_queries[0] print(f"Query: {query}\n") - result = await run_workflow_with_response_tracking(query, deployment_name=deployment_name) + result = await run_workflow_with_response_tracking(query, model=deployment_name) # Create output data structure output_data = {"agents": {}, "query": result["query"], "output": result.get("output", "")} diff --git a/python/samples/05-end-to-end/workflow_evaluation/run_evaluation.py b/python/samples/05-end-to-end/workflow_evaluation/run_evaluation.py index 6ad3641721..2ac8b90cb7 100644 --- a/python/samples/05-end-to-end/workflow_evaluation/run_evaluation.py +++ b/python/samples/05-end-to-end/workflow_evaluation/run_evaluation.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any from azure.ai.projects import AIProjectClient -from azure.identity import DefaultAzureCredential +from azure.identity import AzureCliCredential from create_workflow import create_and_run_workflow from dotenv import load_dotenv @@ -33,8 +33,8 @@ def create_openai_client() -> OpenAI: project_client = AIProjectClient( - endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - credential=DefaultAzureCredential(), + endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + credential=AzureCliCredential(), ) return project_client.get_openai_client() @@ -58,7 +58,7 @@ async def run_workflow(deployment_name: str | None = None) -> dict[str, Any]: print("Executing multi-agent travel planning workflow...") print("This may take a few minutes...") - workflow_data = await create_and_run_workflow(deployment_name=deployment_name) + workflow_data = await create_and_run_workflow(model=deployment_name) print("Workflow execution completed") return workflow_data @@ -216,7 +216,7 @@ async def main(): print_section("Travel Planning Workflow Evaluation") print_section("Step 1: Running Workflow") - workflow_data = await run_workflow(deployment_name=workflow_agent_model) + workflow_data = await run_workflow(model=workflow_agent_model) print_section("Step 2: Response Data Summary") display_response_summary(workflow_data) @@ -225,7 +225,7 @@ async def main(): fetch_agent_responses(openai_client, workflow_data, agents_to_evaluate) print_section("Step 4: Creating Evaluation") - eval_object = create_evaluation(openai_client, deployment_name=eval_model) + eval_object = create_evaluation(openai_client, model=eval_model) print_section("Step 5: Running Evaluation") eval_run = run_evaluation(openai_client, eval_object, workflow_data, agents_to_evaluate) diff --git a/python/samples/SAMPLE_GUIDELINES.md b/python/samples/SAMPLE_GUIDELINES.md index b93f5bdd14..74c839a1a1 100644 --- a/python/samples/SAMPLE_GUIDELINES.md +++ b/python/samples/SAMPLE_GUIDELINES.md @@ -36,29 +36,77 @@ When samples depend on external packages not included in the dev environment (e. This makes samples self-contained and runnable without installing extra packages into the dev environment. Do not add sample-only dependencies to the root `pyproject.toml` dev group. +## Azure Credentials + +**Always use `AzureCliCredential`** in samples — never `DefaultAzureCredential`. `AzureCliCredential` is explicit about the authentication method (requires `az login`) and avoids unexpected credential resolution that can confuse newcomers. + +```python +from azure.identity import AzureCliCredential + +credential = AzureCliCredential() +``` + ## Environment Variables -All samples that use environment variables (API keys, endpoints, etc.) must call `load_dotenv()` at the beginning of the file to load variables from a `.env` file. The `python-dotenv` package is already included as a dependency of `agent-framework-core`. +### Basic / Getting Started Samples + +For getting started samples (`01-get-started/`) and `basic` samples, use **explicit placeholder values** for non-sensitive parameters to make the code immediately readable: ```python # Copyright (c) Microsoft. All rights reserved. import asyncio -import os -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework import Agent +from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() """ Sample docstring explaining what the sample does. """ + + +async def main() -> None: + client = FoundryChatClient( + project_endpoint="https://your-project.services.ai.azure.com", + model="gpt-4o", + credential=AzureCliCredential(), + ) + agent = Agent(client=client, name="MyAgent", instructions="You are helpful.") + result = await agent.run("Hello!") + print(result) +``` + +Basic samples should NOT use `os.environ`, `load_dotenv()`, or `.env` files. The placeholder values make the code self-documenting. + +### Advanced Samples + +For advanced samples that require real credentials or multiple configuration values, use environment variables with `os.environ` and document the required variables in the module docstring: + +```python +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os + +from agent_framework import Agent +from agent_framework.azure import FoundryChatClient +from azure.identity import AzureCliCredential + +""" +Advanced sample demonstrating feature X. + +Environment variables: + FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint + FOUNDRY_MODEL — Model deployment name (e.g. gpt-4o) +""" ``` -Users can create a `.env` file in the `python/` directory based on `.env.example` to set their environment variables without having to export them in their shell. +### Default Client for Samples + +Unless a sample is specifically demonstrating a particular provider (OpenAI direct, Anthropic, Ollama, etc.), use `FoundryChatClient` from `agent_framework.azure` as the default client. This is the recommended client for Azure AI Foundry deployments. + +Provider-specific samples belong in `02-agents/providers//` and should use the provider's native client (e.g., `OpenAIChatClient` for OpenAI, `AnthropicClient` for Anthropic). ## Syntax Checking diff --git a/python/samples/autogen-migration/orchestrations/01_round_robin_group_chat.py b/python/samples/autogen-migration/orchestrations/01_round_robin_group_chat.py index e5c6bd09f8..ce15571701 100644 --- a/python/samples/autogen-migration/orchestrations/01_round_robin_group_chat.py +++ b/python/samples/autogen-migration/orchestrations/01_round_robin_group_chat.py @@ -76,17 +76,20 @@ async def run_agent_framework() -> None: client = OpenAIChatClient(model_id="gpt-4.1-mini") # Create specialized agents - researcher = client.as_agent( + researcher = Agent( + client=client, name="researcher", instructions="You are a researcher. Provide facts and data about the topic.", ) - writer = client.as_agent( + writer = Agent( + client=client, name="writer", instructions="You are a writer. Turn research into engaging content.", ) - editor = client.as_agent( + editor = Agent( + client=client, name="editor", instructions="You are an editor. Review and finalize the content.", ) @@ -107,6 +110,7 @@ async def run_agent_framework() -> None: async def run_agent_framework_with_cycle() -> None: """Agent Framework's WorkflowBuilder with cyclic edges and conditional exit.""" from agent_framework import ( + Agent, AgentExecutorRequest, AgentExecutorResponse, AgentResponseUpdate, @@ -119,17 +123,20 @@ async def run_agent_framework_with_cycle() -> None: client = OpenAIChatClient(model_id="gpt-4.1-mini") # Create specialized agents - researcher = client.as_agent( + researcher = Agent( + client=client, name="researcher", instructions="You are a researcher. Provide facts and data about the topic.", ) - writer = client.as_agent( + writer = Agent( + client=client, name="writer", instructions="You are a writer. Turn research into engaging content.", ) - editor = client.as_agent( + editor = Agent( + client=client, name="editor", instructions="You are an editor. Review and finalize the content. End with APPROVED if satisfied.", ) diff --git a/python/samples/autogen-migration/orchestrations/02_selector_group_chat.py b/python/samples/autogen-migration/orchestrations/02_selector_group_chat.py index 6f16e1dea9..a0fc5b418c 100644 --- a/python/samples/autogen-migration/orchestrations/02_selector_group_chat.py +++ b/python/samples/autogen-migration/orchestrations/02_selector_group_chat.py @@ -17,7 +17,7 @@ import asyncio -from agent_framework import Message +from agent_framework import Agent, Message from dotenv import load_dotenv # Load environment variables from .env file @@ -79,22 +79,22 @@ async def run_agent_framework() -> None: from agent_framework.openai import OpenAIChatClient from agent_framework.orchestrations import GroupChatBuilder - client = OpenAIChatClient(model_id="gpt-4.1-mini") + client = OpenAIChatClient(model="gpt-4.1-mini") # Create specialized agents - python_expert = client.as_agent( + python_expert = Agent(client=client, name="python_expert", instructions="You are a Python programming expert. Answer Python-related questions.", description="Expert in Python programming", ) - javascript_expert = client.as_agent( + javascript_expert = Agent(client=client, name="javascript_expert", instructions="You are a JavaScript programming expert. Answer JavaScript-related questions.", description="Expert in JavaScript programming", ) - database_expert = client.as_agent( + database_expert = Agent(client=client, name="database_expert", instructions="You are a database expert. Answer SQL and database-related questions.", description="Expert in databases and SQL", @@ -103,7 +103,7 @@ async def run_agent_framework() -> None: workflow = GroupChatBuilder( participants=[python_expert, javascript_expert, database_expert], max_rounds=1, - orchestrator_agent=client.as_agent( + orchestrator_agent=Agent(client=client, name="selector_manager", instructions="Based on the conversation, select the most appropriate expert to respond next.", ), diff --git a/python/samples/autogen-migration/orchestrations/03_swarm.py b/python/samples/autogen-migration/orchestrations/03_swarm.py index a178ffcffe..aea5b9723b 100644 --- a/python/samples/autogen-migration/orchestrations/03_swarm.py +++ b/python/samples/autogen-migration/orchestrations/03_swarm.py @@ -18,7 +18,7 @@ import asyncio from typing import Any -from agent_framework import AgentResponseUpdate, WorkflowEvent +from agent_framework import Agent, AgentResponseUpdate, WorkflowEvent from dotenv import load_dotenv # Load environment variables from .env file @@ -119,10 +119,10 @@ async def run_agent_framework() -> None: from agent_framework.openai import OpenAIChatClient from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder - client = OpenAIChatClient(model_id="gpt-4.1-mini") + client = OpenAIChatClient(model="gpt-4.1-mini") # Create triage agent - triage_agent = client.as_agent( + triage_agent = Agent(client=client, name="triage", instructions=( "You are a triage agent. Analyze the user's request and route to the appropriate specialist:\n" @@ -133,14 +133,14 @@ async def run_agent_framework() -> None: ) # Create billing specialist - billing_agent = client.as_agent( + billing_agent = Agent(client=client, name="billing_agent", instructions="You are a billing specialist. Help with payment and billing questions. Provide clear assistance.", description="Handles billing and payment questions", ) # Create technical support specialist - tech_support = client.as_agent( + tech_support = Agent(client=client, name="technical_support", instructions="You are technical support. Help with technical issues. Provide clear assistance.", description="Handles technical support questions", diff --git a/python/samples/autogen-migration/orchestrations/04_magentic_one.py b/python/samples/autogen-migration/orchestrations/04_magentic_one.py index b6728b0e46..8d4aba9ba5 100644 --- a/python/samples/autogen-migration/orchestrations/04_magentic_one.py +++ b/python/samples/autogen-migration/orchestrations/04_magentic_one.py @@ -20,6 +20,7 @@ from typing import cast from agent_framework import ( + Agent, AgentResponseUpdate, Message, WorkflowEvent, @@ -87,19 +88,22 @@ async def run_agent_framework() -> None: client = OpenAIChatClient(model_id="gpt-4.1-mini") # Create specialized agents - researcher = client.as_agent( + researcher = Agent( + client=client, name="researcher", instructions="You are a research analyst. Gather and analyze information.", description="Research analyst for data gathering", ) - coder = client.as_agent( + coder = Agent( + client=client, name="coder", instructions="You are a programmer. Write code based on requirements.", description="Software developer for implementation", ) - reviewer = client.as_agent( + reviewer = Agent( + client=client, name="reviewer", instructions="You are a code reviewer. Review code for quality and correctness.", description="Code reviewer for quality assurance", @@ -108,7 +112,8 @@ async def run_agent_framework() -> None: # Create Magentic workflow workflow = MagenticBuilder( participants=[researcher, coder, reviewer], - manager_agent=client.as_agent( + manager_agent=Agent( + client=client, name="magentic_manager", instructions="You coordinate a team to complete complex tasks efficiently.", description="Orchestrator for team coordination", diff --git a/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py b/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py index 73a3caba02..4cf093fa9c 100644 --- a/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py +++ b/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py @@ -1,4 +1,6 @@ # /// script +from agent_framework import Agent + # requires-python = ">=3.10" # dependencies = [ # "autogen-agentchat", @@ -48,8 +50,8 @@ async def run_agent_framework() -> None: from agent_framework.openai import OpenAIChatClient # AF constructs a lightweight Agent backed by OpenAIChatClient - client = OpenAIChatClient(model_id="gpt-4.1-mini") - agent = client.as_agent( + client = OpenAIChatClient(model="gpt-4.1-mini") + agent = Agent(client=client, name="assistant", instructions="You are a helpful assistant. Answer in one sentence.", ) diff --git a/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py b/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py index aca868b9f2..97432e9b98 100644 --- a/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py +++ b/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py @@ -64,7 +64,7 @@ def get_weather(location: str) -> str: async def run_agent_framework() -> None: """Agent Framework agent with @tool decorator.""" - from agent_framework import tool + from agent_framework import Agent, tool from agent_framework.openai import OpenAIChatClient # Define tool with @tool decorator (automatic schema inference) @@ -82,8 +82,8 @@ def get_weather(location: str) -> str: return f"The weather in {location} is sunny and 72°F." # Create agent with tool - client = OpenAIChatClient(model_id="gpt-4.1-mini") - agent = client.as_agent( + client = OpenAIChatClient(model="gpt-4.1-mini") + agent = Agent(client=client, name="assistant", instructions="You are a helpful assistant. Use available tools to answer questions.", tools=[get_weather], diff --git a/python/samples/autogen-migration/single_agent/03_assistant_agent_thread_and_stream.py b/python/samples/autogen-migration/single_agent/03_assistant_agent_thread_and_stream.py index c544880cb1..f4a2203371 100644 --- a/python/samples/autogen-migration/single_agent/03_assistant_agent_thread_and_stream.py +++ b/python/samples/autogen-migration/single_agent/03_assistant_agent_thread_and_stream.py @@ -1,4 +1,6 @@ # /// script +from agent_framework import Agent + # requires-python = ">=3.10" # dependencies = [ # "autogen-agentchat", @@ -55,8 +57,8 @@ async def run_agent_framework() -> None: """Agent Framework agent with explicit session and streaming.""" from agent_framework.openai import OpenAIChatClient - client = OpenAIChatClient(model_id="gpt-4.1-mini") - agent = client.as_agent( + client = OpenAIChatClient(model="gpt-4.1-mini") + agent = Agent(client=client, name="assistant", instructions="You are a helpful math tutor.", ) diff --git a/python/samples/autogen-migration/single_agent/04_agent_as_tool.py b/python/samples/autogen-migration/single_agent/04_agent_as_tool.py index 489ec74c01..aae76fd0c1 100644 --- a/python/samples/autogen-migration/single_agent/04_agent_as_tool.py +++ b/python/samples/autogen-migration/single_agent/04_agent_as_tool.py @@ -64,13 +64,13 @@ async def run_autogen() -> None: async def run_agent_framework() -> None: """Agent Framework's as_tool() for hierarchical agents with streaming.""" - from agent_framework import Content + from agent_framework import Agent, Content from agent_framework.openai import OpenAIChatClient - client = OpenAIChatClient(model_id="gpt-4.1-mini") + client = OpenAIChatClient(model="gpt-4.1-mini") # Create specialized writer agent - writer = client.as_agent( + writer = Agent(client=client, name="writer", instructions="You are a creative writer. Write short, engaging content.", ) @@ -84,7 +84,7 @@ async def run_agent_framework() -> None: ) # Create coordinator agent with writer tool - coordinator = client.as_agent( + coordinator = Agent(client=client, name="coordinator", instructions="You coordinate with specialized agents. Delegate writing tasks to the writer agent.", tools=[writer_tool], diff --git a/python/samples/demos/ag_ui_workflow_handoff/backend/server.py b/python/samples/demos/ag_ui_workflow_handoff/backend/server.py index 6fca903849..800a4d3224 100644 --- a/python/samples/demos/ag_ui_workflow_handoff/backend/server.py +++ b/python/samples/demos/ag_ui_workflow_handoff/backend/server.py @@ -80,12 +80,12 @@ def lookup_order_details(order_id: str) -> dict[str, str]: def create_agents() -> tuple[Agent, Agent, Agent]: """Create triage, refund, and order agents for the handoff workflow.""" - from agent_framework.azure import AzureOpenAIResponsesClient + from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential - client = AzureOpenAIResponsesClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) diff --git a/python/samples/semantic-kernel-migration/azure_ai_agent/01_basic_azure_ai_agent.py b/python/samples/semantic-kernel-migration/azure_ai_agent/01_basic_azure_ai_agent.py deleted file mode 100644 index b10f38f779..0000000000 --- a/python/samples/semantic-kernel-migration/azure_ai_agent/01_basic_azure_ai_agent.py +++ /dev/null @@ -1,65 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = [ -# "semantic-kernel", -# ] -# /// -# Run with any PEP 723 compatible runner, e.g.: -# uv run samples/semantic-kernel-migration/azure_ai_agent/01_basic_azure_ai_agent.py - -# Copyright (c) Microsoft. All rights reserved. -"""Create an Azure AI agent using both Semantic Kernel and Agent Framework. - -Prerequisites: -- Azure AI agent resource with a deployed model. -- Logged-in Azure CLI or other credential supported by AzureCliCredential. -""" - -import asyncio - -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - - -async def run_semantic_kernel() -> None: - from azure.identity.aio import AzureCliCredential - from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings - - async with AzureCliCredential() as credential, AzureAIAgent.create_client(credential=credential) as client: - settings = AzureAIAgentSettings() # Reads env vars for region/deployment. - # SK builds the remote agent definition then wraps it with AzureAIAgent. - definition = await client.agents.create_agent( - model=settings.model_deployment_name, - name="Support", - instructions="Answer customer questions in one paragraph.", - ) - agent = AzureAIAgent(client=client, definition=definition) - response = await agent.get_response("How do I upgrade my plan?") - print("[SK]", response.message.content) - - -async def run_agent_framework() -> None: - from agent_framework.azure import AzureAIAgentClient - from azure.identity.aio import AzureCliCredential - - async with ( - AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( - name="Support", - instructions="Answer customer questions in one paragraph.", - ) as agent, - ): - # AF client returns an asynchronous context manager for remote agents. - reply = await agent.run("How do I upgrade my plan?") - print("[AF]", reply.text) - - -async def main() -> None: - await run_semantic_kernel() - await run_agent_framework() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/semantic-kernel-migration/azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py b/python/samples/semantic-kernel-migration/azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py deleted file mode 100644 index 599fcf75ad..0000000000 --- a/python/samples/semantic-kernel-migration/azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py +++ /dev/null @@ -1,76 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = [ -# "semantic-kernel", -# ] -# /// -# Run with any PEP 723 compatible runner, e.g.: -# uv run samples/semantic-kernel-migration/azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py - -# Copyright (c) Microsoft. All rights reserved. -"""Enable the hosted code interpreter for Azure AI agents in SK and AF. - -The Azure AI service natively executes the code interpreter tool. Provide the -resource details via AzureAIAgentSettings (SK) or environment variables consumed -by AzureAIAgentClient (AF). -""" - -import asyncio - -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - - -async def run_semantic_kernel() -> None: - from azure.identity.aio import AzureCliCredential - from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings - - async with AzureCliCredential() as credential, AzureAIAgent.create_client(credential=credential) as client: - settings = AzureAIAgentSettings() - # Register the hosted code interpreter tool with the remote agent. - definition = await client.agents.create_agent( - model=settings.model_deployment_name, - name="Analyst", - instructions="Use the code interpreter for numeric work.", - tools=[{"type": "code_interpreter"}], - ) - agent = AzureAIAgent(client=client, definition=definition) - response = await agent.get_response( - "Use Python to compute 42 ** 2 and explain the result.", - ) - print("[SK]", response.message.content) - - -async def run_agent_framework() -> None: - from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider - from azure.identity.aio import AzureCliCredential - - async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, - ): - code_interpreter_tool = AzureAIAgentClient.get_code_interpreter_tool() - - agent = await provider.create_agent( - name="Analyst", - instructions="Use the code interpreter for numeric work.", - tools=[code_interpreter_tool], - ) - - # Code interpreter tool mirrors the built-in Azure AI capability. - reply = await agent.run( - "Use Python to compute 42 ** 2 and explain the result.", - tool_choice="auto", - ) - print("[AF]", reply.text) - - -async def main() -> None: - await run_semantic_kernel() - await run_agent_framework() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/semantic-kernel-migration/azure_ai_agent/03_azure_ai_agent_threads_and_followups.py b/python/samples/semantic-kernel-migration/azure_ai_agent/03_azure_ai_agent_threads_and_followups.py deleted file mode 100644 index 4fb4de4085..0000000000 --- a/python/samples/semantic-kernel-migration/azure_ai_agent/03_azure_ai_agent_threads_and_followups.py +++ /dev/null @@ -1,81 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = [ -# "semantic-kernel", -# ] -# /// -# Run with any PEP 723 compatible runner, e.g.: -# uv run samples/semantic-kernel-migration/azure_ai_agent/03_azure_ai_agent_threads_and_followups.py - -# Copyright (c) Microsoft. All rights reserved. -"""Maintain Azure AI agent conversation state across turns in SK and AF.""" - -import asyncio - -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - - -async def run_semantic_kernel() -> None: - from azure.identity.aio import AzureCliCredential - from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread - - async with AzureCliCredential() as credential, AzureAIAgent.create_client(credential=credential) as client: - settings = AzureAIAgentSettings() - definition = await client.agents.create_agent( - model=settings.model_deployment_name, - name="Planner", - instructions="Track follow-up questions within the same thread.", - ) - agent = AzureAIAgent(client=client, definition=definition) - - thread: AzureAIAgentThread | None = None - # SK returns the updated AzureAIAgentThread on each response. - first = await agent.get_response("Outline the onboarding checklist.", thread=thread) - thread = first.thread - print("[SK][turn1]", first.message.content) - - second = await agent.get_response( - "Highlight the items that require legal review.", - thread=thread, - ) - print("[SK][turn2]", second.message.content) - if thread is not None: - print("[SK][thread-id]", thread.id) - - -async def run_agent_framework() -> None: - from agent_framework.azure import AzureAIAgentClient - from azure.identity.aio import AzureCliCredential - - async with ( - AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( - name="Planner", - instructions="Track follow-up questions within the same thread.", - ) as agent, - ): - session = agent.create_session() - # AF sessions are explicit and can be serialized for external storage. - first = await agent.run("Outline the onboarding checklist.", session=session) - print("[AF][turn1]", first.text) - - second = await agent.run( - "Highlight the items that require legal review.", - session=session, - ) - print("[AF][turn2]", second.text) - - serialized = session.to_dict() - print("[AF][session-json]", serialized) - - -async def main() -> None: - await run_semantic_kernel() - await run_agent_framework() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/semantic-kernel-migration/chat_completion/01_basic_chat_completion.py b/python/samples/semantic-kernel-migration/chat_completion/01_basic_chat_completion.py index 50e98c74ca..ebd10122dc 100644 --- a/python/samples/semantic-kernel-migration/chat_completion/01_basic_chat_completion.py +++ b/python/samples/semantic-kernel-migration/chat_completion/01_basic_chat_completion.py @@ -17,6 +17,7 @@ import asyncio +from agent_framework import Agent from dotenv import load_dotenv # Load environment variables from .env file @@ -44,7 +45,8 @@ async def run_agent_framework() -> None: from agent_framework.openai import OpenAIChatClient # AF constructs a lightweight Agent backed by OpenAIChatClient. - chat_agent = OpenAIChatClient().as_agent( + chat_agent = Agent( + client=OpenAIChatClient(), name="Support", instructions="Answer in one sentence.", ) diff --git a/python/samples/semantic-kernel-migration/chat_completion/02_chat_completion_with_tool.py b/python/samples/semantic-kernel-migration/chat_completion/02_chat_completion_with_tool.py index 78d45862e1..d84e560eb0 100644 --- a/python/samples/semantic-kernel-migration/chat_completion/02_chat_completion_with_tool.py +++ b/python/samples/semantic-kernel-migration/chat_completion/02_chat_completion_with_tool.py @@ -48,7 +48,7 @@ def specials(self) -> str: async def run_agent_framework() -> None: - from agent_framework import tool + from agent_framework import Agent, tool from agent_framework.openai import OpenAIChatClient @tool(name="specials", description="List daily specials") @@ -56,7 +56,8 @@ async def specials() -> str: return "Clam chowder, Cobb salad, Chai tea" # AF tools are provided as callables on each agent instance. - chat_agent = OpenAIChatClient().as_agent( + chat_agent = Agent( + client=OpenAIChatClient(), name="Host", instructions="Answer menu questions accurately.", tools=[specials], diff --git a/python/samples/semantic-kernel-migration/chat_completion/03_chat_completion_thread_and_stream.py b/python/samples/semantic-kernel-migration/chat_completion/03_chat_completion_thread_and_stream.py index fc4658bfab..f656220f20 100644 --- a/python/samples/semantic-kernel-migration/chat_completion/03_chat_completion_thread_and_stream.py +++ b/python/samples/semantic-kernel-migration/chat_completion/03_chat_completion_thread_and_stream.py @@ -16,6 +16,7 @@ import asyncio +from agent_framework import Agent from dotenv import load_dotenv # Load environment variables from .env file @@ -54,7 +55,8 @@ async def run_agent_framework() -> None: from agent_framework.openai import OpenAIChatClient # AF session objects are requested explicitly from the agent. - chat_agent = OpenAIChatClient().as_agent( + chat_agent = Agent( + client=OpenAIChatClient(), name="Writer", instructions="Keep answers short and friendly.", ) diff --git a/python/samples/semantic-kernel-migration/openai_assistant/01_basic_openai_assistant.py b/python/samples/semantic-kernel-migration/openai_assistant/01_basic_openai_assistant.py index 1c0b5a3ae4..f171d076bd 100644 --- a/python/samples/semantic-kernel-migration/openai_assistant/01_basic_openai_assistant.py +++ b/python/samples/semantic-kernel-migration/openai_assistant/01_basic_openai_assistant.py @@ -9,21 +9,19 @@ # Copyright (c) Microsoft. All rights reserved. """Create an OpenAI Assistant using SK and Agent Framework.""" - import asyncio import os +from agent_framework import Agent from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() - ASSISTANT_MODEL = os.environ.get("OPENAI_ASSISTANT_MODEL", "gpt-4o-mini") async def run_semantic_kernel() -> None: from semantic_kernel.agents import AssistantAgentThread, OpenAIAssistantAgent - client = OpenAIAssistantAgent.create_client() # Provision the assistant on the OpenAI Assistants service. definition = await client.beta.assistants.create( @@ -32,7 +30,6 @@ async def run_semantic_kernel() -> None: instructions="Answer questions in one concise paragraph.", ) agent = OpenAIAssistantAgent(client=client, definition=definition) - thread: AssistantAgentThread | None = None response = await agent.get_response("What is the capital of Denmark?", thread=thread) thread = response.thread @@ -43,13 +40,10 @@ async def run_semantic_kernel() -> None: async def run_agent_framework() -> None: from agent_framework.openai import OpenAIAssistantsClient - assistants_client = OpenAIAssistantsClient() # AF wraps the assistant lifecycle with an async context manager. - async with assistants_client.as_agent( - name="Helper", - instructions="Answer questions in one concise paragraph.", - model=ASSISTANT_MODEL, + async with Agent( + client=assistants_client, ) as assistant_agent: session = assistant_agent.create_session() reply = await assistant_agent.run("What is the capital of Denmark?", session=session) diff --git a/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py b/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py index b9407149d6..09b44d0134 100644 --- a/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py +++ b/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py @@ -1,4 +1,6 @@ # /// script +from agent_framework import Agent + # requires-python = ">=3.10" # dependencies = [ # "semantic-kernel", @@ -50,7 +52,7 @@ async def run_agent_framework() -> None: code_interpreter_tool = OpenAIAssistantsClient.get_code_interpreter_tool() # AF exposes the same tool configuration via create_agent. - async with assistants_client.as_agent( + async with Agent(client=assistants_client, name="CodeRunner", instructions="Use the code interpreter when calculations are required.", model="gpt-4.1", diff --git a/python/samples/semantic-kernel-migration/openai_assistant/03_openai_assistant_function_tool.py b/python/samples/semantic-kernel-migration/openai_assistant/03_openai_assistant_function_tool.py index be395cafa6..36d6fea208 100644 --- a/python/samples/semantic-kernel-migration/openai_assistant/03_openai_assistant_function_tool.py +++ b/python/samples/semantic-kernel-migration/openai_assistant/03_openai_assistant_function_tool.py @@ -69,7 +69,7 @@ async def fake_weather_lookup(self, city: str, day: str) -> dict[str, Any]: async def run_agent_framework() -> None: - from agent_framework import tool + from agent_framework import Agent, tool from agent_framework.openai import OpenAIAssistantsClient @tool( @@ -81,7 +81,7 @@ async def get_forecast(city: str, day: str) -> dict[str, Any]: assistants_client = OpenAIAssistantsClient() # AF converts the decorated function into an assistant-compatible tool. - async with assistants_client.as_agent( + async with Agent(client=assistants_client, name="WeatherHelper", instructions="Call get_forecast to fetch weather details.", model=ASSISTANT_MODEL, diff --git a/python/samples/semantic-kernel-migration/openai_responses/01_basic_responses_agent.py b/python/samples/semantic-kernel-migration/openai_responses/01_basic_responses_agent.py index 556407c969..54994d7f1f 100644 --- a/python/samples/semantic-kernel-migration/openai_responses/01_basic_responses_agent.py +++ b/python/samples/semantic-kernel-migration/openai_responses/01_basic_responses_agent.py @@ -25,7 +25,7 @@ async def run_semantic_kernel() -> None: client = OpenAIResponsesAgent.create_client() # SK response agents wrap OpenAI's hosted Responses API. agent = OpenAIResponsesAgent( - ai_model_id=OpenAISettings().responses_model_id, + ai_model=OpenAISettings().responses_model_id, client=client, instructions="Answer in one concise sentence.", name="Expert", diff --git a/python/samples/semantic-kernel-migration/openai_responses/02_responses_agent_with_tool.py b/python/samples/semantic-kernel-migration/openai_responses/02_responses_agent_with_tool.py index ed2609783c..d2855a7810 100644 --- a/python/samples/semantic-kernel-migration/openai_responses/02_responses_agent_with_tool.py +++ b/python/samples/semantic-kernel-migration/openai_responses/02_responses_agent_with_tool.py @@ -31,7 +31,7 @@ def add(self, a: float, b: float) -> float: client = OpenAIResponsesAgent.create_client() # Plugins advertise callable tools to the Responses agent. agent = OpenAIResponsesAgent( - ai_model_id=OpenAISettings().responses_model_id, + ai_model=OpenAISettings().responses_model_id, client=client, instructions="Use the add tool when math is required.", name="MathExpert", diff --git a/python/samples/semantic-kernel-migration/openai_responses/03_responses_agent_structured_output.py b/python/samples/semantic-kernel-migration/openai_responses/03_responses_agent_structured_output.py index 277dbbda40..a4328ce05f 100644 --- a/python/samples/semantic-kernel-migration/openai_responses/03_responses_agent_structured_output.py +++ b/python/samples/semantic-kernel-migration/openai_responses/03_responses_agent_structured_output.py @@ -32,7 +32,7 @@ async def run_semantic_kernel() -> None: client = OpenAIResponsesAgent.create_client() # response_format requests schema-constrained output from the model. agent = OpenAIResponsesAgent( - ai_model_id=OpenAISettings().responses_model_id, + ai_model=OpenAISettings().responses_model_id, client=client, instructions="Return launch briefs as structured JSON.", name="ProductMarketer", diff --git a/python/samples/semantic-kernel-migration/orchestrations/concurrent_basic.py b/python/samples/semantic-kernel-migration/orchestrations/concurrent_basic.py index 38133dbad1..ed0a4b1495 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/concurrent_basic.py +++ b/python/samples/semantic-kernel-migration/orchestrations/concurrent_basic.py @@ -15,7 +15,7 @@ from collections.abc import Sequence from typing import cast -from agent_framework import Message +from agent_framework import Agent, Message from agent_framework.azure import AzureOpenAIChatClient from agent_framework.orchestrations import ConcurrentBuilder from azure.identity import AzureCliCredential @@ -91,12 +91,12 @@ def _print_semantic_kernel_outputs(outputs: Sequence[ChatMessageContent]) -> Non async def run_agent_framework_example(prompt: str) -> Sequence[list[Message]]: client = AzureOpenAIChatClient(credential=AzureCliCredential()) - physics = client.as_agent( + physics = Agent(client=client, instructions=("You are an expert in physics. Answer questions from a physics perspective."), name="physics", ) - chemistry = client.as_agent( + chemistry = Agent(client=client, instructions=("You are an expert in chemistry. Answer questions from a chemistry perspective."), name="chemistry", ) diff --git a/python/samples/semantic-kernel-migration/orchestrations/group_chat.py b/python/samples/semantic-kernel-migration/orchestrations/group_chat.py index c0d7aa3797..a59dbb5aad 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/group_chat.py +++ b/python/samples/semantic-kernel-migration/orchestrations/group_chat.py @@ -17,7 +17,7 @@ from typing import Any, cast from agent_framework import Agent, Message -from agent_framework.azure import AzureOpenAIChatClient, AzureOpenAIResponsesClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import GroupChatBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -235,19 +235,19 @@ async def run_agent_framework_example(task: str) -> str: "Gather concise facts or considerations that help plan a community hackathon. " "Keep your responses factual and scannable." ), - client=AzureOpenAIChatClient(credential=credential), + client=FoundryChatClient(credential=credential), ) planner = Agent( name="Planner", description="Turns the collected notes into a concrete action plan.", instructions=("Propose a structured action plan that accounts for logistics, roles, and timeline."), - client=AzureOpenAIResponsesClient(credential=credential), + client=FoundryChatClient(credential=credential), ) workflow = GroupChatBuilder( participants=[researcher, planner], - orchestrator_agent=AzureOpenAIChatClient(credential=credential).as_agent(), + orchestrator_agent=Agent(client=FoundryChatClient(credential=credential)), ).build() final_response = "" diff --git a/python/samples/semantic-kernel-migration/orchestrations/handoff.py b/python/samples/semantic-kernel-migration/orchestrations/handoff.py index c235da8fe8..b35fc8ee19 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/handoff.py +++ b/python/samples/semantic-kernel-migration/orchestrations/handoff.py @@ -16,10 +16,11 @@ from typing import cast from agent_framework import ( + Agent, Message, WorkflowEvent, ) -from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.azure import FoundryChatClient from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -189,8 +190,9 @@ async def run_semantic_kernel_example(initial_task: str, scripted_responses: Seq ###################################################################### -def _create_af_agents(client: AzureOpenAIChatClient): - triage = client.as_agent( +def _create_af_agents(client: FoundryChatClient): + triage = Agent( + client=client, name="triage_agent", instructions=( "You are a customer support triage agent. Route requests:\n" @@ -199,19 +201,22 @@ def _create_af_agents(client: AzureOpenAIChatClient): "- handoff_to_order_return_agent for returns" ), ) - refund = client.as_agent( + refund = Agent( + client=client, name="refund_agent", instructions=( "Handle refunds. Ask for order id and reason. If shipping info is needed, hand off to order_status_agent." ), ) - status = client.as_agent( + status = Agent( + client=client, name="order_status_agent", instructions=( "Provide order status, tracking, and timelines. If billing questions appear, hand off to refund_agent." ), ) - returns = client.as_agent( + returns = Agent( + client=client, name="order_return_agent", instructions=( "Coordinate returns, confirm addresses, and summarize next steps. Hand off to triage_agent if unsure." @@ -241,7 +246,7 @@ def _extract_final_conversation(events: list[WorkflowEvent]) -> list[Message]: async def run_agent_framework_example(initial_task: str, scripted_responses: Sequence[str]) -> str: - client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = FoundryChatClient(credential=AzureCliCredential()) triage, refund, status, returns = _create_af_agents(client) workflow = ( diff --git a/python/samples/semantic-kernel-migration/orchestrations/magentic.py b/python/samples/semantic-kernel-migration/orchestrations/magentic.py index 5566df1ab1..2594d15f89 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/magentic.py +++ b/python/samples/semantic-kernel-migration/orchestrations/magentic.py @@ -137,7 +137,7 @@ async def run_agent_framework_example(prompt: str) -> str | None: instructions=( "You are a Researcher. You find information without additional computation or quantitative analysis." ), - client=OpenAIChatClient(model_id="gpt-4o-search-preview"), + client=OpenAIChatClient(model="gpt-4o-search-preview"), ) # Create code interpreter tool using static method diff --git a/python/samples/semantic-kernel-migration/orchestrations/sequential.py b/python/samples/semantic-kernel-migration/orchestrations/sequential.py index af3cf973aa..633c854986 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/sequential.py +++ b/python/samples/semantic-kernel-migration/orchestrations/sequential.py @@ -15,7 +15,7 @@ from collections.abc import Sequence from typing import cast -from agent_framework import Message +from agent_framework import Agent, Message from agent_framework.azure import AzureOpenAIChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential @@ -77,12 +77,12 @@ async def sk_agent_response_callback( async def run_agent_framework_example(prompt: str) -> list[Message]: client = AzureOpenAIChatClient(credential=AzureCliCredential()) - writer = client.as_agent( + writer = Agent(client=client, instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), name="writer", ) - reviewer = client.as_agent( + reviewer = Agent(client=client, instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), name="reviewer", ) diff --git a/python/uv.lock b/python/uv.lock index f55686893e..a8175fb812 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -47,6 +47,7 @@ members = [ "agent-framework-lab", "agent-framework-mem0", "agent-framework-ollama", + "agent-framework-openai", "agent-framework-orchestrations", "agent-framework-purview", "agent-framework-redis", @@ -634,6 +635,23 @@ requires-dist = [ { name = "ollama", specifier = ">=0.5.3,<0.5.4" }, ] +[[package]] +name = "agent-framework-openai" +version = "1.0.0rc5" +source = { editable = "packages/openai" } +dependencies = [ + { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-framework-core", editable = "packages/core" }, + { name = "openai", specifier = ">=1.99.0,<3" }, + { name = "packaging", specifier = ">=24.1,<25" }, +] + [[package]] name = "agent-framework-orchestrations" version = "1.0.0b260319" From b36867ec0a964676e80b6c0bb3b45298a8f48c4b Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 20 Mar 2026 17:34:14 +0100 Subject: [PATCH 02/13] =?UTF-8?q?fix:=20missing=20Agent=20imports=20in=20s?= =?UTF-8?q?amples,=20.model=5Fid=20=E2=86=92=20.model=20in=20foundry=5Floc?= =?UTF-8?q?al=20sample?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/samples/01-get-started/02_add_tools.py | 2 ++ python/samples/01-get-started/04_memory.py | 2 ++ .../01-get-started/05_first_workflow.py | 2 ++ .../redis/azure_redis_conversation.py | 3 ++- .../context_providers/redis/redis_basics.py | 6 +++-- .../redis/redis_conversation.py | 3 ++- .../context_providers/redis/redis_sessions.py | 12 ++++++---- .../conversations/suspend_resume_session.py | 6 +++-- .../02-agents/devui/foundry_agent/__init__.py | 1 + .../02-agents/devui/spam_workflow/__init__.py | 1 + .../devui/weather_agent_azure/__init__.py | 1 + .../devui/workflow_agents/__init__.py | 1 + .../devui/workflow_agents/workflow.py | 15 ++++++++---- .../middleware/runtime_context_delegation.py | 18 +++++++++----- .../middleware/usage_tracking_middleware.py | 3 +-- .../providers/anthropic/anthropic_advanced.py | 3 ++- .../providers/anthropic/anthropic_foundry.py | 3 ++- .../providers/azure/foundry_chat_client.py | 6 +++-- .../foundry_chat_client_with_local_mcp.py | 3 ++- .../foundry_local/foundry_local_agent.py | 5 ++-- ...enai_chat_client_with_explicit_settings.py | 3 ++- ...penai_responses_client_image_generation.py | 3 ++- ...nai_responses_client_with_agent_as_tool.py | 6 +++-- ...responses_client_with_explicit_settings.py | 3 ++- .../skills/unit-converter/scripts/convert.py | 4 ++++ .../skills/unit-converter/scripts/convert.py | 4 ++++ .../skills/subprocess_script_runner.py | 1 + .../agent_as_tool_with_session_propagation.py | 6 +++-- ...ool_from_dict_with_dependency_injection.py | 4 +++- python/samples/02-agents/typed_options.py | 1 + .../_start-here/step2_agents_in_a_workflow.py | 6 +++-- .../_start-here/step3_streaming.py | 6 +++-- .../agents/azure_ai_agents_streaming.py | 6 +++-- .../agents/azure_chat_agents_and_executor.py | 16 ++++++------- .../agents/azure_chat_agents_streaming.py | 2 +- ...re_chat_agents_tool_calls_with_feedback.py | 18 +++++++------- .../agents/concurrent_workflow_as_agent.py | 9 ++++--- .../agents/custom_agent_executors.py | 16 ++++++------- .../agents/sequential_workflow_as_agent.py | 6 +++-- .../workflow_as_agent_human_in_the_loop.py | 7 +----- .../agents/workflow_as_agent_kwargs.py | 3 ++- .../workflow_as_agent_reflection_pattern.py | 7 +----- .../agents/workflow_as_agent_with_session.py | 9 ++++--- .../checkpoint_with_human_in_the_loop.py | 8 +++---- .../control-flow/edge_condition.py | 16 ++++++------- .../multi_selection_edge_group.py | 24 +++++++++---------- .../03-workflows/control-flow/simple_loop.py | 8 +++---- .../control-flow/switch_case_edge_group.py | 16 ++++++------- .../agent_to_function_tool/main.py | 3 ++- .../declarative/customer_support/main.py | 18 +++++++++----- .../customer_support/ticketing_plugin.py | 4 ++++ .../declarative/deep_research/main.py | 21 ++++++++++------ .../declarative/function_tools/main.py | 3 ++- .../declarative/marketing/main.py | 9 ++++--- .../declarative/simple_workflow/main.py | 1 + .../declarative/student_teacher/main.py | 6 +++-- .../human-in-the-loop/agents_with_HITL.py | 16 ++++++------- .../agents_with_approval_requests.py | 8 +++---- .../guessing_game_with_human_input.py | 8 +++---- .../orchestrations/concurrent_agents.py | 9 ++++--- .../concurrent_custom_aggregator.py | 9 ++++--- .../orchestrations/sequential_agents.py | 6 +++-- .../parallelism/fan_out_fan_in_edges.py | 24 +++++++++---------- .../state-management/state_with_agents.py | 16 ++++++------- .../state-management/workflow_kwargs.py | 3 ++- .../concurrent_with_visualization.py | 24 +++++++++---------- .../04-hosting/a2a/agent_definitions.py | 9 ++++--- .../01_single_agent/function_app.py | 3 ++- .../02_multi_agent/function_app.py | 6 +++-- .../03_reliable_streaming/function_app.py | 5 ++-- .../function_app.py | 3 ++- .../function_app.py | 6 +++-- .../function_app.py | 6 +++-- .../function_app.py | 3 ++- .../08_mcp_server/function_app.py | 9 ++++--- .../durabletask/01_single_agent/sample.py | 1 + .../durabletask/01_single_agent/worker.py | 3 ++- .../durabletask/02_multi_agent/sample.py | 1 + .../durabletask/02_multi_agent/worker.py | 6 +++-- .../03_single_agent_streaming/sample.py | 1 + .../03_single_agent_streaming/worker.py | 3 ++- .../sample.py | 1 + .../worker.py | 3 ++- .../sample.py | 1 + .../worker.py | 6 +++-- .../worker.py | 6 +++-- .../sample.py | 1 + .../worker.py | 3 ++- .../agent_with_local_tools/main.py | 21 +++++----------- .../main.py | 16 ++++++------- .../m365-agent/m365_agent_demo/app.py | 4 ++-- .../01_round_robin_group_chat.py | 2 +- 92 files changed, 373 insertions(+), 258 deletions(-) diff --git a/python/samples/01-get-started/02_add_tools.py b/python/samples/01-get-started/02_add_tools.py index 8d576bd32b..0ff9b8ae2b 100644 --- a/python/samples/01-get-started/02_add_tools.py +++ b/python/samples/01-get-started/02_add_tools.py @@ -27,6 +27,8 @@ def get_weather( """Get the weather for a given location.""" conditions = ["sunny", "cloudy", "rainy", "stormy"] return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." + + # diff --git a/python/samples/01-get-started/04_memory.py b/python/samples/01-get-started/04_memory.py index 82bf484a45..007aaf8a97 100644 --- a/python/samples/01-get-started/04_memory.py +++ b/python/samples/01-get-started/04_memory.py @@ -59,6 +59,8 @@ async def after_run( text = msg.text if hasattr(msg, "text") else "" if isinstance(text, str) and "my name is" in text.lower(): state["user_name"] = text.lower().split("my name is")[-1].strip().split()[0].capitalize() + + # diff --git a/python/samples/01-get-started/05_first_workflow.py b/python/samples/01-get-started/05_first_workflow.py index 89b4f608b2..74720e529f 100644 --- a/python/samples/01-get-started/05_first_workflow.py +++ b/python/samples/01-get-started/05_first_workflow.py @@ -45,6 +45,8 @@ def create_workflow(): """Build the workflow: UpperCase → reverse_text.""" upper = UpperCase(id="upper_case") return WorkflowBuilder(start_executor=upper).add_edge(upper, reverse_text).build() + + # diff --git a/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py b/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py index dff63a2ac0..007dc8ce14 100644 --- a/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py +++ b/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py @@ -89,7 +89,8 @@ async def main() -> None: ) # 4. Create agent with Azure Redis history provider - agent = Agent(client=client, + agent = Agent( + client=client, name="AzureRedisAssistant", instructions="You are a helpful assistant.", context_providers=[history_provider], diff --git a/python/samples/02-agents/context_providers/redis/redis_basics.py b/python/samples/02-agents/context_providers/redis/redis_basics.py index 85e4e5791b..1dd523da1e 100644 --- a/python/samples/02-agents/context_providers/redis/redis_basics.py +++ b/python/samples/02-agents/context_providers/redis/redis_basics.py @@ -206,7 +206,8 @@ async def main() -> None: client = create_chat_client() # Create agent wired to the Redis context provider. The provider automatically # persists conversational details and surfaces relevant context on each turn. - agent = Agent(client=client, + agent = Agent( + client=client, name="MemoryEnhancedAssistant", instructions=( "You are a helpful assistant. Personalize replies using provided context. " @@ -249,7 +250,8 @@ async def main() -> None: # Create agent exposing the flight search tool. Tool outputs are captured by the # provider and become retrievable context for later turns. client = create_chat_client() - agent = Agent(client=client, + agent = Agent( + client=client, name="MemoryEnhancedAssistant", instructions=( "You are a helpful assistant. Personalize replies using provided context. " diff --git a/python/samples/02-agents/context_providers/redis/redis_conversation.py b/python/samples/02-agents/context_providers/redis/redis_conversation.py index 2e8000cd95..f7edeeb4ba 100644 --- a/python/samples/02-agents/context_providers/redis/redis_conversation.py +++ b/python/samples/02-agents/context_providers/redis/redis_conversation.py @@ -72,7 +72,8 @@ async def main() -> None: ) # Create agent wired to the Redis context provider. The provider automatically # persists conversational details and surfaces relevant context on each turn. - agent = Agent(client=client, + agent = Agent( + client=client, name="MemoryEnhancedAssistant", instructions=( "You are a helpful assistant. Personalize replies using provided context. " diff --git a/python/samples/02-agents/context_providers/redis/redis_sessions.py b/python/samples/02-agents/context_providers/redis/redis_sessions.py index 1b881a1f24..39113b1be8 100644 --- a/python/samples/02-agents/context_providers/redis/redis_sessions.py +++ b/python/samples/02-agents/context_providers/redis/redis_sessions.py @@ -72,7 +72,8 @@ async def example_global_thread_scope() -> None: user_id="threads_demo_user", ) - agent = Agent(client=client, + agent = Agent( + client=client, name="GlobalMemoryAssistant", instructions=( "You are a helpful assistant. Personalize replies using provided context. " @@ -129,7 +130,8 @@ async def example_per_operation_thread_scope() -> None: vector_distance_metric="cosine", ) - agent = Agent(client=client, + agent = Agent( + client=client, name="ScopedMemoryAssistant", instructions="You are an assistant with thread-scoped memory.", context_providers=[provider], @@ -192,7 +194,8 @@ async def example_multiple_agents() -> None: vector_distance_metric="cosine", ) - personal_agent = Agent(client=client, + personal_agent = Agent( + client=client, name="PersonalAssistant", instructions="You are a personal assistant that helps with personal tasks.", context_providers=[personal_provider], @@ -211,7 +214,8 @@ async def example_multiple_agents() -> None: vector_distance_metric="cosine", ) - work_agent = Agent(client=client, + work_agent = Agent( + client=client, name="WorkAssistant", instructions="You are a work assistant that helps with professional tasks.", context_providers=[work_provider], diff --git a/python/samples/02-agents/conversations/suspend_resume_session.py b/python/samples/02-agents/conversations/suspend_resume_session.py index b391ca3671..6d0e4e796e 100644 --- a/python/samples/02-agents/conversations/suspend_resume_session.py +++ b/python/samples/02-agents/conversations/suspend_resume_session.py @@ -28,7 +28,8 @@ async def suspend_resume_service_managed_session() -> None: AzureCliCredential() as credential, Agent( client=FoundryChatClient(credential=credential), - name="MemoryBot", instructions="You are a helpful assistant that remembers our conversation." + name="MemoryBot", + instructions="You are a helpful assistant that remembers our conversation.", ) as agent, ): # Start a new session for the agent conversation. @@ -62,7 +63,8 @@ async def suspend_resume_in_memory_session() -> None: # other chat clients can be used as well. agent = Agent( client=FoundryChatClient(), - name="MemoryBot", instructions="You are a helpful assistant that remembers our conversation." + name="MemoryBot", + instructions="You are a helpful assistant that remembers our conversation.", ) # Start a new session for the agent conversation. diff --git a/python/samples/02-agents/devui/foundry_agent/__init__.py b/python/samples/02-agents/devui/foundry_agent/__init__.py index bf77e4ff2a..0ecbfc3802 100644 --- a/python/samples/02-agents/devui/foundry_agent/__init__.py +++ b/python/samples/02-agents/devui/foundry_agent/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. """Weather agent sample for DevUI testing.""" + from .agent import agent __all__ = ["agent"] diff --git a/python/samples/02-agents/devui/spam_workflow/__init__.py b/python/samples/02-agents/devui/spam_workflow/__init__.py index 1903f792bb..9801f7433a 100644 --- a/python/samples/02-agents/devui/spam_workflow/__init__.py +++ b/python/samples/02-agents/devui/spam_workflow/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. """Spam detection workflow sample for DevUI testing.""" + from .workflow import workflow __all__ = ["workflow"] diff --git a/python/samples/02-agents/devui/weather_agent_azure/__init__.py b/python/samples/02-agents/devui/weather_agent_azure/__init__.py index bf77e4ff2a..0ecbfc3802 100644 --- a/python/samples/02-agents/devui/weather_agent_azure/__init__.py +++ b/python/samples/02-agents/devui/weather_agent_azure/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. """Weather agent sample for DevUI testing.""" + from .agent import agent __all__ = ["agent"] diff --git a/python/samples/02-agents/devui/workflow_agents/__init__.py b/python/samples/02-agents/devui/workflow_agents/__init__.py index 56b84d36cb..67fc70ac2f 100644 --- a/python/samples/02-agents/devui/workflow_agents/__init__.py +++ b/python/samples/02-agents/devui/workflow_agents/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. """Sequential Agents Workflow - Writer → Reviewer.""" + from .workflow import workflow __all__ = ["workflow"] diff --git a/python/samples/02-agents/devui/workflow_agents/workflow.py b/python/samples/02-agents/devui/workflow_agents/workflow.py index 68638d64ad..0fb705b68b 100644 --- a/python/samples/02-agents/devui/workflow_agents/workflow.py +++ b/python/samples/02-agents/devui/workflow_agents/workflow.py @@ -66,7 +66,8 @@ def is_approved(message: Any) -> bool: client = FoundryChatClient(api_key=os.environ.get("AZURE_OPENAI_API_KEY", "")) # Create Writer agent - generates content -writer = Agent(client=client, +writer = Agent( + client=client, name="Writer", instructions=( "You are an excellent content writer. " @@ -76,7 +77,8 @@ def is_approved(message: Any) -> bool: ) # Create Reviewer agent - evaluates and provides structured feedback -reviewer = Agent(client=client, +reviewer = Agent( + client=client, name="Reviewer", instructions=( "You are an expert content reviewer. " @@ -94,7 +96,8 @@ def is_approved(message: Any) -> bool: ) # Create Editor agent - improves content based on feedback -editor = Agent(client=client, +editor = Agent( + client=client, name="Editor", instructions=( "You are a skilled editor. " @@ -105,7 +108,8 @@ def is_approved(message: Any) -> bool: ) # Create Publisher agent - formats content for publication -publisher = Agent(client=client, +publisher = Agent( + client=client, name="Publisher", instructions=( "You are a publishing agent. " @@ -115,7 +119,8 @@ def is_approved(message: Any) -> bool: ) # Create Summarizer agent - creates final publication report -summarizer = Agent(client=client, +summarizer = Agent( + client=client, name="Summarizer", instructions=( "You are a summarizer agent. " diff --git a/python/samples/02-agents/middleware/runtime_context_delegation.py b/python/samples/02-agents/middleware/runtime_context_delegation.py index e241dd0511..5716b3ec0f 100644 --- a/python/samples/02-agents/middleware/runtime_context_delegation.py +++ b/python/samples/02-agents/middleware/runtime_context_delegation.py @@ -152,7 +152,8 @@ async def pattern_1_single_agent_with_closure() -> None: client = FoundryChatClient(model="gpt-4o-mini") # Create agent with both tools and shared context via middleware - communication_agent = Agent(client=client, + communication_agent = Agent( + client=client, name="communication_agent", instructions=( "You are a communication assistant that can send emails and notifications. " @@ -297,14 +298,16 @@ async def sms_kwargs_tracker(context: FunctionInvocationContext, call_next: Call client = FoundryChatClient(model="gpt-4o-mini") # Create specialized sub-agents - email_agent = Agent(client=client, + email_agent = Agent( + client=client, name="email_agent", instructions="You send emails using the send_email_v2 tool.", tools=[send_email_v2], middleware=[email_kwargs_tracker], ) - sms_agent = Agent(client=client, + sms_agent = Agent( + client=client, name="sms_agent", instructions="You send SMS messages using the send_sms tool.", tools=[send_sms], @@ -312,7 +315,8 @@ async def sms_kwargs_tracker(context: FunctionInvocationContext, call_next: Call ) # Create coordinator that delegates to sub-agents - coordinator = Agent(client=client, + coordinator = Agent( + client=client, name="coordinator", instructions=( "You coordinate communication tasks. " @@ -399,7 +403,8 @@ async def pattern_3_hierarchical_with_middleware() -> None: client = FoundryChatClient(model="gpt-4o-mini") # Sub-agent with validation middleware - protected_agent = Agent(client=client, + protected_agent = Agent( + client=client, name="protected_agent", instructions="You perform protected operations that require authentication.", tools=[protected_operation], @@ -407,7 +412,8 @@ async def pattern_3_hierarchical_with_middleware() -> None: ) # Coordinator delegates to protected agent - coordinator = Agent(client=client, + coordinator = Agent( + client=client, name="coordinator", instructions="You coordinate protected operations. Delegate to protected_executor.", tools=[ diff --git a/python/samples/02-agents/middleware/usage_tracking_middleware.py b/python/samples/02-agents/middleware/usage_tracking_middleware.py index 877d2a8a82..bffa01f7d8 100644 --- a/python/samples/02-agents/middleware/usage_tracking_middleware.py +++ b/python/samples/02-agents/middleware/usage_tracking_middleware.py @@ -50,8 +50,7 @@ def _reset_usage_counters() -> None: STREAMING_CALL_COUNT = 0 -def _create_agent( -) -> Agent: +def _create_agent() -> Agent: """Create the shared agent used by both demonstrations.""" return Agent( client=OpenAIResponsesClient(), diff --git a/python/samples/02-agents/providers/anthropic/anthropic_advanced.py b/python/samples/02-agents/providers/anthropic/anthropic_advanced.py index 1bdadd5de9..b9647f104e 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_advanced.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_advanced.py @@ -31,7 +31,8 @@ async def main() -> None: # Create web search tool configuration using instance method web_search_tool = client.get_web_search_tool() - agent = Agent(client=client, + agent = Agent( + client=client, name="DocsAgent", instructions="You are a helpful agent for both Microsoft docs questions and general questions.", tools=[mcp_tool, web_search_tool], diff --git a/python/samples/02-agents/providers/anthropic/anthropic_foundry.py b/python/samples/02-agents/providers/anthropic/anthropic_foundry.py index b07766de98..fdbe59d90f 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_foundry.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_foundry.py @@ -42,7 +42,8 @@ async def main() -> None: # Create web search tool configuration using instance method web_search_tool = client.get_web_search_tool() - agent = Agent(client=client, + agent = Agent( + client=client, name="DocsAgent", instructions="You are a helpful agent for both Microsoft docs questions and general questions.", tools=[mcp_tool, web_search_tool], diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client.py b/python/samples/02-agents/providers/azure/foundry_chat_client.py index dc307e825c..bd21725845 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client.py @@ -52,7 +52,8 @@ async def non_streaming_example() -> None: model=os.environ["FOUNDRY_MODEL"], credential=credential, ) - agent = Agent(client=_client, + agent = Agent( + client=_client, instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -77,7 +78,8 @@ async def streaming_example() -> None: model=os.environ["FOUNDRY_MODEL"], credential=credential, ) - agent = Agent(client=_client, + agent = Agent( + client=_client, instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py index 34ab6f19b2..1c3843e432 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py @@ -41,7 +41,8 @@ async def main(): credential=credential, ) - agent: Agent = Agent(client=responses_client, + agent: Agent = Agent( + client=responses_client, name="DocsAgent", instructions=("You are a helpful assistant that can help with Microsoft documentation questions."), ) diff --git a/python/samples/02-agents/providers/foundry_local/foundry_local_agent.py b/python/samples/02-agents/providers/foundry_local/foundry_local_agent.py index e065967894..0d9092e248 100644 --- a/python/samples/02-agents/providers/foundry_local/foundry_local_agent.py +++ b/python/samples/02-agents/providers/foundry_local/foundry_local_agent.py @@ -60,14 +60,15 @@ async def main() -> None: print("=== Basic Foundry Local Client Agent Example ===") client = FoundryLocalClient(model="phi-4-mini") - print(f"Client Model ID: {client.model_id}\n") + print(f"Client Model ID: {client.model}\n") print("Other available models (tool calling supported only):") for model in client.manager.list_catalog_models(): if model.supports_tool_calling: print( f"- {model.alias} for {model.task} - id={model.id} - {(model.file_size_mb / 1000):.2f} GB - {model.license}" ) - agent = Agent(client=client, + agent = Agent( + client=client, name="LocalAgent", instructions="You are a helpful agent.", tools=get_weather, diff --git a/python/samples/02-agents/providers/openai/openai_chat_client_with_explicit_settings.py b/python/samples/02-agents/providers/openai/openai_chat_client_with_explicit_settings.py index af439f7a05..6d4984c66e 100644 --- a/python/samples/02-agents/providers/openai/openai_chat_client_with_explicit_settings.py +++ b/python/samples/02-agents/providers/openai/openai_chat_client_with_explicit_settings.py @@ -41,7 +41,8 @@ async def main() -> None: api_key=os.environ["OPENAI_API_KEY"], ) - agent = Agent(client=_client, + agent = Agent( + client=_client, instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py b/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py index abb6a24b29..6e01a4dbbd 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py @@ -61,7 +61,8 @@ async def main() -> None: # Create an agent with customized image generation options client = OpenAIResponsesClient() - agent = Agent(client=client, + agent = Agent( + client=client, instructions="You are a helpful AI that can generate images.", tools=[ client.get_image_generation_tool( diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_with_agent_as_tool.py b/python/samples/02-agents/providers/openai/openai_responses_client_with_agent_as_tool.py index aa581058be..567c7fcaef 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_with_agent_as_tool.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_with_agent_as_tool.py @@ -40,7 +40,8 @@ async def main() -> None: client = OpenAIResponsesClient() # Create a specialized writer agent - writer = Agent(client=client, + writer = Agent( + client=client, name="WriterAgent", instructions="You are a creative writer. Write short, engaging content.", ) @@ -54,7 +55,8 @@ async def main() -> None: ) # Create coordinator agent with writer as a tool - coordinator = Agent(client=client, + coordinator = Agent( + client=client, name="CoordinatorAgent", instructions="You coordinate with specialized agents. Delegate writing tasks to the creative_writer tool.", tools=[writer_tool], diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_with_explicit_settings.py b/python/samples/02-agents/providers/openai/openai_responses_client_with_explicit_settings.py index 432ed7a1aa..20a3f720d1 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_with_explicit_settings.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_with_explicit_settings.py @@ -41,7 +41,8 @@ async def main() -> None: api_key=os.environ["OPENAI_API_KEY"], ) - agent = Agent(client=_client, + agent = Agent( + client=_client, instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/02-agents/skills/file_based_skill/skills/unit-converter/scripts/convert.py b/python/samples/02-agents/skills/file_based_skill/skills/unit-converter/scripts/convert.py index 9629d22635..0d1d618eae 100644 --- a/python/samples/02-agents/skills/file_based_skill/skills/unit-converter/scripts/convert.py +++ b/python/samples/02-agents/skills/file_based_skill/skills/unit-converter/scripts/convert.py @@ -7,6 +7,8 @@ import argparse import json + + def main() -> None: parser = argparse.ArgumentParser( description="Convert a value using a multiplication factor.", @@ -20,5 +22,7 @@ def main() -> None: args = parser.parse_args() result = round(args.value * args.factor, 4) print(json.dumps({"value": args.value, "factor": args.factor, "result": result})) + + if __name__ == "__main__": main() diff --git a/python/samples/02-agents/skills/mixed_skills/skills/unit-converter/scripts/convert.py b/python/samples/02-agents/skills/mixed_skills/skills/unit-converter/scripts/convert.py index 9629d22635..0d1d618eae 100644 --- a/python/samples/02-agents/skills/mixed_skills/skills/unit-converter/scripts/convert.py +++ b/python/samples/02-agents/skills/mixed_skills/skills/unit-converter/scripts/convert.py @@ -7,6 +7,8 @@ import argparse import json + + def main() -> None: parser = argparse.ArgumentParser( description="Convert a value using a multiplication factor.", @@ -20,5 +22,7 @@ def main() -> None: args = parser.parse_args() result = round(args.value * args.factor, 4) print(json.dumps({"value": args.value, "factor": args.factor, "result": result})) + + if __name__ == "__main__": main() diff --git a/python/samples/02-agents/skills/subprocess_script_runner.py b/python/samples/02-agents/skills/subprocess_script_runner.py index a49b224d1c..e24724a0e0 100644 --- a/python/samples/02-agents/skills/subprocess_script_runner.py +++ b/python/samples/02-agents/skills/subprocess_script_runner.py @@ -4,6 +4,7 @@ Executes file-based skill scripts as local Python subprocesses. This is provided for demonstration purposes only. """ + from __future__ import annotations import subprocess diff --git a/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py b/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py index cbcac5952f..211c3b5681 100644 --- a/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py +++ b/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py @@ -65,7 +65,8 @@ async def main() -> None: client = OpenAIResponsesClient() - research_agent = Agent(client=client, + research_agent = Agent( + client=client, name="ResearchAgent", instructions="You are a research assistant. Provide concise answers and store your findings.", middleware=[log_session], @@ -80,7 +81,8 @@ async def main() -> None: propagate_session=True, ) - coordinator = Agent(client=client, + coordinator = Agent( + client=client, name="CoordinatorAgent", instructions=( "You coordinate research. Use the 'research' tool to start research " diff --git a/python/samples/02-agents/tools/function_tool_from_dict_with_dependency_injection.py b/python/samples/02-agents/tools/function_tool_from_dict_with_dependency_injection.py index df80c1b1e9..7f6b8896f0 100644 --- a/python/samples/02-agents/tools/function_tool_from_dict_with_dependency_injection.py +++ b/python/samples/02-agents/tools/function_tool_from_dict_with_dependency_injection.py @@ -63,7 +63,9 @@ def func(a, b) -> int: agent = Agent( client=OpenAIResponsesClient(), - name="FunctionToolAgent", instructions="You are a helpful assistant.", tools=tool + name="FunctionToolAgent", + instructions="You are a helpful assistant.", + tools=tool, ) response = await agent.run("What is 5 + 3?") print(f"Response: {response.text}") diff --git a/python/samples/02-agents/typed_options.py b/python/samples/02-agents/typed_options.py index d97fcff393..70a014bec5 100644 --- a/python/samples/02-agents/typed_options.py +++ b/python/samples/02-agents/typed_options.py @@ -5,6 +5,7 @@ from agent_framework import Agent from agent_framework.anthropic import AnthropicClient +from agent_framework.azure import FoundryChatClient from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions from dotenv import load_dotenv diff --git a/python/samples/03-workflows/_start-here/step2_agents_in_a_workflow.py b/python/samples/03-workflows/_start-here/step2_agents_in_a_workflow.py index 46f56eb9f5..9b8ffae46e 100644 --- a/python/samples/03-workflows/_start-here/step2_agents_in_a_workflow.py +++ b/python/samples/03-workflows/_start-here/step2_agents_in_a_workflow.py @@ -38,14 +38,16 @@ async def main(): model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - writer_agent = Agent(client=client, + writer_agent = Agent( + client=client, instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), name="writer", ) - reviewer_agent = Agent(client=client, + reviewer_agent = Agent( + client=client, instructions=( "You are an excellent content reviewer." "Provide actionable feedback to the writer about the provided content." diff --git a/python/samples/03-workflows/_start-here/step3_streaming.py b/python/samples/03-workflows/_start-here/step3_streaming.py index 238c2d3041..78d052db29 100644 --- a/python/samples/03-workflows/_start-here/step3_streaming.py +++ b/python/samples/03-workflows/_start-here/step3_streaming.py @@ -37,14 +37,16 @@ async def main(): model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - writer_agent = Agent(client=client, + writer_agent = Agent( + client=client, instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), name="writer", ) - reviewer_agent = Agent(client=client, + reviewer_agent = Agent( + client=client, instructions=( "You are an excellent content reviewer." "Provide actionable feedback to the writer about the provided content." diff --git a/python/samples/03-workflows/agents/azure_ai_agents_streaming.py b/python/samples/03-workflows/agents/azure_ai_agents_streaming.py index 2750d8edae..0ecddd87b0 100644 --- a/python/samples/03-workflows/agents/azure_ai_agents_streaming.py +++ b/python/samples/03-workflows/agents/azure_ai_agents_streaming.py @@ -32,14 +32,16 @@ async def main() -> None: ) # Create two agents: a Writer and a Reviewer. - writer_agent = Agent(client=client, + writer_agent = Agent( + client=client, name="Writer", instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), ) - reviewer_agent = Agent(client=client, + reviewer_agent = Agent( + client=client, name="Reviewer", instructions=( "You are an excellent content reviewer. " diff --git a/python/samples/03-workflows/agents/azure_chat_agents_and_executor.py b/python/samples/03-workflows/agents/azure_chat_agents_and_executor.py index 7632543586..b14e5ad1d0 100644 --- a/python/samples/03-workflows/agents/azure_chat_agents_and_executor.py +++ b/python/samples/03-workflows/agents/azure_chat_agents_and_executor.py @@ -103,10 +103,10 @@ async def main() -> None: # Create the agents research_agent = Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), name="research_agent", instructions=( "Produce a short, bullet-style briefing with two actionable ideas. Label the section as 'Initial Draft'." @@ -115,10 +115,10 @@ async def main() -> None: final_editor_agent = Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), name="final_editor_agent", instructions=( "Use all conversation context (including external notes) to produce the final answer. " diff --git a/python/samples/03-workflows/agents/azure_chat_agents_streaming.py b/python/samples/03-workflows/agents/azure_chat_agents_streaming.py index 40b6b7ba1c..4122369383 100644 --- a/python/samples/03-workflows/agents/azure_chat_agents_streaming.py +++ b/python/samples/03-workflows/agents/azure_chat_agents_streaming.py @@ -3,7 +3,7 @@ import asyncio import os -from agent_framework import AgentResponseUpdate, WorkflowBuilder +from agent_framework import Agent, AgentResponseUpdate, WorkflowBuilder from agent_framework.azure import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv diff --git a/python/samples/03-workflows/agents/azure_chat_agents_tool_calls_with_feedback.py b/python/samples/03-workflows/agents/azure_chat_agents_tool_calls_with_feedback.py index ff50a323fd..2f72350935 100644 --- a/python/samples/03-workflows/agents/azure_chat_agents_tool_calls_with_feedback.py +++ b/python/samples/03-workflows/agents/azure_chat_agents_tool_calls_with_feedback.py @@ -177,12 +177,10 @@ def create_writer_agent() -> Agent: """Creates a writer agent with tools.""" return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - - - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), name="writer_agent", instructions=( "You are a marketing writer. Call the available tools before drafting copy so you are precise. " @@ -198,10 +196,10 @@ def create_final_editor_agent() -> Agent: """Creates a final editor agent.""" return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), name="final_editor_agent", instructions=( "You are an editor who polishes marketing copy after human approval. " diff --git a/python/samples/03-workflows/agents/concurrent_workflow_as_agent.py b/python/samples/03-workflows/agents/concurrent_workflow_as_agent.py index 37956c1fe0..a4a3be6525 100644 --- a/python/samples/03-workflows/agents/concurrent_workflow_as_agent.py +++ b/python/samples/03-workflows/agents/concurrent_workflow_as_agent.py @@ -38,7 +38,8 @@ async def main() -> None: credential=AzureCliCredential(), ) - researcher = Agent(client=client, + researcher = Agent( + client=client, instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -46,7 +47,8 @@ async def main() -> None: name="researcher", ) - marketer = Agent(client=client, + marketer = Agent( + client=client, instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -54,7 +56,8 @@ async def main() -> None: name="marketer", ) - legal = Agent(client=client, + legal = Agent( + client=client, instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." diff --git a/python/samples/03-workflows/agents/custom_agent_executors.py b/python/samples/03-workflows/agents/custom_agent_executors.py index daa713b642..317fa206e9 100644 --- a/python/samples/03-workflows/agents/custom_agent_executors.py +++ b/python/samples/03-workflows/agents/custom_agent_executors.py @@ -53,10 +53,10 @@ def __init__(self, id: str = "writer"): # Create a domain specific agent using your configured FoundryChatClient. self.agent = Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), @@ -100,10 +100,10 @@ def __init__(self, id: str = "reviewer"): # Create a domain specific agent that evaluates and refines content. self.agent = Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You are an excellent content reviewer. You review the content and provide feedback to the writer." ), diff --git a/python/samples/03-workflows/agents/sequential_workflow_as_agent.py b/python/samples/03-workflows/agents/sequential_workflow_as_agent.py index 4123f0cff6..8a743b5dca 100644 --- a/python/samples/03-workflows/agents/sequential_workflow_as_agent.py +++ b/python/samples/03-workflows/agents/sequential_workflow_as_agent.py @@ -39,12 +39,14 @@ async def main() -> None: credential=AzureCliCredential(), ) - writer = Agent(client=client, + writer = Agent( + client=client, instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), name="writer", ) - reviewer = Agent(client=client, + reviewer = Agent( + client=client, instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), name="reviewer", ) diff --git a/python/samples/03-workflows/agents/workflow_as_agent_human_in_the_loop.py b/python/samples/03-workflows/agents/workflow_as_agent_human_in_the_loop.py index 744d09769b..170c238e2f 100644 --- a/python/samples/03-workflows/agents/workflow_as_agent_human_in_the_loop.py +++ b/python/samples/03-workflows/agents/workflow_as_agent_human_in_the_loop.py @@ -120,12 +120,7 @@ async def main() -> None: reviewer = ReviewerWithHumanInTheLoop(worker_id="worker") agent = Agent( - client=( - WorkflowBuilder(start_executor=worker) - .add_edge(worker, reviewer) - .add_edge(reviewer, worker) - .build() - ), + client=(WorkflowBuilder(start_executor=worker).add_edge(worker, reviewer).add_edge(reviewer, worker).build()), ) print("Running workflow agent with user query...") diff --git a/python/samples/03-workflows/agents/workflow_as_agent_kwargs.py b/python/samples/03-workflows/agents/workflow_as_agent_kwargs.py index 12c424d4c4..5dfbcb3e94 100644 --- a/python/samples/03-workflows/agents/workflow_as_agent_kwargs.py +++ b/python/samples/03-workflows/agents/workflow_as_agent_kwargs.py @@ -94,7 +94,8 @@ async def main() -> None: ) # Create agent with tools that use kwargs - agent = Agent(client=client, + agent = Agent( + client=client, name="assistant", instructions=( "You are a helpful assistant. Use the available tools to help users. " diff --git a/python/samples/03-workflows/agents/workflow_as_agent_reflection_pattern.py b/python/samples/03-workflows/agents/workflow_as_agent_reflection_pattern.py index e80b4ee303..052ecd57a7 100644 --- a/python/samples/03-workflows/agents/workflow_as_agent_reflection_pattern.py +++ b/python/samples/03-workflows/agents/workflow_as_agent_reflection_pattern.py @@ -212,12 +212,7 @@ async def main() -> None: ) agent = Agent( - client=( - WorkflowBuilder(start_executor=worker) - .add_edge(worker, reviewer) - .add_edge(reviewer, worker) - .build() - ), + client=(WorkflowBuilder(start_executor=worker).add_edge(worker, reviewer).add_edge(reviewer, worker).build()), ) print("Running workflow agent with user query...") diff --git a/python/samples/03-workflows/agents/workflow_as_agent_with_session.py b/python/samples/03-workflows/agents/workflow_as_agent_with_session.py index 3b65cd94d7..e54ef7ed7f 100644 --- a/python/samples/03-workflows/agents/workflow_as_agent_with_session.py +++ b/python/samples/03-workflows/agents/workflow_as_agent_with_session.py @@ -50,7 +50,8 @@ async def main() -> None: credential=AzureCliCredential(), ) - assistant = Agent(client=client, + assistant = Agent( + client=client, name="assistant", instructions=( "You are a helpful assistant. Answer questions based on the conversation " @@ -58,7 +59,8 @@ async def main() -> None: ), ) - summarizer = Agent(client=client, + summarizer = Agent( + client=client, name="summarizer", instructions=( "You are a summarizer. After the assistant responds, provide a brief " @@ -135,7 +137,8 @@ async def demonstrate_session_serialization() -> None: credential=AzureCliCredential(), ) - memory_assistant = Agent(client=client, + memory_assistant = Agent( + client=client, name="memory_assistant", instructions="You are a helpful assistant with good memory. Remember details from our conversation.", ) diff --git a/python/samples/03-workflows/checkpoint/checkpoint_with_human_in_the_loop.py b/python/samples/03-workflows/checkpoint/checkpoint_with_human_in_the_loop.py index c39cb84c79..f848a5b7c3 100644 --- a/python/samples/03-workflows/checkpoint/checkpoint_with_human_in_the_loop.py +++ b/python/samples/03-workflows/checkpoint/checkpoint_with_human_in_the_loop.py @@ -181,10 +181,10 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow: # edges is often the quickest way to understand execution order. writer_agent = Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions="Write concise, warm release notes that sound human and helpful.", name="writer", ) diff --git a/python/samples/03-workflows/control-flow/edge_condition.py b/python/samples/03-workflows/control-flow/edge_condition.py index 4d1decd901..2f4ba2d265 100644 --- a/python/samples/03-workflows/control-flow/edge_condition.py +++ b/python/samples/03-workflows/control-flow/edge_condition.py @@ -138,10 +138,10 @@ def create_spam_detector_agent() -> Agent: # AzureCliCredential uses your current az login. This avoids embedding secrets in code. return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You are a spam detection assistant that identifies spam emails. " "Always return JSON with fields is_spam (bool), reason (string), and email_content (string). " @@ -157,10 +157,10 @@ def create_email_assistant_agent() -> Agent: # AzureCliCredential uses your current az login. This avoids embedding secrets in code. return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You are an email assistant that helps users draft professional responses to emails. " "Your input may be a JSON object that includes 'email_content'; base your reply on that content. " diff --git a/python/samples/03-workflows/control-flow/multi_selection_edge_group.py b/python/samples/03-workflows/control-flow/multi_selection_edge_group.py index 5cd700081b..e68f5065ae 100644 --- a/python/samples/03-workflows/control-flow/multi_selection_edge_group.py +++ b/python/samples/03-workflows/control-flow/multi_selection_edge_group.py @@ -190,10 +190,10 @@ def create_email_analysis_agent() -> Agent: """Creates the email analysis agent.""" return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You are a spam detection assistant that identifies spam emails. " "Always return JSON with fields 'spam_decision' (one of NotSpam, Spam, Uncertain) " @@ -208,10 +208,10 @@ def create_email_assistant_agent() -> Agent: """Creates the email assistant agent.""" return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=("You are an email assistant that helps users draft responses to emails with professionalism."), name="email_assistant_agent", default_options={"response_format": EmailResponse}, @@ -222,10 +222,10 @@ def create_email_summary_agent() -> Agent: """Creates the email summary agent.""" return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=("You are an assistant that helps users summarize emails."), name="email_summary_agent", default_options={"response_format": EmailSummaryModel}, diff --git a/python/samples/03-workflows/control-flow/simple_loop.py b/python/samples/03-workflows/control-flow/simple_loop.py index 4462abba13..1934f5befb 100644 --- a/python/samples/03-workflows/control-flow/simple_loop.py +++ b/python/samples/03-workflows/control-flow/simple_loop.py @@ -125,10 +125,10 @@ def create_judge_agent() -> Agent: """Create a judge agent that evaluates guesses.""" return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=("You strictly respond with one of: MATCHED, ABOVE, BELOW based on the given target and guess."), name="judge_agent", ) diff --git a/python/samples/03-workflows/control-flow/switch_case_edge_group.py b/python/samples/03-workflows/control-flow/switch_case_edge_group.py index e45bb05c92..70baebb1ee 100644 --- a/python/samples/03-workflows/control-flow/switch_case_edge_group.py +++ b/python/samples/03-workflows/control-flow/switch_case_edge_group.py @@ -161,10 +161,10 @@ def create_spam_detection_agent() -> Agent: """Create and return the spam detection agent.""" return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You are a spam detection assistant that identifies spam emails. " "Be less confident in your assessments. " @@ -180,10 +180,10 @@ def create_email_assistant_agent() -> Agent: """Create and return the email assistant agent.""" return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=("You are an email assistant that helps users draft responses to emails with professionalism."), name="email_assistant_agent", default_options={"response_format": EmailResponse}, diff --git a/python/samples/03-workflows/declarative/agent_to_function_tool/main.py b/python/samples/03-workflows/declarative/agent_to_function_tool/main.py index 76108af735..568cacefff 100644 --- a/python/samples/03-workflows/declarative/agent_to_function_tool/main.py +++ b/python/samples/03-workflows/declarative/agent_to_function_tool/main.py @@ -206,7 +206,8 @@ async def main(): ) # Create the order analysis agent with structured output - order_analysis_agent = Agent(client=chat_client, + order_analysis_agent = Agent( + client=chat_client, name="OrderAnalysisAgent", instructions=ORDER_ANALYSIS_INSTRUCTIONS, default_options={"response_format": OrderAnalysis}, diff --git a/python/samples/03-workflows/declarative/customer_support/main.py b/python/samples/03-workflows/declarative/customer_support/main.py index df75433aa4..4d54a5975b 100644 --- a/python/samples/03-workflows/declarative/customer_support/main.py +++ b/python/samples/03-workflows/declarative/customer_support/main.py @@ -178,40 +178,46 @@ async def main() -> None: ) # Create agents with structured outputs - self_service_agent = Agent(client=client, + self_service_agent = Agent( + client=client, name="SelfServiceAgent", instructions=SELF_SERVICE_INSTRUCTIONS, default_options={"response_format": SelfServiceResponse}, ) - ticketing_agent = Agent(client=client, + ticketing_agent = Agent( + client=client, name="TicketingAgent", instructions=TICKETING_INSTRUCTIONS, tools=plugin.get_functions(), default_options={"response_format": TicketingResponse}, ) - routing_agent = Agent(client=client, + routing_agent = Agent( + client=client, name="TicketRoutingAgent", instructions=TICKET_ROUTING_INSTRUCTIONS, tools=[plugin.get_ticket], default_options={"response_format": RoutingResponse}, ) - windows_support_agent = Agent(client=client, + windows_support_agent = Agent( + client=client, name="WindowsSupportAgent", instructions=WINDOWS_SUPPORT_INSTRUCTIONS, tools=[plugin.get_ticket], default_options={"response_format": SupportResponse}, ) - resolution_agent = Agent(client=client, + resolution_agent = Agent( + client=client, name="TicketResolutionAgent", instructions=RESOLUTION_INSTRUCTIONS, tools=[plugin.resolve_ticket], ) - escalation_agent = Agent(client=client, + escalation_agent = Agent( + client=client, name="TicketEscalationAgent", instructions=ESCALATION_INSTRUCTIONS, tools=[plugin.get_ticket, plugin.send_notification], diff --git a/python/samples/03-workflows/declarative/customer_support/ticketing_plugin.py b/python/samples/03-workflows/declarative/customer_support/ticketing_plugin.py index 5742ba0c4e..f25f1b473d 100644 --- a/python/samples/03-workflows/declarative/customer_support/ticketing_plugin.py +++ b/python/samples/03-workflows/declarative/customer_support/ticketing_plugin.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. """Ticketing plugin for CustomerSupport workflow.""" + import uuid from collections.abc import Callable from dataclasses import dataclass @@ -13,6 +14,7 @@ class TicketStatus(Enum): """Status of a support ticket.""" + OPEN = "open" IN_PROGRESS = "in_progress" RESOLVED = "resolved" @@ -22,6 +24,7 @@ class TicketStatus(Enum): @dataclass class TicketItem: """A support ticket.""" + id: str subject: str = "" description: str = "" @@ -31,6 +34,7 @@ class TicketItem: class TicketingPlugin: """Mock ticketing plugin for customer support workflow.""" + def __init__(self) -> None: self._ticket_store: dict[str, TicketItem] = {} diff --git a/python/samples/03-workflows/declarative/deep_research/main.py b/python/samples/03-workflows/declarative/deep_research/main.py index 7edd7e5df4..e2a5bf345d 100644 --- a/python/samples/03-workflows/declarative/deep_research/main.py +++ b/python/samples/03-workflows/declarative/deep_research/main.py @@ -132,38 +132,45 @@ async def main() -> None: ) # Create agents - research_agent = Agent(client=client, + research_agent = Agent( + client=client, name="ResearchAgent", instructions=RESEARCH_INSTRUCTIONS, ) - planner_agent = Agent(client=client, + planner_agent = Agent( + client=client, name="PlannerAgent", instructions=PLANNER_INSTRUCTIONS, ) - manager_agent = Agent(client=client, + manager_agent = Agent( + client=client, name="ManagerAgent", instructions=MANAGER_INSTRUCTIONS, default_options={"response_format": ManagerResponse}, ) - summary_agent = Agent(client=client, + summary_agent = Agent( + client=client, name="SummaryAgent", instructions=SUMMARY_INSTRUCTIONS, ) - knowledge_agent = Agent(client=client, + knowledge_agent = Agent( + client=client, name="KnowledgeAgent", instructions=KNOWLEDGE_INSTRUCTIONS, ) - coder_agent = Agent(client=client, + coder_agent = Agent( + client=client, name="CoderAgent", instructions=CODER_INSTRUCTIONS, ) - weather_agent = Agent(client=client, + weather_agent = Agent( + client=client, name="WeatherAgent", instructions=WEATHER_INSTRUCTIONS, ) diff --git a/python/samples/03-workflows/declarative/function_tools/main.py b/python/samples/03-workflows/declarative/function_tools/main.py index 898acfb493..c6e1c23c5e 100644 --- a/python/samples/03-workflows/declarative/function_tools/main.py +++ b/python/samples/03-workflows/declarative/function_tools/main.py @@ -74,7 +74,8 @@ async def main(): model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], credential=AzureCliCredential(), ) - menu_agent = Agent(client=client, + menu_agent = Agent( + client=client, name="MenuAgent", instructions="Answer questions about menu items, specials, and prices.", tools=[get_menu, get_specials, get_item_price], diff --git a/python/samples/03-workflows/declarative/marketing/main.py b/python/samples/03-workflows/declarative/marketing/main.py index 021d525cc9..6de42bd83f 100644 --- a/python/samples/03-workflows/declarative/marketing/main.py +++ b/python/samples/03-workflows/declarative/marketing/main.py @@ -61,15 +61,18 @@ async def main() -> None: credential=AzureCliCredential(), ) - analyst_agent = Agent(client=client, + analyst_agent = Agent( + client=client, name="AnalystAgent", instructions=ANALYST_INSTRUCTIONS, ) - writer_agent = Agent(client=client, + writer_agent = Agent( + client=client, name="WriterAgent", instructions=WRITER_INSTRUCTIONS, ) - editor_agent = Agent(client=client, + editor_agent = Agent( + client=client, name="EditorAgent", instructions=EDITOR_INSTRUCTIONS, ) diff --git a/python/samples/03-workflows/declarative/simple_workflow/main.py b/python/samples/03-workflows/declarative/simple_workflow/main.py index 5e7752673f..4deb1532f1 100644 --- a/python/samples/03-workflows/declarative/simple_workflow/main.py +++ b/python/samples/03-workflows/declarative/simple_workflow/main.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. """Simple workflow sample - demonstrates basic variable setting and output.""" + import asyncio from pathlib import Path diff --git a/python/samples/03-workflows/declarative/student_teacher/main.py b/python/samples/03-workflows/declarative/student_teacher/main.py index d4d8508e3c..e8f99c139d 100644 --- a/python/samples/03-workflows/declarative/student_teacher/main.py +++ b/python/samples/03-workflows/declarative/student_teacher/main.py @@ -64,12 +64,14 @@ async def main() -> None: ) # Create student and teacher agents - student_agent = Agent(client=client, + student_agent = Agent( + client=client, name="StudentAgent", instructions=STUDENT_INSTRUCTIONS, ) - teacher_agent = Agent(client=client, + teacher_agent = Agent( + client=client, name="TeacherAgent", instructions=TEACHER_INSTRUCTIONS, ) diff --git a/python/samples/03-workflows/human-in-the-loop/agents_with_HITL.py b/python/samples/03-workflows/human-in-the-loop/agents_with_HITL.py index aed542e8ad..5de9e60344 100644 --- a/python/samples/03-workflows/human-in-the-loop/agents_with_HITL.py +++ b/python/samples/03-workflows/human-in-the-loop/agents_with_HITL.py @@ -171,10 +171,10 @@ async def main() -> None: # Create the agents writer_agent = Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), name="writer_agent", instructions=("You are a marketing writer."), tool_choice="required", @@ -182,10 +182,10 @@ async def main() -> None: final_editor_agent = Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), name="final_editor_agent", instructions=( "You are an editor who polishes marketing copy after human approval. " diff --git a/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py b/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py index 53b33e3dad..37ede1d037 100644 --- a/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py +++ b/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py @@ -227,10 +227,10 @@ async def main() -> None: # Create agent email_writer_agent = Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), name="EmailWriter", instructions=("You are an excellent email assistant. You respond to incoming emails."), # tools with `approval_mode="always_require"` will trigger approval requests diff --git a/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py b/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py index 490a461c5b..04e1601ff8 100644 --- a/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py +++ b/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py @@ -199,10 +199,10 @@ async def main() -> None: # Create agent and executor guessing_agent = Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), name="GuessingAgent", instructions=( "You guess a number between 1 and 10. " diff --git a/python/samples/03-workflows/orchestrations/concurrent_agents.py b/python/samples/03-workflows/orchestrations/concurrent_agents.py index 4f3575e263..c407ac7767 100644 --- a/python/samples/03-workflows/orchestrations/concurrent_agents.py +++ b/python/samples/03-workflows/orchestrations/concurrent_agents.py @@ -42,7 +42,8 @@ async def main() -> None: credential=AzureCliCredential(), ) - researcher = Agent(client=client, + researcher = Agent( + client=client, instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -50,7 +51,8 @@ async def main() -> None: name="researcher", ) - marketer = Agent(client=client, + marketer = Agent( + client=client, instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -58,7 +60,8 @@ async def main() -> None: name="marketer", ) - legal = Agent(client=client, + legal = Agent( + client=client, instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." diff --git a/python/samples/03-workflows/orchestrations/concurrent_custom_aggregator.py b/python/samples/03-workflows/orchestrations/concurrent_custom_aggregator.py index 4b44111538..98480aaa79 100644 --- a/python/samples/03-workflows/orchestrations/concurrent_custom_aggregator.py +++ b/python/samples/03-workflows/orchestrations/concurrent_custom_aggregator.py @@ -42,21 +42,24 @@ async def main() -> None: credential=AzureCliCredential(), ) - researcher = Agent(client=client, + researcher = Agent( + client=client, instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." ), name="researcher", ) - marketer = Agent(client=client, + marketer = Agent( + client=client, instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." ), name="marketer", ) - legal = Agent(client=client, + legal = Agent( + client=client, instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." diff --git a/python/samples/03-workflows/orchestrations/sequential_agents.py b/python/samples/03-workflows/orchestrations/sequential_agents.py index b7da904bc6..7e61e40dfb 100644 --- a/python/samples/03-workflows/orchestrations/sequential_agents.py +++ b/python/samples/03-workflows/orchestrations/sequential_agents.py @@ -43,12 +43,14 @@ async def main() -> None: credential=AzureCliCredential(), ) - writer = Agent(client=client, + writer = Agent( + client=client, instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), name="writer", ) - reviewer = Agent(client=client, + reviewer = Agent( + client=client, instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), name="reviewer", ) diff --git a/python/samples/03-workflows/parallelism/fan_out_fan_in_edges.py b/python/samples/03-workflows/parallelism/fan_out_fan_in_edges.py index aa811b9932..4e89c8ab6f 100644 --- a/python/samples/03-workflows/parallelism/fan_out_fan_in_edges.py +++ b/python/samples/03-workflows/parallelism/fan_out_fan_in_edges.py @@ -117,10 +117,10 @@ async def main() -> None: researcher = AgentExecutor( Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -131,10 +131,10 @@ async def main() -> None: marketer = AgentExecutor( Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -145,10 +145,10 @@ async def main() -> None: legal = AgentExecutor( Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." diff --git a/python/samples/03-workflows/state-management/state_with_agents.py b/python/samples/03-workflows/state-management/state_with_agents.py index 7f7a6f8ac7..1a4658b8ab 100644 --- a/python/samples/03-workflows/state-management/state_with_agents.py +++ b/python/samples/03-workflows/state-management/state_with_agents.py @@ -164,10 +164,10 @@ def create_spam_detection_agent() -> Agent: """Creates a spam detection agent.""" return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You are a spam detection assistant that identifies spam emails. " "Always return JSON with fields is_spam (bool) and reason (string)." @@ -182,10 +182,10 @@ def create_email_assistant_agent() -> Agent: """Creates an email assistant agent.""" return Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You are an email assistant that helps users draft responses to emails with professionalism. " "Return JSON with a single field 'response' containing the drafted reply." diff --git a/python/samples/03-workflows/state-management/workflow_kwargs.py b/python/samples/03-workflows/state-management/workflow_kwargs.py index aaff1b07c1..6e61ec3a86 100644 --- a/python/samples/03-workflows/state-management/workflow_kwargs.py +++ b/python/samples/03-workflows/state-management/workflow_kwargs.py @@ -88,7 +88,8 @@ async def main() -> None: ) # Create agent with tools that use kwargs - agent = Agent(client=client, + agent = Agent( + client=client, name="assistant", instructions=( "You are a helpful assistant. Use the available tools to help users. " diff --git a/python/samples/03-workflows/visualization/concurrent_with_visualization.py b/python/samples/03-workflows/visualization/concurrent_with_visualization.py index 70b4188bea..b3ba00b536 100644 --- a/python/samples/03-workflows/visualization/concurrent_with_visualization.py +++ b/python/samples/03-workflows/visualization/concurrent_with_visualization.py @@ -99,10 +99,10 @@ async def main() -> None: researcher = AgentExecutor( Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -114,10 +114,10 @@ async def main() -> None: marketer = AgentExecutor( Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -129,10 +129,10 @@ async def main() -> None: legal = AgentExecutor( Agent( client=FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), - ), + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ), instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." diff --git a/python/samples/04-hosting/a2a/agent_definitions.py b/python/samples/04-hosting/a2a/agent_definitions.py index 08e2edaced..2efcd7ebd9 100644 --- a/python/samples/04-hosting/a2a/agent_definitions.py +++ b/python/samples/04-hosting/a2a/agent_definitions.py @@ -56,7 +56,8 @@ def create_invoice_agent(client: FoundryChatClient) -> Agent: """Create an invoice agent backed by the given client with query tools.""" - return Agent(client=client, + return Agent( + client=client, name="InvoiceAgent", instructions=INVOICE_INSTRUCTIONS, tools=[query_invoices, query_by_transaction_id, query_by_invoice_id], @@ -65,7 +66,8 @@ def create_invoice_agent(client: FoundryChatClient) -> Agent: def create_policy_agent(client: FoundryChatClient) -> Agent: """Create a policy agent backed by the given client.""" - return Agent(client=client, + return Agent( + client=client, name="PolicyAgent", instructions=POLICY_INSTRUCTIONS, ) @@ -73,7 +75,8 @@ def create_policy_agent(client: FoundryChatClient) -> Agent: def create_logistics_agent(client: FoundryChatClient) -> Agent: """Create a logistics agent backed by the given client.""" - return Agent(client=client, + return Agent( + client=client, name="LogisticsAgent", instructions=LOGISTICS_INSTRUCTIONS, ) diff --git a/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py b/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py index a7ed6a2e8e..a1f45d64fa 100644 --- a/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py +++ b/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py @@ -23,7 +23,8 @@ def _create_agent() -> Any: """Create the Joker agent.""" _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name="Joker", instructions="You are good at telling jokes.", ) diff --git a/python/samples/04-hosting/azure_functions/02_multi_agent/function_app.py b/python/samples/04-hosting/azure_functions/02_multi_agent/function_app.py index ec3875b9c0..bcf8c6b1da 100644 --- a/python/samples/04-hosting/azure_functions/02_multi_agent/function_app.py +++ b/python/samples/04-hosting/azure_functions/02_multi_agent/function_app.py @@ -61,13 +61,15 @@ def calculate_tip(bill_amount: float, tip_percentage: float = 15.0) -> dict[str, # 1. Create multiple agents, each with its own instruction set and tools. client = FoundryChatClient(credential=AzureCliCredential()) -weather_agent = Agent(client=client, +weather_agent = Agent( + client=client, name="WeatherAgent", instructions="You are a helpful weather assistant. Provide current weather information.", tools=[get_weather], ) -math_agent = Agent(client=client, +math_agent = Agent( + client=client, name="MathAgent", instructions="You are a helpful math assistant. Help users with calculations like tip calculations.", tools=[calculate_tip], diff --git a/python/samples/04-hosting/azure_functions/03_reliable_streaming/function_app.py b/python/samples/04-hosting/azure_functions/03_reliable_streaming/function_app.py index 1ee9e85d3e..9804b199e9 100644 --- a/python/samples/04-hosting/azure_functions/03_reliable_streaming/function_app.py +++ b/python/samples/04-hosting/azure_functions/03_reliable_streaming/function_app.py @@ -21,7 +21,7 @@ import azure.functions as func import redis.asyncio as aioredis -from agent_framework import AgentResponseUpdate +from agent_framework import Agent, AgentResponseUpdate from agent_framework.azure import ( AgentCallbackContext, AgentFunctionApp, @@ -156,7 +156,8 @@ async def on_agent_response(self, response, context: AgentCallbackContext) -> No def create_travel_agent(): """Create the TravelPlanner agent with tools.""" _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name="TravelPlanner", instructions="""You are an expert travel planner who creates detailed, personalized travel itineraries. When asked to plan a trip, you should: diff --git a/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py b/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py index b7eaaf40b8..6e19a00555 100644 --- a/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py +++ b/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py @@ -40,7 +40,8 @@ def _create_writer_agent() -> Any: ) _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name=WRITER_AGENT_NAME, instructions=instructions, ) diff --git a/python/samples/04-hosting/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py b/python/samples/04-hosting/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py index 5f0e367d6e..756bfdb5d9 100644 --- a/python/samples/04-hosting/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py +++ b/python/samples/04-hosting/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py @@ -36,12 +36,14 @@ def _create_agents() -> list[Any]: client = FoundryChatClient(credential=AzureCliCredential()) - physicist = Agent(client=client, + physicist = Agent( + client=client, name=PHYSICIST_AGENT_NAME, instructions="You are an expert in physics. You answer questions from a physics perspective.", ) - chemist = Agent(client=client, + chemist = Agent( + client=client, name=CHEMIST_AGENT_NAME, instructions="You are an expert in chemistry. You answer questions from a chemistry perspective.", ) diff --git a/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py b/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py index 2bda1e0463..1cdaa4165f 100644 --- a/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py +++ b/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py @@ -52,12 +52,14 @@ class EmailPayload(BaseModel): def _create_agents() -> list[Any]: client = FoundryChatClient(credential=AzureCliCredential()) - spam_agent = Agent(client=client, + spam_agent = Agent( + client=client, name=SPAM_AGENT_NAME, instructions="You are a spam detection assistant that identifies spam emails.", ) - email_agent = Agent(client=client, + email_agent = Agent( + client=client, name=EMAIL_AGENT_NAME, instructions="You are an email assistant that helps users draft responses to emails with professionalism.", ) diff --git a/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py b/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py index 7f16213d91..2bf75e0a74 100644 --- a/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py +++ b/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py @@ -59,7 +59,8 @@ def _create_writer_agent() -> Any: ) _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name=WRITER_AGENT_NAME, instructions=instructions, ) diff --git a/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py b/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py index 91c18e451a..dde11bbdf5 100644 --- a/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py +++ b/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py @@ -37,19 +37,22 @@ # Define three AI agents with different roles # Agent 1: Joker - HTTP trigger only (default) -agent1 = Agent(client=client, +agent1 = Agent( + client=client, name="Joker", instructions="You are good at telling jokes.", ) # Agent 2: StockAdvisor - MCP tool trigger only -agent2 = Agent(client=client, +agent2 = Agent( + client=client, name="StockAdvisor", instructions="Check stock prices.", ) # Agent 3: PlantAdvisor - Both HTTP and MCP tool triggers -agent3 = Agent(client=client, +agent3 = Agent( + client=client, name="PlantAdvisor", instructions="Recommend plants.", description="Get plant recommendations.", diff --git a/python/samples/04-hosting/durabletask/01_single_agent/sample.py b/python/samples/04-hosting/durabletask/01_single_agent/sample.py index 8adb31e40d..fb96a8e65a 100644 --- a/python/samples/04-hosting/durabletask/01_single_agent/sample.py +++ b/python/samples/04-hosting/durabletask/01_single_agent/sample.py @@ -11,6 +11,7 @@ To run this sample: python sample.py """ + import logging # Import helper functions from worker and client modules diff --git a/python/samples/04-hosting/durabletask/01_single_agent/worker.py b/python/samples/04-hosting/durabletask/01_single_agent/worker.py index a660c0ac6f..8ac960940f 100644 --- a/python/samples/04-hosting/durabletask/01_single_agent/worker.py +++ b/python/samples/04-hosting/durabletask/01_single_agent/worker.py @@ -36,7 +36,8 @@ def create_joker_agent() -> Agent: Agent: The configured Joker agent """ _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name="Joker", instructions="You are good at telling jokes.", ) diff --git a/python/samples/04-hosting/durabletask/02_multi_agent/sample.py b/python/samples/04-hosting/durabletask/02_multi_agent/sample.py index 8f2decaba8..581456f861 100644 --- a/python/samples/04-hosting/durabletask/02_multi_agent/sample.py +++ b/python/samples/04-hosting/durabletask/02_multi_agent/sample.py @@ -11,6 +11,7 @@ To run this sample: python sample.py """ + import logging # Import helper functions from worker and client modules diff --git a/python/samples/04-hosting/durabletask/02_multi_agent/worker.py b/python/samples/04-hosting/durabletask/02_multi_agent/worker.py index 50640e0621..db81860a51 100644 --- a/python/samples/04-hosting/durabletask/02_multi_agent/worker.py +++ b/python/samples/04-hosting/durabletask/02_multi_agent/worker.py @@ -72,7 +72,8 @@ def create_weather_agent(): Agent: The configured Weather agent with weather tool """ _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name=WEATHER_AGENT_NAME, instructions="You are a helpful weather assistant. Provide current weather information.", tools=[get_weather], @@ -86,7 +87,8 @@ def create_math_agent(): Agent: The configured Math agent with calculation tools """ _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name=MATH_AGENT_NAME, instructions="You are a helpful math assistant. Help users with calculations like tip calculations.", tools=[calculate_tip], diff --git a/python/samples/04-hosting/durabletask/03_single_agent_streaming/sample.py b/python/samples/04-hosting/durabletask/03_single_agent_streaming/sample.py index 6ea11ad15e..7495952602 100644 --- a/python/samples/04-hosting/durabletask/03_single_agent_streaming/sample.py +++ b/python/samples/04-hosting/durabletask/03_single_agent_streaming/sample.py @@ -13,6 +13,7 @@ To run this sample: python sample.py """ + import logging # Import helper functions from worker and client modules diff --git a/python/samples/04-hosting/durabletask/03_single_agent_streaming/worker.py b/python/samples/04-hosting/durabletask/03_single_agent_streaming/worker.py index b98ed45bdc..2e63df4fa8 100644 --- a/python/samples/04-hosting/durabletask/03_single_agent_streaming/worker.py +++ b/python/samples/04-hosting/durabletask/03_single_agent_streaming/worker.py @@ -154,7 +154,8 @@ def create_travel_agent() -> "Agent": Agent: The configured TravelPlanner agent with travel planning tools. """ _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name="TravelPlanner", instructions="""You are an expert travel planner who creates detailed, personalized travel itineraries. When asked to plan a trip, you should: diff --git a/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/sample.py b/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/sample.py index c1d3749964..65faae511e 100644 --- a/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/sample.py +++ b/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/sample.py @@ -16,6 +16,7 @@ To run this sample: python sample.py """ + import logging # Import helper functions from worker and client modules diff --git a/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/worker.py b/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/worker.py index 54935d2bf3..c6a50d4a81 100644 --- a/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/worker.py +++ b/python/samples/04-hosting/durabletask/04_single_agent_orchestration_chaining/worker.py @@ -50,7 +50,8 @@ def create_writer_agent() -> "Agent": ) _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name=WRITER_AGENT_NAME, instructions=instructions, ) diff --git a/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/sample.py b/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/sample.py index 4653a98a7b..7c4203b24d 100644 --- a/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/sample.py +++ b/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/sample.py @@ -13,6 +13,7 @@ To run this sample: python sample.py """ + import logging # Import helper functions from worker and client modules diff --git a/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/worker.py b/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/worker.py index 6f40311a75..18ddbcf073 100644 --- a/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/worker.py +++ b/python/samples/04-hosting/durabletask/05_multi_agent_orchestration_concurrency/worker.py @@ -44,7 +44,8 @@ def create_physicist_agent() -> "Agent": Agent: The configured Physicist agent """ _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name=PHYSICIST_AGENT_NAME, instructions="You are an expert in physics. You answer questions from a physics perspective.", ) @@ -57,7 +58,8 @@ def create_chemist_agent() -> "Agent": Agent: The configured Chemist agent """ _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name=CHEMIST_AGENT_NAME, instructions="You are an expert in chemistry. You answer questions from a chemistry perspective.", ) diff --git a/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/worker.py b/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/worker.py index c95ee4f997..61e98f0c21 100644 --- a/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/worker.py +++ b/python/samples/04-hosting/durabletask/06_multi_agent_orchestration_conditionals/worker.py @@ -65,7 +65,8 @@ def create_spam_agent() -> "Agent": Agent: The configured Spam Detection agent """ _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name=SPAM_AGENT_NAME, instructions="You are a spam detection assistant that identifies spam emails.", ) @@ -78,7 +79,8 @@ def create_email_agent() -> "Agent": Agent: The configured Email Assistant agent """ _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name=EMAIL_AGENT_NAME, instructions="You are an email assistant that helps users draft responses to emails with professionalism.", ) diff --git a/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/sample.py b/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/sample.py index ae00a7cb67..9ce7521ea5 100644 --- a/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/sample.py +++ b/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/sample.py @@ -14,6 +14,7 @@ To run this sample: python sample.py """ + import logging # Import helper functions from worker and client modules diff --git a/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/worker.py b/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/worker.py index 4d5fb85e21..8030ec4144 100644 --- a/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/worker.py +++ b/python/samples/04-hosting/durabletask/07_single_agent_orchestration_hitl/worker.py @@ -75,7 +75,8 @@ def create_writer_agent() -> "Agent": ) _client = FoundryChatClient(credential=AzureCliCredential()) - return Agent(client=_client, + return Agent( + client=_client, name=WRITER_AGENT_NAME, instructions=instructions, ) diff --git a/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py b/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py index 6ee26b6157..f513708261 100644 --- a/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py +++ b/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py @@ -21,9 +21,7 @@ # Configure these for your Foundry project # Read the explicit variables present in the .env file -PROJECT_ENDPOINT = os.getenv( - "PROJECT_ENDPOINT" -) # e.g., "https://.services.ai.azure.com" +PROJECT_ENDPOINT = os.getenv("PROJECT_ENDPOINT") # e.g., "https://.services.ai.azure.com" MODEL_DEPLOYMENT_NAME = os.getenv( "MODEL_DEPLOYMENT_NAME", "gpt-4.1-mini" ) # Your model deployment name e.g., "gpt-4.1-mini" @@ -91,14 +89,10 @@ def get_available_hotels( nights = (check_out - check_in).days # Filter hotels by price - available_hotels = [ - hotel for hotel in SEATTLE_HOTELS if hotel["price_per_night"] <= max_price - ] + available_hotels = [hotel for hotel in SEATTLE_HOTELS if hotel["price_per_night"] <= max_price] if not available_hotels: - return ( - f"No hotels found in Seattle within your budget of ${max_price}/night." - ) + return f"No hotels found in Seattle within your budget of ${max_price}/night." # Build response result = f"Available hotels in Seattle from {check_in_date} to {check_out_date} ({nights} nights):\n\n" @@ -118,11 +112,7 @@ def get_available_hotels( def get_credential(): """Will use Managed Identity when running in Azure, otherwise falls back to Azure CLI Credential.""" - return ( - ManagedIdentityCredential() - if os.getenv("MSI_ENDPOINT") - else AzureCliCredential() - ) + return ManagedIdentityCredential() if os.getenv("MSI_ENDPOINT") else AzureCliCredential() async def main(): @@ -133,7 +123,8 @@ async def main(): model=MODEL_DEPLOYMENT_NAME, credential=credential, ) - agent = Agent(client=client, + agent = Agent( + client=client, name="SeattleHotelAgent", instructions="""You are a helpful travel assistant specializing in finding hotels in Seattle, Washington. diff --git a/python/samples/05-end-to-end/hosted_agents/writer_reviewer_agents_in_workflow/main.py b/python/samples/05-end-to-end/hosted_agents/writer_reviewer_agents_in_workflow/main.py index 13ff15b219..5ce63d2350 100644 --- a/python/samples/05-end-to-end/hosted_agents/writer_reviewer_agents_in_workflow/main.py +++ b/python/samples/05-end-to-end/hosted_agents/writer_reviewer_agents_in_workflow/main.py @@ -24,11 +24,7 @@ def get_credential(): """Will use Managed Identity when running in Azure, otherwise falls back to Azure CLI Credential.""" - return ( - ManagedIdentityCredential() - if os.getenv("MSI_ENDPOINT") - else AzureCliCredential() - ) + return ManagedIdentityCredential() if os.getenv("MSI_ENDPOINT") else AzureCliCredential() @asynccontextmanager @@ -39,11 +35,13 @@ async def create_agents(): model=MODEL_DEPLOYMENT_NAME, credential=credential, ) - writer = Agent(client=client, + writer = Agent( + client=client, name="Writer", instructions="You are an excellent content writer. You create new content and edit contents based on the feedback.", ) - reviewer = Agent(client=client, + reviewer = Agent( + client=client, name="Reviewer", instructions="You are an excellent content reviewer. Provide actionable feedback to the writer about the provided content in the most concise manner possible.", ) @@ -52,7 +50,9 @@ async def create_agents(): def create_workflow(writer, reviewer): workflow = WorkflowBuilder(start_executor=writer).add_edge(writer, reviewer).build() - return Agent(client=workflow,) + return Agent( + client=workflow, + ) async def main() -> None: diff --git a/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py b/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py index 5a5856fdd6..d70ce97712 100644 --- a/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py +++ b/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py @@ -102,8 +102,8 @@ def get_weather( def build_agent() -> Agent: """Create and return the chat agent instance with weather tool registered.""" _client = FoundryChatClient() - return Agent(client=_client, - name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather + return Agent( + client=_client, name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather ) diff --git a/python/samples/autogen-migration/orchestrations/01_round_robin_group_chat.py b/python/samples/autogen-migration/orchestrations/01_round_robin_group_chat.py index ce15571701..bb0c4c2367 100644 --- a/python/samples/autogen-migration/orchestrations/01_round_robin_group_chat.py +++ b/python/samples/autogen-migration/orchestrations/01_round_robin_group_chat.py @@ -17,7 +17,7 @@ import asyncio -from agent_framework import Message +from agent_framework import Agent, Message from dotenv import load_dotenv # Load environment variables from .env file From 21580fb0bc76badc497bc284d50436026795a62f Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 20 Mar 2026 19:57:51 +0100 Subject: [PATCH 03/13] =?UTF-8?q?fix:=20CI=20failures=20=E2=80=94=20mypy?= =?UTF-8?q?=20errors,=20coverage=20targets,=20sample=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - azure-ai mypy: add type ignores for TypedDict total=, model arg, forward ref - Coverage: replace core.azure/openai targets with openai package target - project_provider: add type annotation for opts dict Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/python-check-coverage.py | 3 +-- python/packages/azure-ai/agent_framework_azure_ai/_client.py | 4 ++-- .../azure-ai/agent_framework_azure_ai/_foundry_agent.py | 2 +- .../azure-ai/agent_framework_azure_ai/_project_provider.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-check-coverage.py b/.github/workflows/python-check-coverage.py index 84cd500b94..af6d38ffea 100644 --- a/.github/workflows/python-check-coverage.py +++ b/.github/workflows/python-check-coverage.py @@ -41,8 +41,7 @@ "packages.purview.agent_framework_purview", "packages.anthropic.agent_framework_anthropic", "packages.azure-ai-search.agent_framework_azure_ai_search", - "packages.core.agent_framework.azure", - "packages.core.agent_framework.openai", + "packages.openai.agent_framework_openai", # Individual files (if you want to enforce specific files instead of whole packages) "packages/core/agent_framework/observability.py", # Add more targets here as coverage improves diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 5c217caee9..c07ecaf8a8 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -68,7 +68,7 @@ logger = logging.getLogger("agent_framework.azure") -class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False): # type: ignore[misc] +class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False): # type: ignore[misc, call-arg] """Azure AI Project Agent options.""" rai_config: RaiConfig @@ -218,7 +218,7 @@ class MyOptions(ChatOptions, total=False): # Initialize parent with OpenAI client from project super().__init__( async_client=project_client.get_openai_client(), - model=azure_ai_settings.get("model"), + model=azure_ai_settings.get("model"), # type: ignore[arg-type] additional_properties=additional_properties, ) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py index c141cc1115..51c476c192 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py @@ -43,7 +43,7 @@ FoundryAgentOptionsT = TypeVar( "FoundryAgentOptionsT", bound=TypedDict, # type: ignore[valid-type] - default="OpenAIChatOptions", # noqa: F821 # pyright: ignore[reportUndefinedVariable] + default="OpenAIChatOptions", # noqa: F821 # pyright: ignore[reportUndefinedVariable] # type: ignore[name-defined] covariant=True, ) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py index 2b83b69ae4..f73171b15f 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py @@ -200,7 +200,7 @@ async def create_agent( ) # Extract options from default_options if present - opts = dict(default_options) if default_options else {} + opts: dict[str, Any] = dict(default_options) if default_options else {} response_format = opts.get("response_format") rai_config = opts.get("rai_config") reasoning = opts.get("reasoning") From da4228ed1207495da1f957aceee4f40840df453e Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 20 Mar 2026 20:01:34 +0100 Subject: [PATCH 04/13] fix: populate openai .pyi stub, fix broken README links, coverage targets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../core/agent_framework/openai/__init__.pyi | 49 ++- python/samples/02-agents/providers/README.md | 6 +- .../02-agents/providers/azure/README.md | 283 ++---------------- .../semantic-kernel-migration/README.md | 3 - 4 files changed, 73 insertions(+), 268 deletions(-) diff --git a/python/packages/core/agent_framework/openai/__init__.pyi b/python/packages/core/agent_framework/openai/__init__.pyi index d654b4d09e..3f7ad148fb 100644 --- a/python/packages/core/agent_framework/openai/__init__.pyi +++ b/python/packages/core/agent_framework/openai/__init__.pyi @@ -1,7 +1,48 @@ # Copyright (c) Microsoft. All rights reserved. -# This is a dynamic namespace — all symbols are lazily loaded from agent-framework-openai. -from typing import Any +# Type stubs for the agent_framework.openai lazy-loading namespace. +# Install agent-framework-openai for full type support. -def __getattr__(name: str) -> Any: ... # pyright: ignore[reportIncompleteStub] -def __dir__() -> list[str]: ... +from agent_framework_openai import ( + AssistantToolResources, + ContentFilterResultSeverity, + OpenAIAssistantProvider, + OpenAIAssistantsClient, + OpenAIAssistantsOptions, + OpenAIChatClient, + OpenAIChatCompletionClient, + OpenAIChatCompletionOptions, + OpenAIChatOptions, + OpenAIContentFilterException, + OpenAIContinuationToken, + OpenAIEmbeddingClient, + OpenAIEmbeddingOptions, + OpenAIResponsesClient, + OpenAIResponsesOptions, + OpenAISettings, + RawOpenAIChatClient, + RawOpenAIChatCompletionClient, + RawOpenAIResponsesClient, +) + +__all__ = [ + "AssistantToolResources", + "ContentFilterResultSeverity", + "OpenAIAssistantProvider", + "OpenAIAssistantsClient", + "OpenAIAssistantsOptions", + "OpenAIChatClient", + "OpenAIChatCompletionClient", + "OpenAIChatCompletionOptions", + "OpenAIChatOptions", + "OpenAIContentFilterException", + "OpenAIContinuationToken", + "OpenAIEmbeddingClient", + "OpenAIEmbeddingOptions", + "OpenAIResponsesClient", + "OpenAIResponsesOptions", + "OpenAISettings", + "RawOpenAIChatClient", + "RawOpenAIChatCompletionClient", + "RawOpenAIResponsesClient", +] diff --git a/python/samples/02-agents/providers/README.md b/python/samples/02-agents/providers/README.md index 20a598b08e..214ace2182 100644 --- a/python/samples/02-agents/providers/README.md +++ b/python/samples/02-agents/providers/README.md @@ -6,14 +6,12 @@ This directory groups provider-specific samples for Agent Framework. | --- | --- | | [`anthropic/`](anthropic/) | Anthropic Claude samples using both `AnthropicClient` and `ClaudeAgent`, including tools, MCP, sessions, and Foundry Anthropic integration. | | [`amazon/`](amazon/) | AWS Bedrock samples using `BedrockChatClient`, including tool-enabled agent usage. | -| [`azure_ai/`](azure_ai/) | Azure AI Foundry V2 (`azure-ai-projects`) samples with `AzureAIClient`, from basic setup to advanced patterns like search, memory, A2A, MCP, and provider methods. | -| [`azure_ai_agent/`](azure_ai_agent/) | Azure AI Foundry V1 (`azure-ai-agents`) samples with `AzureAIAgentsProvider`, including provider methods and common hosted tool integrations. | -| [`azure_openai/`](azure_openai/) | Azure OpenAI samples for Assistants, Chat, and Responses clients, with examples for sessions, tools, MCP, file search, and code interpreter. | +| [`azure/`](azure/) | Azure AI Foundry samples using `FoundryChatClient`, `FoundryAgent`, and `FoundryAgentClient` for Responses API, pre-configured agents, and hosted tools. | | [`copilotstudio/`](copilotstudio/) | Microsoft Copilot Studio agent samples, including required environment/app registration setup and explicit authentication patterns. | | [`custom/`](custom/) | Framework extensibility samples for building custom `BaseAgent` and `BaseChatClient` implementations, including layer-composition guidance. | | [`foundry_local/`](foundry_local/) | Foundry Local samples using `FoundryLocalClient` for local model inference with streaming, non-streaming, and tool-calling patterns. | | [`github_copilot/`](github_copilot/) | `GitHubCopilotAgent` samples showing basic usage, session handling, permission-scoped shell/file/url access, and MCP integration. | | [`ollama/`](ollama/) | Local Ollama samples using `OllamaChatClient` (recommended) plus OpenAI-compatible Ollama setup, including reasoning and multimodal examples. | -| [`openai/`](openai/) | OpenAI provider samples for Assistants, Chat, and Responses clients, including tools, structured output, sessions, MCP, web search, and multimodal tasks. | +| [`openai/`](openai/) | OpenAI provider samples for Chat and Chat Completion clients, including tools, structured output, sessions, MCP, web search, and multimodal tasks. | Each folder has its own README with setup requirements and file-by-file details. diff --git a/python/samples/02-agents/providers/azure/README.md b/python/samples/02-agents/providers/azure/README.md index 87b492fe16..6d7b9e40c4 100644 --- a/python/samples/02-agents/providers/azure/README.md +++ b/python/samples/02-agents/providers/azure/README.md @@ -1,269 +1,38 @@ -# Azure OpenAI Agent Examples +# Azure Provider Samples -This folder contains examples demonstrating different ways to create and use agents with the different Azure OpenAI chat client from the `agent_framework.azure` package. +This folder contains examples demonstrating different ways to use Azure AI Foundry with Agent Framework. -## Examples +## FoundryAgent Samples | File | Description | |------|-------------| -| [`azure_assistants_basic.py`](azure_assistants_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIAssistantsClient`. Shows both streaming and non-streaming responses with automatic assistant creation and cleanup. | -| [`azure_assistants_with_code_interpreter.py`](azure_assistants_with_code_interpreter.py) | Shows how to use `AzureOpenAIAssistantsClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | -| [`azure_assistants_with_existing_assistant.py`](azure_assistants_with_existing_assistant.py) | Shows how to work with a pre-existing assistant by providing the assistant ID to the Azure Assistants client. Demonstrates proper cleanup of manually created assistants. | -| [`azure_assistants_with_explicit_settings.py`](azure_assistants_with_explicit_settings.py) | Shows how to initialize an agent with a specific assistants client, configuring settings explicitly including endpoint and deployment name. | -| [`azure_assistants_with_function_tools.py`](azure_assistants_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | -| [`azure_assistants_with_session.py`](azure_assistants_with_session.py) | Demonstrates session management with Azure agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | -| [`azure_chat_client_basic.py`](azure_chat_client_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIChatClient`. Shows both streaming and non-streaming responses for chat-based interactions with Azure OpenAI models. | -| [`azure_chat_client_with_explicit_settings.py`](azure_chat_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific chat client, configuring settings explicitly including endpoint and deployment name. | -| [`azure_chat_client_with_function_tools.py`](azure_chat_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | -| [`azure_chat_client_with_session.py`](azure_chat_client_with_session.py) | Demonstrates session management with Azure agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | -| [`azure_responses_client_basic.py`](azure_responses_client_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with Azure OpenAI models. | -| [`azure_responses_client_code_interpreter_files.py`](azure_responses_client_code_interpreter_files.py) | Demonstrates using `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with file uploads for data analysis. Shows how to create, upload, and analyze CSV files using Python code execution with Azure OpenAI Responses. | -| [`azure_responses_client_image_analysis.py`](azure_responses_client_image_analysis.py) | Shows how to use Azure OpenAI Responses for image analysis and vision tasks. Demonstrates multi-modal messages combining text and image content using remote URLs. | -| [`azure_responses_client_with_code_interpreter.py`](azure_responses_client_with_code_interpreter.py) | Shows how to use `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | -| [`azure_responses_client_with_explicit_settings.py`](azure_responses_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific responses client, configuring settings explicitly including endpoint and deployment name. | -| [`azure_responses_client_with_file_search.py`](azure_responses_client_with_file_search.py) | Demonstrates using `AzureOpenAIResponsesClient.get_file_search_tool()` with Azure OpenAI Responses Client for direct document-based question answering and information retrieval from vector stores. | -| [`azure_responses_client_with_foundry.py`](azure_responses_client_with_foundry.py) | Shows how to create an agent using an Azure AI Foundry project endpoint instead of a direct Azure OpenAI endpoint. Requires the `azure-ai-projects` package. | -| [`azure_responses_client_with_function_tools.py`](azure_responses_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | -| [`azure_responses_client_with_hosted_mcp.py`](azure_responses_client_with_hosted_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with hosted Model Context Protocol (MCP) servers using `AzureOpenAIResponsesClient.get_mcp_tool()` for extended functionality. | -| [`azure_responses_client_with_local_mcp.py`](azure_responses_client_with_local_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with local Model Context Protocol (MCP) servers using MCPStreamableHTTPTool for extended functionality. | -| [`azure_responses_client_with_session.py`](azure_responses_client_with_session.py) | Demonstrates session management with Azure agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | +| [`foundry_agent_basic.py`](foundry_agent_basic.py) | | +| [`foundry_agent_custom_client.py`](foundry_agent_custom_client.py) | Foundry Agent — Custom client configuration | +| [`foundry_agent_hosted.py`](foundry_agent_hosted.py) | | +| [`foundry_agent_with_env_vars.py`](foundry_agent_with_env_vars.py) | | +| [`foundry_agent_with_function_tools.py`](foundry_agent_with_function_tools.py) | Foundry Agent with Local Function Tools | -## Environment Variables - -Make sure to set the following environment variables before running the examples: - -- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint -- `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`: The name of your Azure OpenAI chat model deployment -- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: The name of your Azure OpenAI Responses deployment - -For the Foundry project sample (`azure_responses_client_with_foundry.py`), also set: -- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint - -Optionally, you can set: -- `AZURE_OPENAI_API_VERSION`: The API version to use (default is `2024-02-15-preview`) -- `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (if not using `AzureCliCredential`) -- `AZURE_OPENAI_BASE_URL`: Your Azure OpenAI base URL (if different from the endpoint) - -## Authentication - -All examples use `AzureCliCredential` for authentication. Run `az login` in your terminal before running the examples, or replace `AzureCliCredential` with your preferred authentication method. - -## Required role-based access control (RBAC) roles - -To access the Azure OpenAI API, your Azure account or service principal needs one of the following RBAC roles assigned to the Azure OpenAI resource: - -- **Cognitive Services OpenAI User**: Provides read access to Azure OpenAI resources and the ability to call the inference APIs. This is the minimum role required for running these examples. -- **Cognitive Services OpenAI Contributor**: Provides full access to Azure OpenAI resources, including the ability to create, update, and delete deployments and models. - -For most scenarios, the **Cognitive Services OpenAI User** role is sufficient. You can assign this role through the Azure portal under the Azure OpenAI resource's "Access control (IAM)" section. - -For more detailed information about Azure OpenAI RBAC roles, see: [Role-based access control for Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/role-based-access-control) -# Azure AI Agent Examples - -This folder contains examples demonstrating different ways to create and use agents with the Azure AI client from the `agent_framework.azure` package. These examples use the `AzureAIClient` with the `azure-ai-projects` 2.x (V2) API surface (see [changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-projects/CHANGELOG.md#200b1-2025-11-11)). For V1 (`azure-ai-agents` 1.x) samples using `AzureAIAgentClient`, see the [Azure AI V1 examples folder](../azure_ai_agent/). When using preview-only agent creation features on GA SDK versions, create `AIProjectClient` with `allow_preview=True`. - -## Examples +## FoundryChatClient Samples | File | Description | |------|-------------| -| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIProjectAgentProvider`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. | -| [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive guide to `AzureAIProjectAgentProvider` methods: `create_agent()` for creating new agents, `get_agent()` for retrieving existing agents (by name, reference, or details), and `as_agent()` for wrapping SDK objects without HTTP calls. | -| [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation by using `provider.get_agent()` to retrieve the latest version. | -| [`azure_ai_with_agent_as_tool.py`](azure_ai_with_agent_as_tool.py) | Shows how to use the agent-as-tool pattern with Azure AI agents, where one agent delegates work to specialized sub-agents wrapped as tools using `as_tool()`. Demonstrates hierarchical agent architectures. | -| [`azure_ai_with_agent_to_agent.py`](azure_ai_with_agent_to_agent.py) | Shows how to use Agent-to-Agent (A2A) capabilities with Azure AI agents to enable communication with other agents using the A2A protocol. Requires an A2A connection configured in your Azure AI project. | -| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Shows how to use Azure AI Search with Azure AI agents to search through indexed data and answer user questions with proper citations. Requires an Azure AI Search connection and index configured in your Azure AI project. | -| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to search the web for current information and provide grounded responses with citations. Requires a Bing connection configured in your Azure AI project. | -| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to search custom search instances and provide responses with relevant results. Requires a Bing Custom Search connection and instance configured in your Azure AI project. | -| [`azure_ai_with_browser_automation.py`](azure_ai_with_browser_automation.py) | Shows how to use Browser Automation with Azure AI agents to perform automated web browsing tasks and provide responses based on web interactions. Requires a Browser Automation connection configured in your Azure AI project. | -| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use `AzureAIClient.get_code_interpreter_tool()` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. | -| [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. | -| [`azure_ai_with_code_interpreter_file_download.py`](azure_ai_with_code_interpreter_file_download.py) | Shows how to download files generated by code interpreter using the OpenAI containers API. | -| [`azure_ai_with_content_filtering.py`](azure_ai_with_content_filtering.py) | Shows how to enable content filtering (RAI policy) on Azure AI agents using `RaiConfig`. Requires creating an RAI policy in Azure AI Foundry portal first. | -| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent name and version to the Azure AI client. Demonstrates agent reuse patterns for production scenarios. | -| [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Demonstrates how to use an existing conversation created on the service side with Azure AI agents. Shows two approaches: specifying conversation ID at the client level and using AgentSession with an existing conversation ID. | -| [`azure_ai_with_application_endpoint.py`](azure_ai_with_application_endpoint.py) | Demonstrates calling the Azure AI application-scoped endpoint. | -| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIClient` settings, including project endpoint, model deployment, and credentials rather than relying on environment variable defaults. | -| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use `AzureAIClient.get_file_search_tool()` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. | -| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent using `AzureAIClient.get_mcp_tool()`. | -| [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate local Model Context Protocol (MCP) tools with Azure AI agents. | -| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. | -| [`azure_ai_with_runtime_json_schema.py`](azure_ai_with_runtime_json_schema.py) | Shows how to use structured outputs (response format) with Azure AI agents using a JSON schema to enforce specific response schemas. | -| [`azure_ai_with_search_context_agentic.py`](../../context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py) | Shows how to use AzureAISearchContextProvider with agentic mode. Uses Knowledge Bases for multi-hop reasoning across documents with query planning. Recommended for most scenarios - slightly slower with more token consumption for query planning, but more accurate results. | -| [`azure_ai_with_search_context_semantic.py`](../../context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py) | Shows how to use AzureAISearchContextProvider with semantic mode. Fast hybrid search with vector + keyword search and semantic ranking for RAG. Best for simple queries where speed is critical. | -| [`azure_ai_with_sharepoint.py`](azure_ai_with_sharepoint.py) | Shows how to use SharePoint grounding with Azure AI agents to search through SharePoint content and answer user questions with proper citations. Requires a SharePoint connection configured in your Azure AI project. | -| [`azure_ai_with_session.py`](azure_ai_with_session.py) | Demonstrates session management with Azure AI agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | -| [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use `AzureAIClient.get_image_generation_tool()` with Azure AI agents to generate images based on text prompts. | -| [`azure_ai_with_memory_search.py`](azure_ai_with_memory_search.py) | Shows how to use memory search functionality with Azure AI agents for conversation persistence. Demonstrates creating memory stores and enabling agents to search through conversation history. | -| [`azure_ai_with_microsoft_fabric.py`](azure_ai_with_microsoft_fabric.py) | Shows how to use Microsoft Fabric with Azure AI agents to query Fabric data sources and provide responses based on data analysis. Requires a Microsoft Fabric connection configured in your Azure AI project. | -| [`azure_ai_with_openapi.py`](azure_ai_with_openapi.py) | Shows how to integrate OpenAPI specifications with Azure AI agents using dictionary-based tool configuration. Demonstrates using external REST APIs for dynamic data lookup. | -| [`azure_ai_with_reasoning.py`](azure_ai_with_reasoning.py) | Shows how to enable reasoning for a model that supports it. | -| [`azure_ai_with_web_search.py`](azure_ai_with_web_search.py) | Shows how to use `AzureAIClient.get_web_search_tool()` with Azure AI agents to perform web searches and retrieve up-to-date information from the internet. | - -## Environment Variables - -Before running the examples, you need to set up your environment variables. You can do this in one of two ways: - -### Option 1: Using a .env file (Recommended) - -1. Copy the `.env.example` file from the `python` directory to create a `.env` file: - - ```bash - cp ../../../../.env.example ../../../../.env - ``` - -2. Edit the `.env` file and add your values: - - ```env - AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" - AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" - ``` - -### Option 2: Using environment variables directly - -Set the environment variables in your shell: - -```bash -export AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" -export AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" -``` - -### Required Variables - -- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint (required for all examples) -- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (required for all examples) - -## Authentication - -All examples use `AzureCliCredential` for authentication by default. Before running the examples: - -1. Install the Azure CLI -2. Run `az login` to authenticate with your Azure account -3. Ensure you have appropriate permissions to the Azure AI project - -Alternatively, you can replace `AzureCliCredential` with other authentication options like `DefaultAzureCredential` or environment-based credentials. - -## Running the Examples - -Each example can be run independently. Navigate to this directory and run any example: - -```bash -python azure_ai_basic.py -python azure_ai_with_code_interpreter.py -# ... etc -``` - -The examples demonstrate various patterns for working with Azure AI agents, from basic usage to advanced scenarios like session management and structured outputs. -# Azure AI Agent Examples - -This folder contains examples demonstrating different ways to create and use agents with Azure AI using the `AzureAIAgentsProvider` from the `agent_framework.azure` package. These examples use the `azure-ai-agents` 1.x (V1) API surface. For updated V2 (`azure-ai-projects` 2.x) samples, see the [Azure AI V2 examples folder](../azure_ai/). - -## Provider Pattern - -All examples in this folder use the `AzureAIAgentsProvider` class which provides a high-level interface for agent operations: - -- **`create_agent()`** - Create a new agent on the Azure AI service -- **`get_agent()`** - Retrieve an existing agent by ID or from a pre-fetched Agent object -- **`as_agent()`** - Wrap an SDK Agent object as a Agent without HTTP calls - -```python -from agent_framework.azure import AzureAIAgentsProvider -from azure.identity.aio import AzureCliCredential - -async with ( - AzureCliCredential() as credential, - AzureAIAgentsProvider(credential=credential) as provider, -): - agent = await provider.create_agent( - name="MyAgent", - instructions="You are a helpful assistant.", - tools=my_function, - ) - result = await agent.run("Hello!") -``` - -## Examples +| [`foundry_chat_client.py`](foundry_chat_client.py) | Azure OpenAI Responses Client with Foundry Project Example | +| [`foundry_chat_client_basic.py`](foundry_chat_client_basic.py) | Azure OpenAI Chat Client Basic Example | +| [`foundry_chat_client_code_interpreter_files.py`](foundry_chat_client_code_interpreter_files.py) | Azure OpenAI Responses Client with Code Interpreter and Files Example | +| [`foundry_chat_client_image_analysis.py`](foundry_chat_client_image_analysis.py) | | +| [`foundry_chat_client_with_code_interpreter.py`](foundry_chat_client_with_code_interpreter.py) | """ | +| [`foundry_chat_client_with_explicit_settings.py`](foundry_chat_client_with_explicit_settings.py) | Azure OpenAI Chat Client with Explicit Settings Example | +| [`foundry_chat_client_with_file_search.py`](foundry_chat_client_with_file_search.py) | Azure OpenAI Responses Client with File Search Example | +| [`foundry_chat_client_with_function_tools.py`](foundry_chat_client_with_function_tools.py) | Azure OpenAI Chat Client with Function Tools Example | +| [`foundry_chat_client_with_hosted_mcp.py`](foundry_chat_client_with_hosted_mcp.py) | | +| [`foundry_chat_client_with_local_mcp.py`](foundry_chat_client_with_local_mcp.py) | | +| [`foundry_chat_client_with_session.py`](foundry_chat_client_with_session.py) | Azure OpenAI Chat Client with Session Management Example | + +## OpenAI ChatCompletionClient with Azure OpenAI Samples | File | Description | |------|-------------| -| [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive example demonstrating all `AzureAIAgentsProvider` methods: `create_agent()`, `get_agent()`, `as_agent()`, and managing multiple agents from a single provider. | -| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIAgentsProvider`. It automatically handles all configuration using environment variables. Shows both streaming and non-streaming responses. | -| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to find real-time information from the web using custom search configurations. Demonstrates how to use `AzureAIAgentClient.get_web_search_tool()` with custom search instances. | -| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to find real-time information from the web. Demonstrates `AzureAIAgentClient.get_web_search_tool()` with proper source citations and comprehensive error handling. | -| [`azure_ai_with_bing_grounding_citations.py`](azure_ai_with_bing_grounding_citations.py) | Demonstrates how to extract and display citations from Bing Grounding search responses. Shows how to collect citation annotations (title, URL, snippet) during streaming responses, enabling users to verify sources and access referenced content. | -| [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. | -| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use `AzureAIAgentClient.get_code_interpreter_tool()` with Azure AI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | -| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with an existing SDK Agent object using `provider.as_agent()`. This wraps the agent without making HTTP calls. | -| [`azure_ai_with_existing_session.py`](azure_ai_with_existing_session.py) | Shows how to work with a pre-existing session by providing the session ID. Demonstrates proper cleanup of manually created sessions. | -| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured provider settings, including project endpoint and model deployment name. | -| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Demonstrates how to use Azure AI Search with Azure AI agents. Shows how to create an agent with search tools using the SDK directly and wrap it with `provider.get_agent()`. | -| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Demonstrates how to use `AzureAIAgentClient.get_file_search_tool()` with Azure AI agents to search through uploaded documents. Shows file upload, vector store creation, and querying document content. | -| [`azure_ai_with_function_tools.py`](azure_ai_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | -| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to use `AzureAIAgentClient.get_mcp_tool()` with hosted Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates remote MCP server connections and tool discovery. | -| [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate Azure AI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates both agent-level and run-level tool configuration. | -| [`azure_ai_with_multiple_tools.py`](azure_ai_with_multiple_tools.py) | Demonstrates how to use multiple tools together with Azure AI agents, including web search, MCP servers, and function tools using client static methods. Shows coordinated multi-tool interactions and approval workflows. | -| [`azure_ai_with_openapi_tools.py`](azure_ai_with_openapi_tools.py) | Demonstrates how to use OpenAPI tools with Azure AI agents to integrate external REST APIs. Shows OpenAPI specification loading, anonymous authentication, session context management, and coordinated multi-API conversations. | -| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Demonstrates how to use structured outputs with Azure AI agents using Pydantic models. | -| [`azure_ai_with_session.py`](azure_ai_with_session.py) | Demonstrates session management with Azure AI agents, including automatic session creation for stateless conversations and explicit session management for maintaining conversation context across multiple interactions. | - -## Environment Variables - -Before running the examples, you need to set up your environment variables. You can do this in one of two ways: - -### Option 1: Using a .env file (Recommended) - -1. Copy the `.env.example` file from the `python` directory to create a `.env` file: - ```bash - cp ../../.env.example ../../.env - ``` - -2. Edit the `.env` file and add your values: - ``` - AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" - AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" - ``` - -3. For samples using Bing Grounding search (like `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`), you'll also need: - ``` - BING_CONNECTION_ID="your-bing-connection-id" - ``` - - To get your Bing connection details: - - Go to [Azure AI Foundry portal](https://ai.azure.com) - - Navigate to your project's "Connected resources" section - - Add a new connection for "Grounding with Bing Search" - - Copy the ID - -4. For samples using Bing Custom Search (like `azure_ai_with_bing_custom_search.py`), you'll also need: - ``` - BING_CUSTOM_CONNECTION_ID="your-bing-custom-connection-id" - BING_CUSTOM_INSTANCE_NAME="your-bing-custom-instance-name" - ``` - - To get your Bing Custom Search connection details: - - Go to [Azure AI Foundry portal](https://ai.azure.com) - - Navigate to your project's "Connected resources" section - - Add a new connection for "Grounding with Bing Custom Search" - - Copy the connection ID and instance name - -### Option 2: Using environment variables directly - -Set the environment variables in your shell: - -```bash -export AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint" -export AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name" -export BING_CONNECTION_ID="your-bing-connection-id" -export BING_CUSTOM_CONNECTION_ID="your-bing-custom-connection-id" -export BING_CUSTOM_INSTANCE_NAME="your-bing-custom-instance-name" -``` - -### Required Variables - -- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint (required for all examples) -- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (required for all examples) - -### Optional Variables - -- `BING_CONNECTION_ID`: Your Bing connection ID (required for `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`) -- `BING_CUSTOM_CONNECTION_ID`: Your Bing Custom Search connection ID (required for `azure_ai_with_bing_custom_search.py`) -- `BING_CUSTOM_INSTANCE_NAME`: Your Bing Custom Search instance name (required for `azure_ai_with_bing_custom_search.py`) +| [`openai_chat_completion_client_azure_basic.py`](openai_chat_completion_client_azure_basic.py) | Azure OpenAI Chat Client Basic Example | +| [`openai_chat_completion_client_azure_with_explicit_settings.py`](openai_chat_completion_client_azure_with_explicit_settings.py) | Azure OpenAI Chat Client with Explicit Settings Example | +| [`openai_chat_completion_client_azure_with_function_tools.py`](openai_chat_completion_client_azure_with_function_tools.py) | Azure OpenAI Chat Client with Function Tools Example | +| [`openai_chat_completion_client_azure_with_session.py`](openai_chat_completion_client_azure_with_session.py) | Azure OpenAI Chat Client with Session Management Example | diff --git a/python/samples/semantic-kernel-migration/README.md b/python/samples/semantic-kernel-migration/README.md index 3a298fcf3d..f7da9de9c5 100644 --- a/python/samples/semantic-kernel-migration/README.md +++ b/python/samples/semantic-kernel-migration/README.md @@ -12,9 +12,6 @@ This gallery helps Semantic Kernel (SK) developers move to the Microsoft Agent F - [03_chat_completion_thread_and_stream.py](chat_completion/03_chat_completion_thread_and_stream.py) — Demonstrates session reuse and streaming prompts. ### Azure AI agent parity -- [01_basic_azure_ai_agent.py](azure_ai_agent/01_basic_azure_ai_agent.py) — Create and run an Azure AI agent end to end. -- [02_azure_ai_agent_with_code_interpreter.py](azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py) — Enable hosted code interpreter/tool execution. -- [03_azure_ai_agent_threads_and_followups.py](azure_ai_agent/03_azure_ai_agent_threads_and_followups.py) — Persist sessions and follow-ups across invocations. ### OpenAI Assistants API parity - [01_basic_openai_assistant.py](openai_assistant/01_basic_openai_assistant.py) — Baseline assistant comparison. From 33bf7a44c0b8cc6c2d481ab4e5de9de18706119b Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 20 Mar 2026 20:14:32 +0100 Subject: [PATCH 05/13] fixes --- .../azure-ai/agent_framework_azure_ai/_foundry_agent.py | 3 ++- .../packages/core/agent_framework/_workflows/_events.py | 2 +- .../packages/lab/lightning/samples/train_math_agent.py | 5 +++-- .../agent_framework_orchestrations/_sequential.py | 5 +++-- .../context_providers/redis/azure_redis_conversation.py | 6 ++---- .../context_providers/redis/redis_conversation.py | 7 +++---- .../02-agents/context_providers/redis/redis_sessions.py | 7 +++---- .../declarative/agent_to_function_tool/main.py | 5 ++++- .../03-workflows/declarative/customer_support/main.py | 2 +- .../03-workflows/declarative/deep_research/main.py | 7 +++---- .../samples/03-workflows/declarative/marketing/main.py | 7 +++---- .../03-workflows/declarative/student_teacher/main.py | 7 +++---- .../azure_functions/01_single_agent/function_app.py | 6 ++---- .../function_app.py | 7 +++---- .../function_app.py | 7 +++---- .../07_single_agent_orchestration_hitl/function_app.py | 7 +++---- .../azure_functions/08_mcp_server/function_app.py | 7 +++---- .../hosted_agents/agent_with_local_tools/main.py | 5 +---- .../single_agent/01_basic_assistant_agent.py | 6 +----- .../single_agent/02_assistant_agent_with_tool.py | 2 +- .../single_agent/03_assistant_agent_thread_and_stream.py | 2 +- .../02_openai_assistant_with_code_interpreter.py | 2 +- .../orchestrations/group_chat.py | 3 +-- .../semantic-kernel-migration/orchestrations/handoff.py | 9 ++++----- .../orchestrations/sequential.py | 5 +++-- 25 files changed, 58 insertions(+), 73 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py index 51c476c192..4c4fc579aa 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py @@ -39,11 +39,12 @@ if TYPE_CHECKING: from agent_framework._middleware import MiddlewareTypes from agent_framework._tools import FunctionTool + from agent_framework_openai._chat_client import OpenAIChatOptions FoundryAgentOptionsT = TypeVar( "FoundryAgentOptionsT", bound=TypedDict, # type: ignore[valid-type] - default="OpenAIChatOptions", # noqa: F821 # pyright: ignore[reportUndefinedVariable] # type: ignore[name-defined] + default="OpenAIChatOptions", covariant=True, ) diff --git a/python/packages/core/agent_framework/_workflows/_events.py b/python/packages/core/agent_framework/_workflows/_events.py index c4694bf31b..d26952d8e5 100644 --- a/python/packages/core/agent_framework/_workflows/_events.py +++ b/python/packages/core/agent_framework/_workflows/_events.py @@ -121,7 +121,7 @@ def from_exception( "executor_completed", # Executor handler completed (use .executor_id, .data) "executor_failed", # Executor handler raised error (use .executor_id, .details) # Orchestration event types (use .data for typed payload) - "group_chat", # Group chat orchestrator events (use .data as GroupChatRequestSentEvent | GroupChatResponseReceivedEvent) # noqa: E501 + "group_chat", # Group chat orchestrator events (use .data as GroupChatRequestSentEvent | GroupChatResponseReceivedEvent) # noqa: E501 "handoff_sent", # Handoff routing events (use .data as HandoffSentEvent) "magentic_orchestrator", # Magentic orchestrator events (use .data as MagenticOrchestratorEvent) ] diff --git a/python/packages/lab/lightning/samples/train_math_agent.py b/python/packages/lab/lightning/samples/train_math_agent.py index f702b5a631..98e79484bc 100644 --- a/python/packages/lab/lightning/samples/train_math_agent.py +++ b/python/packages/lab/lightning/samples/train_math_agent.py @@ -140,8 +140,9 @@ def evaluate(result: AgentResponse, ground_truth: str) -> float: AGENT_INSTRUCTION = """ Solve the following math problem. Use the calculator tool to help you calculate math expressions. -Output the answer when you are ready. The answer should be after three sharps (`###`), with no extra punctuations or texts. For example: ### 123 -""".strip() # noqa: E501 +Output the answer when you are ready. The answer should be after three sharps (`###`), +with no extra punctuations or texts. For example: ### 123 +""".strip() # The @rollout decorator is the key integration point with agent-lightning. diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py b/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py index 1ccfed8f49..bda7f194ab 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py @@ -11,7 +11,8 @@ - The workflow finishes with the final context produced by the last participant Typical wiring: - input -> _InputToConversation -> participant1 -> (agent? -> _ResponseToConversation) -> ... -> participantN -> _EndWithConversation + input -> _InputToConversation -> participant1 -> (agent? -> _ResponseToConversation) -> + ... -> participantN -> _EndWithConversation Notes: - Participants can mix SupportsAgentRun and Executor objects @@ -34,7 +35,7 @@ observable (ExecutorInvoke/Completed events), and easily testable/reusable. Their IDs are deterministic and self-describing (for example, "to-conversation:writer") to reduce event-log confusion and to mirror how the concurrent builder uses explicit dispatcher/aggregator nodes. -""" # noqa: E501 +""" import logging from collections.abc import Sequence diff --git a/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py b/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py index 007dc8ce14..3d1a18f68e 100644 --- a/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py +++ b/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """Azure Managed Redis History Provider with Azure AD Authentication @@ -26,15 +25,14 @@ import asyncio import os +from agent_framework import Agent from agent_framework.azure import FoundryChatClient from agent_framework.redis import RedisHistoryProvider from azure.identity import AzureCliCredential from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential -from dotenv import load_dotenv from redis.credentials import CredentialProvider -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. class AzureCredentialProvider(CredentialProvider): diff --git a/python/samples/02-agents/context_providers/redis/redis_conversation.py b/python/samples/02-agents/context_providers/redis/redis_conversation.py index f7edeeb4ba..7197e2ea7a 100644 --- a/python/samples/02-agents/context_providers/redis/redis_conversation.py +++ b/python/samples/02-agents/context_providers/redis/redis_conversation.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """Redis Context Provider: Basic usage and agent integration @@ -22,15 +21,15 @@ import asyncio import os +from agent_framework import Agent from agent_framework.azure import FoundryChatClient from agent_framework.redis import RedisContextProvider from azure.identity import AzureCliCredential -from dotenv import load_dotenv from redisvl.extensions.cache.embeddings import EmbeddingsCache from redisvl.utils.vectorize import OpenAITextVectorizer -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. + # Default Redis URL for local Redis Stack. # Override via the REDIS_URL environment variable for remote or authenticated instances. diff --git a/python/samples/02-agents/context_providers/redis/redis_sessions.py b/python/samples/02-agents/context_providers/redis/redis_sessions.py index 39113b1be8..1562ea88f3 100644 --- a/python/samples/02-agents/context_providers/redis/redis_sessions.py +++ b/python/samples/02-agents/context_providers/redis/redis_sessions.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """Redis Context Provider: Thread scoping examples @@ -30,15 +29,15 @@ import asyncio import os +from agent_framework import Agent from agent_framework.azure import FoundryChatClient from agent_framework.redis import RedisContextProvider from azure.identity import AzureCliCredential -from dotenv import load_dotenv from redisvl.extensions.cache.embeddings import EmbeddingsCache from redisvl.utils.vectorize import OpenAITextVectorizer -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. + # Default Redis URL for local Redis Stack. # Override via the REDIS_URL environment variable for remote or authenticated instances. diff --git a/python/samples/03-workflows/declarative/agent_to_function_tool/main.py b/python/samples/03-workflows/declarative/agent_to_function_tool/main.py index 568cacefff..34f9fd30b1 100644 --- a/python/samples/03-workflows/declarative/agent_to_function_tool/main.py +++ b/python/samples/03-workflows/declarative/agent_to_function_tool/main.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """Agent to Function Tool sample - demonstrates chaining agent output to function tools. @@ -23,11 +22,15 @@ from pathlib import Path from typing import Any +from agent_framework import Agent from agent_framework.azure import FoundryChatClient from agent_framework.declarative import WorkflowFactory from azure.identity import AzureCliCredential from pydantic import BaseModel, Field +# Copyright (c) Microsoft. All rights reserved. + + # Pricing data for the order calculation ITEM_PRICES = { "pizza": {"small": 10.99, "medium": 14.99, "large": 18.99, "default": 14.99}, diff --git a/python/samples/03-workflows/declarative/customer_support/main.py b/python/samples/03-workflows/declarative/customer_support/main.py index 4d54a5975b..c9975ddd3f 100644 --- a/python/samples/03-workflows/declarative/customer_support/main.py +++ b/python/samples/03-workflows/declarative/customer_support/main.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """ CustomerSupport workflow sample. @@ -28,6 +27,7 @@ import uuid from pathlib import Path +from agent_framework import Agent from agent_framework.azure import FoundryChatClient from agent_framework.declarative import ( AgentExternalInputRequest, diff --git a/python/samples/03-workflows/declarative/deep_research/main.py b/python/samples/03-workflows/declarative/deep_research/main.py index e2a5bf345d..a5638e07c2 100644 --- a/python/samples/03-workflows/declarative/deep_research/main.py +++ b/python/samples/03-workflows/declarative/deep_research/main.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """ DeepResearch workflow sample. @@ -26,14 +25,14 @@ import os from pathlib import Path +from agent_framework import Agent from agent_framework.azure import FoundryChatClient from agent_framework.declarative import WorkflowFactory from azure.identity import AzureCliCredential -from dotenv import load_dotenv from pydantic import BaseModel, Field -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. + # Agent Instructions RESEARCH_INSTRUCTIONS = """In order to help begin addressing the user request, please answer the following pre-survey to the best of your ability. diff --git a/python/samples/03-workflows/declarative/marketing/main.py b/python/samples/03-workflows/declarative/marketing/main.py index 6de42bd83f..761632038a 100644 --- a/python/samples/03-workflows/declarative/marketing/main.py +++ b/python/samples/03-workflows/declarative/marketing/main.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """ Run the marketing copy workflow sample. @@ -17,13 +16,13 @@ import os from pathlib import Path +from agent_framework import Agent from agent_framework.azure import FoundryChatClient from agent_framework.declarative import WorkflowFactory from azure.identity import AzureCliCredential -from dotenv import load_dotenv -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. + ANALYST_INSTRUCTIONS = """You are a product analyst. Analyze the given product and identify: 1. Key features and benefits diff --git a/python/samples/03-workflows/declarative/student_teacher/main.py b/python/samples/03-workflows/declarative/student_teacher/main.py index e8f99c139d..58a01eb03b 100644 --- a/python/samples/03-workflows/declarative/student_teacher/main.py +++ b/python/samples/03-workflows/declarative/student_teacher/main.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """ Run the student-teacher (MathChat) workflow sample. @@ -24,13 +23,13 @@ import os from pathlib import Path +from agent_framework import Agent from agent_framework.azure import FoundryChatClient from agent_framework.declarative import WorkflowFactory from azure.identity import AzureCliCredential -from dotenv import load_dotenv -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. + STUDENT_INSTRUCTIONS = """You are a curious math student working on understanding mathematical concepts. When given a problem: diff --git a/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py b/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py index a1f45d64fa..0367cd1678 100644 --- a/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py +++ b/python/samples/04-hosting/azure_functions/01_single_agent/function_app.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """Host a single Azure OpenAI-powered agent inside Azure Functions. @@ -11,12 +10,11 @@ from typing import Any +from agent_framework import Agent from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.identity import AzureCliCredential -from dotenv import load_dotenv -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. # 1. Instantiate the agent with the chosen deployment and instructions. diff --git a/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py b/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py index 6e19a00555..acc27a6f7f 100644 --- a/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py +++ b/python/samples/04-hosting/azure_functions/04_single_agent_orchestration_chaining/function_app.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """Chain two runs of a single agent inside a Durable Functions orchestration. @@ -17,13 +16,13 @@ from typing import Any import azure.functions as func +from agent_framework import Agent from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext from azure.identity import AzureCliCredential -from dotenv import load_dotenv -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. + logger = logging.getLogger(__name__) diff --git a/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py b/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py index 1cdaa4165f..87eea97a46 100644 --- a/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py +++ b/python/samples/04-hosting/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """Route email requests through conditional orchestration with two agents. @@ -18,14 +17,14 @@ from typing import Any import azure.functions as func +from agent_framework import Agent from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext from azure.identity import AzureCliCredential -from dotenv import load_dotenv from pydantic import BaseModel, ValidationError -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. + logger = logging.getLogger(__name__) diff --git a/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py b/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py index 2bf75e0a74..40359b9a23 100644 --- a/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py +++ b/python/samples/04-hosting/azure_functions/07_single_agent_orchestration_hitl/function_app.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """Iterate on generated content with a human-in-the-loop Durable orchestration. @@ -18,14 +17,14 @@ from typing import Any import azure.functions as func +from agent_framework import Agent from agent_framework.azure import AgentFunctionApp, FoundryChatClient from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext from azure.identity import AzureCliCredential -from dotenv import load_dotenv from pydantic import BaseModel, ValidationError -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. + logger = logging.getLogger(__name__) diff --git a/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py b/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py index dde11bbdf5..5d92bbbb23 100644 --- a/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py +++ b/python/samples/04-hosting/azure_functions/08_mcp_server/function_app.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """ Example showing how to configure AI agents with different trigger configurations. @@ -25,11 +24,11 @@ Authentication uses AzureCliCredential (Azure Identity). """ +from agent_framework import Agent from agent_framework.azure import AgentFunctionApp, FoundryChatClient -from dotenv import load_dotenv -# Load environment variables from .env file -load_dotenv() +# Copyright (c) Microsoft. All rights reserved. + # Create Azure OpenAI Chat Client # This uses AzureCliCredential for authentication (requires 'az login') diff --git a/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py b/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py index f513708261..ae42b8afa8 100644 --- a/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py +++ b/python/samples/05-end-to-end/hosted_agents/agent_with_local_tools/main.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import Agent """ Seattle Hotel Agent - A simple agent with a tool to find hotels in Seattle. @@ -12,12 +11,10 @@ from datetime import datetime from typing import Annotated +from agent_framework import Agent from agent_framework.azure import FoundryChatClient from azure.ai.agentserver.agentframework import from_agent_framework from azure.identity.aio import AzureCliCredential, ManagedIdentityCredential -from dotenv import load_dotenv - -load_dotenv(override=True) # Configure these for your Foundry project # Read the explicit variables present in the .env file diff --git a/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py b/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py index 4cf093fa9c..6a89e25bf4 100644 --- a/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py +++ b/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py @@ -1,5 +1,4 @@ # /// script -from agent_framework import Agent # requires-python = ">=3.10" # dependencies = [ @@ -20,10 +19,7 @@ import asyncio -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() +from agent_framework import Agent async def run_autogen() -> None: diff --git a/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py b/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py index 97432e9b98..89629a9fb7 100644 --- a/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py +++ b/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py @@ -68,7 +68,7 @@ async def run_agent_framework() -> None: from agent_framework.openai import OpenAIChatClient # Define tool with @tool decorator (automatic schema inference) - # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. + # NOTE: approval_mode="never_require" is for sample brevity. @tool(approval_mode="never_require") def get_weather(location: str) -> str: """Get the weather for a location. diff --git a/python/samples/autogen-migration/single_agent/03_assistant_agent_thread_and_stream.py b/python/samples/autogen-migration/single_agent/03_assistant_agent_thread_and_stream.py index f4a2203371..b9214c2c26 100644 --- a/python/samples/autogen-migration/single_agent/03_assistant_agent_thread_and_stream.py +++ b/python/samples/autogen-migration/single_agent/03_assistant_agent_thread_and_stream.py @@ -1,5 +1,4 @@ # /// script -from agent_framework import Agent # requires-python = ">=3.10" # dependencies = [ @@ -18,6 +17,7 @@ import asyncio +from agent_framework import Agent from dotenv import load_dotenv # Load environment variables from .env file diff --git a/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py b/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py index 09b44d0134..f968f7851e 100644 --- a/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py +++ b/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py @@ -1,5 +1,4 @@ # /// script -from agent_framework import Agent # requires-python = ">=3.10" # dependencies = [ @@ -14,6 +13,7 @@ import asyncio +from agent_framework import Agent from dotenv import load_dotenv # Load environment variables from .env file diff --git a/python/samples/semantic-kernel-migration/orchestrations/group_chat.py b/python/samples/semantic-kernel-migration/orchestrations/group_chat.py index a59dbb5aad..f0467b838d 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/group_chat.py +++ b/python/samples/semantic-kernel-migration/orchestrations/group_chat.py @@ -130,8 +130,7 @@ async def should_terminate(self, chat_history: ChatHistory) -> BooleanResult: chat_history, settings=PromptExecutionSettings(response_format=BooleanResult), ) - result = BooleanResult.model_validate_json(response.content) - return result + return BooleanResult.model_validate_json(response.content) @override async def select_next_agent( diff --git a/python/samples/semantic-kernel-migration/orchestrations/handoff.py b/python/samples/semantic-kernel-migration/orchestrations/handoff.py index b35fc8ee19..a9e7aadc8a 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/handoff.py +++ b/python/samples/semantic-kernel-migration/orchestrations/handoff.py @@ -13,7 +13,6 @@ import asyncio import sys from collections.abc import AsyncIterable, Iterator, Sequence -from typing import cast from agent_framework import ( Agent, @@ -24,7 +23,8 @@ from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv -from semantic_kernel.agents import Agent, ChatCompletionAgent, HandoffOrchestration, OrchestrationHandoffs +from semantic_kernel.agents import Agent as SKAgent +from semantic_kernel.agents import ChatCompletionAgent, HandoffOrchestration, OrchestrationHandoffs from semantic_kernel.agents.runtime import InProcessRuntime from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion from semantic_kernel.contents import ( @@ -75,7 +75,7 @@ def process_return(self, order_id: str, reason: str) -> str: return f"Return for order {order_id} has been processed successfully (reason: {reason})." -def build_semantic_kernel_agents() -> tuple[list[Agent], OrchestrationHandoffs]: +def build_semantic_kernel_agents() -> tuple[list[SKAgent], OrchestrationHandoffs]: credential = AzureCliCredential() triage = ChatCompletionAgent( @@ -240,8 +240,7 @@ def _collect_handoff_requests(events: list[WorkflowEvent]) -> list[WorkflowEvent def _extract_final_conversation(events: list[WorkflowEvent]) -> list[Message]: for event in events: if event.type == "output": - data = cast(list[Message], event.data) - return data + return event.data return [] diff --git a/python/samples/semantic-kernel-migration/orchestrations/sequential.py b/python/samples/semantic-kernel-migration/orchestrations/sequential.py index 633c854986..fd9794fdfe 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/sequential.py +++ b/python/samples/semantic-kernel-migration/orchestrations/sequential.py @@ -20,7 +20,8 @@ from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential from dotenv import load_dotenv -from semantic_kernel.agents import Agent, ChatCompletionAgent, SequentialOrchestration +from semantic_kernel.agents import Agent as SKAgent +from semantic_kernel.agents import ChatCompletionAgent, SequentialOrchestration from semantic_kernel.agents.runtime import InProcessRuntime from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion from semantic_kernel.contents import ChatMessageContent @@ -36,7 +37,7 @@ ###################################################################### -def build_semantic_kernel_agents() -> list[Agent]: +def build_semantic_kernel_agents() -> list[SKAgent]: credential = AzureCliCredential() writer_agent = ChatCompletionAgent( From 4b02e97994baed4db7f07179ea3988a7695744e3 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 20 Mar 2026 20:52:35 +0100 Subject: [PATCH 06/13] updated observabilitty --- .../_foundry_agent.py | 60 +++++++++ .../_foundry_chat_client.py | 119 ++++++++++++------ .../samples/02-agents/observability/README.md | 44 +++---- .../observability/advanced_zero_code.py | 4 +- .../agent_with_foundry_tracing.py | 107 ---------------- .../configure_otel_providers_with_env_var.py | 4 +- ...onfigure_otel_providers_with_parameters.py | 4 +- .../observability/foundry_tracing.py | 94 ++++++++++++++ 8 files changed, 262 insertions(+), 174 deletions(-) delete mode 100644 python/samples/02-agents/observability/agent_with_foundry_tracing.py create mode 100644 python/samples/02-agents/observability/foundry_tracing.py diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py index 4c4fc579aa..878f3deed0 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent.py @@ -9,6 +9,7 @@ from __future__ import annotations +import logging import sys from collections.abc import Callable, Sequence from typing import TYPE_CHECKING, Any @@ -41,6 +42,8 @@ from agent_framework._tools import FunctionTool from agent_framework_openai._chat_client import OpenAIChatOptions +logger: logging.Logger = logging.getLogger("agent_framework.azure") + FoundryAgentOptionsT = TypeVar( "FoundryAgentOptionsT", bound=TypedDict, # type: ignore[valid-type] @@ -133,6 +136,63 @@ def __init__( **kwargs, ) + async def configure_azure_monitor( + self, + enable_sensitive_data: bool = False, + **kwargs: Any, + ) -> None: + """Setup observability with Azure Monitor (Microsoft Foundry integration). + + This method configures Azure Monitor for telemetry collection using the + connection string from the Foundry project client (accessed via the internal client). + + Args: + enable_sensitive_data: Enable sensitive data logging (prompts, responses). + Should only be enabled in development/test environments. Default is False. + **kwargs: Additional arguments passed to configure_azure_monitor(). + + Raises: + ImportError: If azure-monitor-opentelemetry-exporter is not installed. + """ + from azure.core.exceptions import ResourceNotFoundError + + from ._foundry_agent_client import RawFoundryAgentChatClient + + client = self.client + if not isinstance(client, RawFoundryAgentChatClient): + raise TypeError("configure_azure_monitor requires a RawFoundryAgentChatClient-based client.") + + try: + conn_string = await client.project_client.telemetry.get_application_insights_connection_string() + except ResourceNotFoundError: + logger.warning( + "No Application Insights connection string found for the Foundry project. " + "Please ensure Application Insights is configured in your project, " + "or call configure_otel_providers() manually with custom exporters." + ) + return + + try: + from azure.monitor.opentelemetry import configure_azure_monitor # type: ignore[import] + except ImportError as exc: + raise ImportError( + "azure-monitor-opentelemetry is required for Azure Monitor integration. " + "Install it with: pip install azure-monitor-opentelemetry" + ) from exc + + from agent_framework.observability import create_metric_views, create_resource, enable_instrumentation + + if "resource" not in kwargs: + kwargs["resource"] = create_resource() + + configure_azure_monitor( + connection_string=conn_string, + views=create_metric_views(), + **kwargs, + ) + + enable_instrumentation(enable_sensitive_data=enable_sensitive_data) + class FoundryAgent( # type: ignore[misc] AgentTelemetryLayer, diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_chat_client.py index bd3250cc64..6cb50c762f 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_chat_client.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging import sys from collections.abc import Sequence from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal @@ -48,7 +49,8 @@ FunctionMiddleware, FunctionMiddlewareCallable, ) - from openai import AsyncOpenAI + +logger: logging.Logger = logging.getLogger("agent_framework.azure") class FoundrySettings(TypedDict, total=False): @@ -134,56 +136,38 @@ def __init__( if not resolved_model: raise ValueError("Model is required. Set via 'model' parameter or 'FOUNDRY_MODEL' environment variable.") - resolved_endpoint = foundry_settings.get("project_endpoint") + project_endpoint = foundry_settings.get("project_endpoint") - if resolved_endpoint is None and project_client is None: + if project_endpoint is None and project_client is None: raise ValueError( "Either 'project_endpoint' or 'project_client' is required. " "Set project_endpoint via parameter or 'FOUNDRY_PROJECT_ENDPOINT' environment variable." ) - - async_client = self._create_client_from_project( - project_client=project_client, - project_endpoint=resolved_endpoint, - credential=credential, - allow_preview=allow_preview, - ) + if not project_client: + if not project_endpoint: + raise ValueError( + "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " + "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable," + "or pass in a AIProjectClient." + ) + if not credential: + raise ValueError("Azure credential is required when using project_endpoint without a project_client.") + project_client_kwargs: dict[str, Any] = { + "endpoint": project_endpoint, + "credential": credential, # type: ignore[arg-type] + "user_agent": AGENT_FRAMEWORK_USER_AGENT, + } + if allow_preview is not None: + project_client_kwargs["allow_preview"] = allow_preview + project_client = AIProjectClient(**project_client_kwargs) super().__init__( model=resolved_model, - async_client=async_client, + async_client=project_client.get_openai_client(), instruction_role=instruction_role, **kwargs, ) - - @staticmethod - def _create_client_from_project( - *, - project_client: AIProjectClient | None, - project_endpoint: str | None, - credential: AzureCredentialTypes | AzureTokenProvider | None, - allow_preview: bool | None = None, - ) -> AsyncOpenAI: - """Create an AsyncOpenAI client from a Foundry project.""" - if project_client is not None: - return project_client.get_openai_client() - - if not project_endpoint: - raise ValueError( - "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " - "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." - ) - if not credential: - raise ValueError("Azure credential is required when using project_endpoint without a project_client.") - project_client_kwargs: dict[str, Any] = { - "endpoint": project_endpoint, - "credential": credential, # type: ignore[arg-type] - "user_agent": AGENT_FRAMEWORK_USER_AGENT, - } - if allow_preview is not None: - project_client_kwargs["allow_preview"] = allow_preview - project_client = AIProjectClient(**project_client_kwargs) - return project_client.get_openai_client() + self.project_client = project_client @override def _check_model_presence(self, options: dict[str, Any]) -> None: @@ -192,6 +176,61 @@ def _check_model_presence(self, options: dict[str, Any]) -> None: raise ValueError("model must be a non-empty string") options["model"] = self.model + async def configure_azure_monitor( + self, + enable_sensitive_data: bool = False, + **kwargs: Any, + ) -> None: + """Setup observability with Azure Monitor (Microsoft Foundry integration). + + This method configures Azure Monitor for telemetry collection using the + connection string from the Foundry project client. + + Args: + enable_sensitive_data: Enable sensitive data logging (prompts, responses). + Should only be enabled in development/test environments. Default is False. + **kwargs: Additional arguments passed to configure_azure_monitor(). + Common options include: + - enable_live_metrics (bool): Enable Azure Monitor Live Metrics + - credential (TokenCredential): Azure credential for Entra ID auth + - resource (Resource): Custom OpenTelemetry resource + + Raises: + ImportError: If azure-monitor-opentelemetry-exporter is not installed. + """ + from azure.core.exceptions import ResourceNotFoundError + + try: + conn_string = await self.project_client.telemetry.get_application_insights_connection_string() + except ResourceNotFoundError: + logger.warning( + "No Application Insights connection string found for the Foundry project. " + "Please ensure Application Insights is configured in your project, " + "or call configure_otel_providers() manually with custom exporters." + ) + return + + try: + from azure.monitor.opentelemetry import configure_azure_monitor # type: ignore[import] + except ImportError as exc: + raise ImportError( + "azure-monitor-opentelemetry is required for Azure Monitor integration. " + "Install it with: pip install azure-monitor-opentelemetry" + ) from exc + + from agent_framework.observability import create_metric_views, create_resource, enable_instrumentation + + if "resource" not in kwargs: + kwargs["resource"] = create_resource() + + configure_azure_monitor( + connection_string=conn_string, + views=create_metric_views(), + **kwargs, + ) + + enable_instrumentation(enable_sensitive_data=enable_sensitive_data) + # region Tool factory methods (override OpenAI defaults with Foundry versions) @staticmethod diff --git a/python/samples/02-agents/observability/README.md b/python/samples/02-agents/observability/README.md index 7d210d434a..edff911e07 100644 --- a/python/samples/02-agents/observability/README.md +++ b/python/samples/02-agents/observability/README.md @@ -88,18 +88,20 @@ configure_azure_monitor( # This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars enable_instrumentation(enable_sensitive_data=False) ``` -For Azure AI projects, use the `client.configure_azure_monitor()` method which wraps the calls to `configure_azure_monitor()` and `enable_instrumentation()`: +For Microsoft Foundry projects, use `client.configure_azure_monitor()` which retrieves the connection string from the project and configures everything: ```python -from agent_framework.azure import AzureAIClient -from azure.ai.projects.aio import AIProjectClient - -async with ( - AIProjectClient(...) as project_client, - AzureAIClient(project_client=project_client) as client, -): - # Automatically configures Azure Monitor with connection string from project - await client.configure_azure_monitor(enable_live_metrics=True) +from agent_framework.azure import FoundryChatClient +from azure.identity import AzureCliCredential + +client = FoundryChatClient( + project_endpoint="https://your-project.services.ai.azure.com", + model="gpt-4o", + credential=AzureCliCredential(), +) + +# Automatically configures Azure Monitor with connection string from project +await client.configure_azure_monitor(enable_sensitive_data=True) ``` Or with [Langfuse](https://langfuse.com/integrations/frameworks/microsoft-agent-framework): @@ -227,8 +229,7 @@ This folder contains different samples demonstrating how to use telemetry in var | [configure_otel_providers_with_parameters.py](./configure_otel_providers_with_parameters.py) | **Recommended starting point**: Shows how to create custom exporters with specific configuration and pass them to `configure_otel_providers()`. Useful for advanced scenarios. | | [configure_otel_providers_with_env_var.py](./configure_otel_providers_with_env_var.py) | Shows how to setup telemetry using standard OpenTelemetry environment variables (`OTEL_EXPORTER_OTLP_*`). | | [agent_observability.py](./agent_observability.py) | Shows telemetry collection for an agentic application with tool calls using environment variables. | -| [agent_with_foundry_tracing.py](./agent_with_foundry_tracing.py) | Shows Azure Monitor integration with Foundry for any chat client. | -| [azure_ai_agent_observability.py](./azure_ai_agent_observability.py) | Shows Azure Monitor integration for a AzureAIClient. | +| [foundry_tracing.py](./foundry_tracing.py) | Shows Azure Monitor integration with Foundry for any chat client. | | [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) | Advanced: Shows manual setup of exporters and providers with console output. Useful for understanding how observability works under the hood. | | [advanced_zero_code.py](./advanced_zero_code.py) | Advanced: Shows zero-code telemetry setup using the `opentelemetry-enable_instrumentation` CLI tool. | | [workflow_observability.py](./workflow_observability.py) | Shows telemetry collection for a workflow with multiple executors and message passing. | @@ -347,15 +348,16 @@ setup_observability( **After (Current):** ```python -# For Azure AI projects -from agent_framework.azure import AzureAIClient -from azure.ai.projects.aio import AIProjectClient - -async with ( - AIProjectClient(...) as project_client, - AzureAIClient(project_client=project_client) as client, -): - await client.configure_azure_monitor(enable_live_metrics=True) +# For Microsoft Foundry projects +from agent_framework.azure import FoundryChatClient +from azure.identity import AzureCliCredential + +client = FoundryChatClient( + project_endpoint="https://your-project.services.ai.azure.com", + model="gpt-4o", + credential=AzureCliCredential(), +) +await client.configure_azure_monitor(enable_live_metrics=True) # For non-Azure AI projects from azure.monitor.opentelemetry import configure_azure_monitor diff --git a/python/samples/02-agents/observability/advanced_zero_code.py b/python/samples/02-agents/observability/advanced_zero_code.py index 981b14a0e6..d2c2c41cb3 100644 --- a/python/samples/02-agents/observability/advanced_zero_code.py +++ b/python/samples/02-agents/observability/advanced_zero_code.py @@ -5,8 +5,8 @@ from typing import TYPE_CHECKING, Annotated from agent_framework import Message, tool +from agent_framework.azure import FoundryChatClient from agent_framework.observability import get_tracer -from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv from opentelemetry.trace import SpanKind from opentelemetry.trace.span import format_trace_id @@ -103,7 +103,7 @@ async def main() -> None: with get_tracer().start_as_current_span("Zero Code", kind=SpanKind.CLIENT) as current_span: print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}") - client = OpenAIResponsesClient() + client = FoundryChatClient() await run_chat_client(client, stream=True) await run_chat_client(client, stream=False) diff --git a/python/samples/02-agents/observability/agent_with_foundry_tracing.py b/python/samples/02-agents/observability/agent_with_foundry_tracing.py deleted file mode 100644 index 34177d1215..0000000000 --- a/python/samples/02-agents/observability/agent_with_foundry_tracing.py +++ /dev/null @@ -1,107 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = [ -# "azure-monitor-opentelemetry", -# ] -# /// -# Run with any PEP 723 compatible runner, e.g.: -# uv run python/samples/02-agents/observability/agent_with_foundry_tracing.py - -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import logging -import os -from random import randint -from typing import Annotated - -from agent_framework import Agent, tool -from agent_framework.observability import create_resource, enable_instrumentation, get_tracer -from agent_framework.openai import OpenAIResponsesClient -from azure.ai.projects.aio import AIProjectClient -from azure.identity.aio import AzureCliCredential -from azure.monitor.opentelemetry import configure_azure_monitor -from dotenv import load_dotenv -from opentelemetry.trace import SpanKind -from opentelemetry.trace.span import format_trace_id -from pydantic import Field - -""" -This sample shows you can can setup telemetry in Microsoft Foundry for a custom agent. -First ensure you have a Foundry workspace with Application Insights enabled. -And use the Operate tab to Register an Agent. -Set the OpenTelemetry agent ID to the value used below in the Agent creation: `weather-agent` (or change both). -The sample uses the Azure Monitor OpenTelemetry exporter to send traces to Application Insights. -So ensure you have the `azure-monitor-opentelemetry` package installed. -""" - -# For loading the `FOUNDRY_PROJECT_ENDPOINT` environment variable -load_dotenv() - -logger = logging.getLogger(__name__) - - -# NOTE: approval_mode="never_require" is for sample brevity. -# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py -# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. -@tool(approval_mode="never_require") -async def get_weather( - location: Annotated[str, Field(description="The location to get the weather for.")], -) -> str: - """Get the weather for a given location.""" - await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call - conditions = ["sunny", "cloudy", "rainy", "stormy"] - return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." - - -async def main(): - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=credential) as project_client, - ): - # This will enable tracing and configure the application to send telemetry data to the - # Application Insights instance attached to the Azure AI project. - # This will override any existing configuration. - try: - conn_string = await project_client.telemetry.get_application_insights_connection_string() - except Exception: - logger.warning( - "No Application Insights connection string found for the Azure AI Project. " - "Please ensure Application Insights is configured in your Azure AI project, " - "or call configure_otel_providers() manually with custom exporters." - ) - return - configure_azure_monitor( - connection_string=conn_string, - enable_live_metrics=True, - resource=create_resource(), - enable_performance_counters=False, - ) - # This call is not necessary if you have the environment variable ENABLE_INSTRUMENTATION=true set - # If not or set to false, or if you want to enable or disable sensitive data collection, call this function. - enable_instrumentation(enable_sensitive_data=True) - print("Observability is set up. Starting Weather Agent...") - - questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"] - - with get_tracer().start_as_current_span("Weather Agent Chat", kind=SpanKind.CLIENT) as current_span: - print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}") - - agent = Agent( - client=OpenAIResponsesClient(), - tools=get_weather, - name="WeatherAgent", - instructions="You are a weather assistant.", - id="weather-agent", - ) - session = agent.create_session() - for question in questions: - print(f"\nUser: {question}") - print(f"{agent.name}: ", end="") - async for update in agent.run(question, session=session, stream=True): - if update.text: - print(update.text, end="") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/02-agents/observability/configure_otel_providers_with_env_var.py b/python/samples/02-agents/observability/configure_otel_providers_with_env_var.py index 0e20c0a6f5..971a5ac736 100644 --- a/python/samples/02-agents/observability/configure_otel_providers_with_env_var.py +++ b/python/samples/02-agents/observability/configure_otel_providers_with_env_var.py @@ -7,8 +7,8 @@ from typing import TYPE_CHECKING, Annotated, Literal from agent_framework import Message, tool +from agent_framework.azure import FoundryChatClient from agent_framework.observability import configure_otel_providers, get_tracer -from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv from opentelemetry import trace from opentelemetry.trace.span import format_trace_id @@ -114,7 +114,7 @@ async def main(scenario: Literal["client", "client_stream", "tool", "all"] = "al with get_tracer().start_as_current_span("Sample Scenarios", kind=trace.SpanKind.CLIENT) as current_span: print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}") - client = OpenAIResponsesClient() + client = FoundryChatClient() # Scenarios where telemetry is collected in the SDK, from the most basic to the most complex. if scenario == "tool" or scenario == "all": diff --git a/python/samples/02-agents/observability/configure_otel_providers_with_parameters.py b/python/samples/02-agents/observability/configure_otel_providers_with_parameters.py index c811fd55ae..5e2950b9a1 100644 --- a/python/samples/02-agents/observability/configure_otel_providers_with_parameters.py +++ b/python/samples/02-agents/observability/configure_otel_providers_with_parameters.py @@ -8,8 +8,8 @@ from typing import TYPE_CHECKING, Annotated, Literal from agent_framework import Message, tool +from agent_framework.azure import FoundryChatClient from agent_framework.observability import configure_otel_providers, get_tracer -from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv from opentelemetry import trace from opentelemetry.trace.span import format_trace_id @@ -153,7 +153,7 @@ async def main(scenario: Literal["client", "client_stream", "tool", "all"] = "al with get_tracer().start_as_current_span("Sample Scenarios", kind=trace.SpanKind.CLIENT) as current_span: print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}") - client = OpenAIResponsesClient() + client = FoundryChatClient() # Scenarios where telemetry is collected in the SDK, from the most basic to the most complex. if scenario == "tool" or scenario == "all": diff --git a/python/samples/02-agents/observability/foundry_tracing.py b/python/samples/02-agents/observability/foundry_tracing.py new file mode 100644 index 0000000000..f9c2a3d86a --- /dev/null +++ b/python/samples/02-agents/observability/foundry_tracing.py @@ -0,0 +1,94 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "azure-monitor-opentelemetry", +# ] +# /// +# Run with any PEP 723 compatible runner, e.g.: +# uv run python/samples/02-agents/observability/foundry_tracing.py + +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import logging +import os +from random import randint +from typing import Annotated + +from agent_framework import Agent, tool +from agent_framework.azure import FoundryChatClient +from agent_framework.observability import get_tracer +from azure.identity import AzureCliCredential +from dotenv import load_dotenv +from opentelemetry.trace import SpanKind +from opentelemetry.trace.span import format_trace_id +from pydantic import Field + +""" +This sample shows how to setup telemetry in Microsoft Foundry for a custom agent +using ``FoundryChatClient.configure_azure_monitor()``. + +First ensure you have a Foundry workspace with Application Insights enabled. +And use the Operate tab to Register an Agent. +Set the OpenTelemetry agent ID to the value used below in the Agent creation: ``weather-agent`` +(or change both). + +Environment variables: + FOUNDRY_PROJECT_ENDPOINT — Microsoft Foundry project endpoint + FOUNDRY_MODEL — Model deployment name (e.g. gpt-4o) +""" + +load_dotenv() + +logger = logging.getLogger(__name__) + + +# NOTE: approval_mode="never_require" is for sample brevity. +@tool(approval_mode="never_require") +async def get_weather( + location: Annotated[str, Field(description="The location to get the weather for.")], +) -> str: + """Get the weather for a given location.""" + await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call + conditions = ["sunny", "cloudy", "rainy", "stormy"] + return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." + + +async def main(): + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], + credential=AzureCliCredential(), + ) + + # configure_azure_monitor() retrieves the Application Insights connection string + # from the project client and sets up tracing automatically. + await client.configure_azure_monitor( + enable_sensitive_data=True, + enable_live_metrics=True, + ) + print("Observability is set up. Starting Weather Agent...") + + questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"] + + with get_tracer().start_as_current_span("Weather Agent Chat", kind=SpanKind.CLIENT) as current_span: + print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}") + + agent = Agent( + client=client, + tools=[get_weather], + name="WeatherAgent", + instructions="You are a weather assistant.", + id="weather-agent", + ) + session = agent.create_session() + for question in questions: + print(f"\nUser: {question}") + print(f"{agent.name}: ", end="") + async for update in agent.run(question, session=session, stream=True): + if update.text: + print(update.text, end="") + + +if __name__ == "__main__": + asyncio.run(main()) From f2b57f270601266ab3a6160ea6967f5723626dd1 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 20 Mar 2026 20:55:38 +0100 Subject: [PATCH 07/13] reset azure init.pyi --- .../core/agent_framework/azure/__init__.pyi | 81 ++++++++++++++++++- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/python/packages/core/agent_framework/azure/__init__.pyi b/python/packages/core/agent_framework/azure/__init__.pyi index 8a0c1d19a7..44cc11b070 100644 --- a/python/packages/core/agent_framework/azure/__init__.pyi +++ b/python/packages/core/agent_framework/azure/__init__.pyi @@ -1,7 +1,80 @@ # Copyright (c) Microsoft. All rights reserved. -# This is a dynamic namespace — all symbols are lazily loaded from optional packages. -from typing import Any +# Type stubs for the agent_framework.azure lazy-loading namespace. +# Install the relevant packages for full type support. -def __getattr__(name: str) -> Any: ... # pyright: ignore[reportIncompleteStub] -def __dir__() -> list[str]: ... +from agent_framework_azure_ai import ( + AzureAIAgentClient, + AzureAIAgentsProvider, + AzureAIClient, + AzureAIProjectAgentOptions, + AzureAIProjectAgentProvider, + AzureAISettings, + AzureCredentialTypes, + AzureOpenAIAssistantsClient, + AzureOpenAIAssistantsOptions, + AzureOpenAIChatClient, + AzureOpenAIChatOptions, + AzureOpenAIEmbeddingClient, + AzureOpenAIResponsesClient, + AzureOpenAIResponsesOptions, + AzureOpenAISettings, + AzureTokenProvider, + AzureUserSecurityContext, + FoundryAgent, + FoundryChatClient, + FoundryMemoryProvider, + RawAzureAIClient, + RawFoundryAgent, + RawFoundryAgentChatClient, + RawFoundryChatClient, +) +from agent_framework_azure_ai_search import ( + AzureAISearchContextProvider, + AzureAISearchSettings, +) +from agent_framework_azurefunctions import AgentFunctionApp +from agent_framework_durabletask import ( + AgentCallbackContext, + AgentResponseCallbackProtocol, + DurableAIAgent, + DurableAIAgentClient, + DurableAIAgentOrchestrationContext, + DurableAIAgentWorker, +) + +__all__ = [ + "AgentCallbackContext", + "AgentFunctionApp", + "AgentResponseCallbackProtocol", + "AzureAIAgentClient", + "AzureAIAgentsProvider", + "AzureAIClient", + "AzureAIProjectAgentOptions", + "AzureAIProjectAgentProvider", + "AzureAISearchContextProvider", + "AzureAISearchSettings", + "AzureAISettings", + "AzureCredentialTypes", + "AzureOpenAIAssistantsClient", + "AzureOpenAIAssistantsOptions", + "AzureOpenAIChatClient", + "AzureOpenAIChatOptions", + "AzureOpenAIEmbeddingClient", + "AzureOpenAIResponsesClient", + "AzureOpenAIResponsesOptions", + "AzureOpenAISettings", + "AzureTokenProvider", + "AzureUserSecurityContext", + "DurableAIAgent", + "DurableAIAgentClient", + "DurableAIAgentOrchestrationContext", + "DurableAIAgentWorker", + "FoundryAgent", + "FoundryChatClient", + "FoundryMemoryProvider", + "RawAzureAIClient", + "RawFoundryAgent", + "RawFoundryAgentChatClient", + "RawFoundryChatClient", +] From 8411243e990c4488cca885c9d7c6f8bc3719e7d1 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 20 Mar 2026 21:03:21 +0100 Subject: [PATCH 08/13] fix errors --- python/packages/azure-ai-search/README.md | 2 +- .../02-agents/context_providers/azure_ai_search/README.md | 4 ++-- ..._search_context_agentic.py => search_context_agentic.py} | 0 ...earch_context_semantic.py => search_context_semantic.py} | 0 .../providers/azure/foundry_agent_custom_client.py | 6 +++--- 5 files changed, 6 insertions(+), 6 deletions(-) rename python/samples/02-agents/context_providers/azure_ai_search/{azure_ai_with_search_context_agentic.py => search_context_agentic.py} (100%) rename python/samples/02-agents/context_providers/azure_ai_search/{azure_ai_with_search_context_semantic.py => search_context_semantic.py} (100%) diff --git a/python/packages/azure-ai-search/README.md b/python/packages/azure-ai-search/README.md index c24677b3b0..fcd3161f94 100644 --- a/python/packages/azure-ai-search/README.md +++ b/python/packages/azure-ai-search/README.md @@ -15,7 +15,7 @@ The Azure AI Search integration provides context providers for RAG (Retrieval Au ### Basic Usage Example -See the [Azure AI Search context provider examples](../../samples/02-agents/providers/azure_ai/) which demonstrate: +See the [Azure AI Search context provider examples](../../samples/02-agents/context_providers/azure_ai_search/) which demonstrate: - Semantic search with hybrid (vector + keyword) queries - Agentic mode with Knowledge Bases for complex multi-hop reasoning diff --git a/python/samples/02-agents/context_providers/azure_ai_search/README.md b/python/samples/02-agents/context_providers/azure_ai_search/README.md index 49403d106c..dfabc152f1 100644 --- a/python/samples/02-agents/context_providers/azure_ai_search/README.md +++ b/python/samples/02-agents/context_providers/azure_ai_search/README.md @@ -8,8 +8,8 @@ This folder contains examples demonstrating how to use the Azure AI Search conte | File | Description | |------|-------------| -| [`azure_ai_with_search_context_agentic.py`](azure_ai_with_search_context_agentic.py) | **Agentic mode** (recommended for most scenarios): Uses Knowledge Bases in Azure AI Search for query planning and multi-hop reasoning. Provides more accurate results through intelligent retrieval with automatic query reformulation. Slightly slower with more token consumption for query planning. [Learn more](https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/foundry-iq-boost-response-relevance-by-36-with-agentic-retrieval/4470720) | -| [`azure_ai_with_search_context_semantic.py`](azure_ai_with_search_context_semantic.py) | **Semantic mode** (fast queries): Fast hybrid search combining vector and keyword search with semantic ranking. Returns raw search results as context. Best for scenarios where speed is critical and simple retrieval is sufficient. | +| [`search_context_agentic.py`](search_context_agentic.py) | **Agentic mode** (recommended for most scenarios): Uses Knowledge Bases in Azure AI Search for query planning and multi-hop reasoning. Provides more accurate results through intelligent retrieval with automatic query reformulation. Slightly slower with more token consumption for query planning. [Learn more](https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/foundry-iq-boost-response-relevance-by-36-with-agentic-retrieval/4470720) | +| [`search_context_semantic.py`](search_context_semantic.py) | **Semantic mode** (fast queries): Fast hybrid search combining vector and keyword search with semantic ranking. Returns raw search results as context. Best for scenarios where speed is critical and simple retrieval is sufficient. | ## Installation diff --git a/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py b/python/samples/02-agents/context_providers/azure_ai_search/search_context_agentic.py similarity index 100% rename from python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py rename to python/samples/02-agents/context_providers/azure_ai_search/search_context_agentic.py diff --git a/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py b/python/samples/02-agents/context_providers/azure_ai_search/search_context_semantic.py similarity index 100% rename from python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py rename to python/samples/02-agents/context_providers/azure_ai_search/search_context_semantic.py diff --git a/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py b/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py index 9b51c533b9..54d62176fb 100644 --- a/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py +++ b/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py @@ -3,7 +3,7 @@ import asyncio from agent_framework import Agent -from agent_framework.azure import RawFoundryAgentChatClient, RawRawFoundryAgentChatClient +from agent_framework.azure import RawFoundryAgentChatClient from azure.identity import AzureCliCredential """ @@ -12,7 +12,7 @@ This sample demonstrates three ways to customize the FoundryAgent client layer: 1. Default: FoundryAgent creates a RawFoundryAgentChatClient (full middleware) internally -2. client_type: Pass RawRawFoundryAgentChatClient for no client middleware +2. client_type: Pass RawFoundryAgentChatClient for no client middleware 3. Composition: Use Agent(client=RawFoundryAgentChatClient(...)) directly Environment variables: @@ -41,7 +41,7 @@ async def main() -> None: agent_name="my-agent", agent_version="1.0", credential=AzureCliCredential(), - client_type=RawRawFoundryAgentChatClient, + client_type=RawFoundryAgentChatClient, ) result = await agent_raw_client.run("Hello from raw client!") print(f"Raw client: {result}\n") From b9eb63f6dbe9892185e208e2782af68b5a36a8ab Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 20 Mar 2026 21:06:25 +0100 Subject: [PATCH 09/13] updated adr number --- ...ovider-leading-clients.md => 0021-provider-leading-clients.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/decisions/{0020-provider-leading-clients.md => 0021-provider-leading-clients.md} (100%) diff --git a/docs/decisions/0020-provider-leading-clients.md b/docs/decisions/0021-provider-leading-clients.md similarity index 100% rename from docs/decisions/0020-provider-leading-clients.md rename to docs/decisions/0021-provider-leading-clients.md From e3ec31e7e7c1fed1854b84ee0012f1ea1058c9c5 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Sat, 21 Mar 2026 16:39:04 +0100 Subject: [PATCH 10/13] fix foundry local --- .../agent_framework_foundry_local/_foundry_local_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py index 5f4941cd57..90c2acb361 100644 --- a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py +++ b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py @@ -266,7 +266,7 @@ class MyOptions(FoundryLocalChatOptions, total=False): manager.load_model(alias_or_model_id=model_info.id, device=device) super().__init__( - model_id=model_info.id, + model=model_info.id, async_client=AsyncOpenAI(base_url=manager.endpoint, api_key=manager.api_key), additional_properties=additional_properties, middleware=middleware, From 11403cda59fce28b7539b275d3300cf9b40e99a1 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 23 Mar 2026 17:22:21 +0100 Subject: [PATCH 11/13] fixed not renamed docstrings and comments, and added deprecated markers to old classes --- .../agent_framework_azure_ai/__init__.py | 6 +-- .../_agent_provider.py | 4 +- .../agent_framework_azure_ai/_chat_client.py | 37 ++++++++------- .../agent_framework_azure_ai/_client.py | 29 ++++++++---- .../_foundry_agent_client.py | 46 ++++++++++++++++++- .../_project_provider.py | 14 +++--- .../tests/test_azure_ai_agent_client.py | 9 ++++ .../azure-ai/tests/test_foundry_agent.py | 30 ++++++++++++ .../02-agents/providers/azure/README.md | 22 ++++----- .../providers/azure/foundry_agent_basic.py | 2 +- .../azure/foundry_agent_custom_client.py | 5 +- .../providers/azure/foundry_chat_client.py | 10 ++-- .../azure/foundry_chat_client_basic.py | 9 ++-- ...ndry_chat_client_code_interpreter_files.py | 16 +++---- .../foundry_chat_client_image_analysis.py | 8 ++-- ...undry_chat_client_with_code_interpreter.py | 8 ++-- ...ndry_chat_client_with_explicit_settings.py | 10 ++-- .../foundry_chat_client_with_file_search.py | 10 ++-- ...foundry_chat_client_with_function_tools.py | 6 +-- .../foundry_chat_client_with_hosted_mcp.py | 6 +-- .../foundry_chat_client_with_local_mcp.py | 15 +++--- .../azure/foundry_chat_client_with_session.py | 8 ++-- 22 files changed, 205 insertions(+), 105 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py index 5eb19edc63..ada8d2a20d 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py @@ -3,8 +3,8 @@ import importlib.metadata from ._agent_provider import AzureAIAgentsProvider -from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions -from ._client import AzureAIClient, AzureAIProjectAgentOptions, RawAzureAIClient +from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions # pyright: ignore[reportDeprecated] +from ._client import AzureAIClient, AzureAIProjectAgentOptions, RawAzureAIClient # pyright: ignore[reportDeprecated] from ._deprecated_azure_openai import ( AzureOpenAIAssistantsClient, # pyright: ignore[reportDeprecated] AzureOpenAIAssistantsOptions, @@ -28,7 +28,7 @@ from ._foundry_agent_client import RawFoundryAgentChatClient from ._foundry_chat_client import FoundryChatClient, RawFoundryChatClient from ._foundry_memory_provider import FoundryMemoryProvider -from ._project_provider import AzureAIProjectAgentProvider +from ._project_provider import AzureAIProjectAgentProvider # pyright: ignore[reportDeprecated] from ._shared import AzureAISettings try: diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py index 043819c6a6..1d613443c7 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py @@ -23,7 +23,7 @@ from azure.ai.agents.models import ResponseFormatJsonSchema, ResponseFormatJsonSchemaType from pydantic import BaseModel -from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions +from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions # pyright: ignore[reportDeprecated] from ._entra_id_authentication import AzureCredentialTypes from ._shared import AzureAISettings, to_azure_ai_agent_tools @@ -426,7 +426,7 @@ def _to_chat_agent_from_agent( context_providers: Context providers to include during agent invocation. """ # Create the underlying client - client = AzureAIAgentClient( + client = AzureAIAgentClient( # pyright: ignore[reportDeprecated] agents_client=self._agents_client, agent_id=agent.id, agent_name=agent.name, diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index d533a46f11..7faba8c47c 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -96,8 +96,9 @@ if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover + from warnings import deprecated # type: ignore # pragma: no cover else: - from typing_extensions import TypeVar # type: ignore # pragma: no cover + from typing_extensions import TypeVar, deprecated # type: ignore # pragma: no cover if sys.version_info >= (3, 12): from typing import override # type: ignore # pragma: no cover else: @@ -210,6 +211,11 @@ class AzureAIAgentOptions(ChatOptions, total=False): # endregion +@deprecated( + "AzureAIAgentClient is deprecated. " + "It targets the V1 Agents Service API and has no direct replacement; " + "for new Foundry projects, use FoundryAgent." +) class AzureAIAgentClient( FunctionInvocationLayer[AzureAIAgentOptionsT], ChatMiddlewareLayer[AzureAIAgentOptionsT], @@ -221,7 +227,8 @@ class AzureAIAgentClient( .. deprecated:: AzureAIAgentClient is deprecated and will be removed in a future release. - Use :class:`AzureAIClient` instead for the V2 (Projects/Responses) API. + It targets the V1 Agents Service API and has no direct replacement. + For new Foundry projects, use :class:`FoundryAgent`. """ OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai" # type: ignore[reportIncompatibleVariableOverride, misc] @@ -239,7 +246,8 @@ def get_code_interpreter_tool( .. deprecated:: This method is deprecated and will be removed in a future release. - Use :meth:`AzureAIClient.get_code_interpreter_tool` instead. + For new Foundry projects, configure hosted tools on the Foundry agent definition + in the service instead. Keyword Args: file_ids: List of uploaded file IDs or Content objects to make available to @@ -272,7 +280,7 @@ def get_code_interpreter_tool( """ warnings.warn( "AzureAIAgentClient.get_code_interpreter_tool() is deprecated and will be removed in a future release; " - "use AzureAIClient.get_code_interpreter_tool() instead.", + "for new Foundry projects, configure hosted tools on the Foundry agent definition in the service instead.", DeprecationWarning, stacklevel=2, ) @@ -288,7 +296,8 @@ def get_file_search_tool( .. deprecated:: This method is deprecated and will be removed in a future release. - Use :meth:`AzureAIClient.get_file_search_tool` instead. + For new Foundry projects, configure hosted tools on the Foundry agent definition + in the service instead. Keyword Args: vector_store_ids: List of vector store IDs to search within. @@ -308,7 +317,7 @@ def get_file_search_tool( """ warnings.warn( "AzureAIAgentClient.get_file_search_tool() is deprecated and will be removed in a future release; " - "use AzureAIClient.get_file_search_tool() instead.", + "for new Foundry projects, configure hosted tools on the Foundry agent definition in the service instead.", DeprecationWarning, stacklevel=2, ) @@ -325,7 +334,8 @@ def get_web_search_tool( .. deprecated:: This method is deprecated and will be removed in a future release. - Use :meth:`AzureAIClient.get_web_search_tool` instead. + For new Foundry projects, configure hosted tools on the Foundry agent definition + in the service instead. For Azure AI Agents, web search uses Bing Grounding or Bing Custom Search. If no arguments are provided, attempts to read from environment variables. @@ -369,7 +379,7 @@ def get_web_search_tool( """ warnings.warn( "AzureAIAgentClient.get_web_search_tool() is deprecated and will be removed in a future release; " - "use AzureAIClient.get_web_search_tool() instead.", + "for new Foundry projects, configure hosted tools on the Foundry agent definition in the service instead.", DeprecationWarning, stacklevel=2, ) @@ -410,7 +420,8 @@ def get_mcp_tool( .. deprecated:: This method is deprecated and will be removed in a future release. - Use :meth:`AzureAIClient.get_mcp_tool` instead. + For new Foundry projects, configure hosted tools on the Foundry agent definition + in the service instead. This configures an MCP (Model Context Protocol) server that will be called by Azure AI's service. The tools from this MCP server are executed remotely @@ -446,7 +457,7 @@ def get_mcp_tool( """ warnings.warn( "AzureAIAgentClient.get_mcp_tool() is deprecated and will be removed in a future release; " - "use AzureAIClient.get_mcp_tool() instead.", + "for new Foundry projects, configure hosted tools on the Foundry agent definition in the service instead.", DeprecationWarning, stacklevel=2, ) @@ -561,12 +572,6 @@ class MyOptions(AzureAIAgentOptions, total=False): client: AzureAIAgentClient[MyOptions] = AzureAIAgentClient(credential=credential) response = await client.get_response("Hello", options={"my_custom_option": "value"}) """ - warnings.warn( - "AzureAIAgentClient is deprecated and will be removed in a future release; " - "use AzureAIClient instead for the V2 (Projects/Responses) API.", - DeprecationWarning, - stacklevel=2, - ) azure_ai_settings = load_settings( AzureAISettings, env_prefix="AZURE_AI_", diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index c07ecaf8a8..f974227bde 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -54,8 +54,9 @@ if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover + from warnings import deprecated # type: ignore # pragma: no cover else: - from typing_extensions import TypeVar # type: ignore # pragma: no cover + from typing_extensions import TypeVar, deprecated # type: ignore # pragma: no cover if sys.version_info >= (3, 12): from typing import override # type: ignore # pragma: no cover else: @@ -88,8 +89,13 @@ class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False): # type: _DOC_INDEX_PATTERN = re.compile(r"doc_(\d+)") +@deprecated( + "RawAzureAIClient is deprecated. " + "Use RawFoundryAgentChatClient for low-level Foundry agent client customization, " + "or FoundryAgent for the recommended production API." +) class RawAzureAIClient(RawOpenAIChatClient[AzureAIClientOptionsT], Generic[AzureAIClientOptionsT]): - """Raw Azure AI client without middleware, telemetry, or function invocation layers. + """Deprecated raw Azure AI client without middleware, telemetry, or function invocation layers. Warning: **This class should not normally be used directly.** It does not include middleware, @@ -101,7 +107,8 @@ class RawAzureAIClient(RawOpenAIChatClient[AzureAIClientOptionsT], Generic[Azure 2. **ChatMiddlewareLayer** - Applies chat middleware per model call and stays outside telemetry 3. **ChatTelemetryLayer** - Must stay inside chat middleware for correct per-call telemetry - Use ``AzureAIClient`` instead for a fully-featured client with all layers applied. + Use ``RawFoundryAgentChatClient`` for low-level Foundry agent customization, or + ``FoundryAgent`` for the recommended production API. """ OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai" # type: ignore[reportIncompatibleVariableOverride, misc] @@ -840,7 +847,7 @@ def _inner_get_response( if not stream: async def _enrich_response() -> ChatResponse: - response = await super(RawAzureAIClient, self)._inner_get_response( + response = await super(RawAzureAIClient, self)._inner_get_response( # pyright: ignore[reportDeprecated] messages=messages, options=options, stream=False, **kwargs ) get_urls = self._extract_azure_search_urls(response.raw_representation.output) # type: ignore[union-attr] @@ -1180,8 +1187,8 @@ def as_agent( It does NOT create an agent on the Azure AI service - the actual agent will be created on the server during the first invocation (run). - For creating and managing persistent agents on the server, use - :class:`~agent_framework_azure_ai.AzureAIProjectAgentProvider` instead. + For working with pre-configured persistent agents on the server, use + :class:`~agent_framework_azure_ai.FoundryAgent` instead. Keyword Args: id: The unique identifier for the agent. Will be created automatically if not provided. @@ -1211,21 +1218,23 @@ def as_agent( ) +@deprecated("AzureAIClient is deprecated. Use FoundryAgent instead.") class AzureAIClient( FunctionInvocationLayer[AzureAIClientOptionsT], ChatMiddlewareLayer[AzureAIClientOptionsT], ChatTelemetryLayer[AzureAIClientOptionsT], - RawAzureAIClient[AzureAIClientOptionsT], + RawAzureAIClient[AzureAIClientOptionsT], # pyright: ignore[reportDeprecated] Generic[AzureAIClientOptionsT], ): - """Azure AI client with middleware, telemetry, and function invocation support. + """Deprecated Azure AI client with middleware, telemetry, and function invocation support. - This is the recommended client for most use cases. It includes: + This class is deprecated. Use ``FoundryAgent`` instead for connecting to + pre-configured agents in Foundry. It includes: - Chat middleware support for request/response interception - OpenTelemetry-based telemetry for observability - Automatic function/tool invocation handling - For a minimal implementation without these features, use :class:`RawAzureAIClient`. + For a minimal implementation without these features, use :class:`RawFoundryAgentChatClient`. """ def __init__( diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent_client.py index 8a90d5bea7..2b5567fb60 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_foundry_agent_client.py @@ -10,7 +10,7 @@ import logging import sys -from collections.abc import Mapping, MutableMapping, Sequence +from collections.abc import Callable, Mapping, MutableMapping, Sequence from typing import TYPE_CHECKING, Any, ClassVar, Generic, cast from agent_framework._middleware import ChatMiddlewareLayer @@ -40,12 +40,15 @@ from typing_extensions import TypedDict # type: ignore # pragma: no cover if TYPE_CHECKING: + from agent_framework import Agent, BaseContextProvider from agent_framework._middleware import ( ChatMiddleware, ChatMiddlewareCallable, FunctionMiddleware, FunctionMiddlewareCallable, + MiddlewareTypes, ) + from agent_framework._tools import ToolTypes class FoundryAgentSettings(TypedDict, total=False): @@ -185,6 +188,47 @@ def _get_agent_reference(self) -> dict[str, str]: ref["version"] = self.agent_version return ref + @override + def as_agent( + self, + *, + id: str | None = None, + name: str | None = None, + description: str | None = None, + instructions: str | None = None, + tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None, + default_options: FoundryAgentOptionsT | Mapping[str, Any] | None = None, + context_providers: Sequence[BaseContextProvider] | None = None, + middleware: Sequence[MiddlewareTypes] | None = None, + **kwargs: Any, + ) -> Agent[FoundryAgentOptionsT]: + """Create a FoundryAgent that reuses this client's Foundry configuration.""" + from ._foundry_agent import FoundryAgent + + function_tools = cast( + FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None, + tools, + ) + + return cast( + "Agent[FoundryAgentOptionsT]", + FoundryAgent( + project_client=self.project_client, + agent_name=self.agent_name, + agent_version=self.agent_version, + tools=function_tools, + context_providers=context_providers, + middleware=middleware, + client_type=cast(type[RawFoundryAgentChatClient], self.__class__), + id=id, + name=self.agent_name if name is None else name, + description=description, + instructions=instructions, + default_options=default_options, + **kwargs, + ), + ) + @override async def _prepare_options( self, diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py index f73171b15f..b4c948efa3 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py @@ -28,14 +28,15 @@ FunctionTool as AzureFunctionTool, ) -from ._client import AzureAIClient, AzureAIProjectAgentOptions +from ._client import AzureAIClient, AzureAIProjectAgentOptions # pyright: ignore[reportDeprecated] from ._entra_id_authentication import AzureCredentialTypes from ._shared import AzureAISettings, create_text_format_config, from_azure_ai_tools, to_azure_ai_tools if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover + from warnings import deprecated # type: ignore # pragma: no cover else: - from typing_extensions import TypeVar # type: ignore # pragma: no cover + from typing_extensions import TypeVar, deprecated # type: ignore # pragma: no cover if sys.version_info >= (3, 11): from typing import Self, TypedDict # type: ignore # pragma: no cover else: @@ -55,11 +56,12 @@ ) +@deprecated("AzureAIProjectAgentProvider is deprecated. Use FoundryAgent instead.") class AzureAIProjectAgentProvider(Generic[OptionsCoT]): - """Provider for Azure AI Agent Service (Responses API). + """Deprecated provider for Azure AI Agent Service (Responses API). - This provider allows you to create, retrieve, and manage Azure AI agents - using the AIProjectClient from the Azure AI Projects SDK. + This provider is deprecated. Use ``FoundryAgent`` instead to connect to + pre-configured agents in Foundry. Examples: Using with explicit AIProjectClient: @@ -384,7 +386,7 @@ def _to_chat_agent_from_details( if not isinstance(details.definition, PromptAgentDefinition): raise ValueError("Agent definition must be PromptAgentDefinition to get a Agent.") - client = AzureAIClient( + client = AzureAIClient( # pyright: ignore[reportDeprecated] project_client=self._project_client, agent_name=details.name, agent_version=details.version, diff --git a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py index 65922e76b2..b6560a68b0 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py @@ -102,6 +102,15 @@ def create_test_azure_ai_chat_client( return client +def test_init_emits_updated_deprecation_warning(mock_agents_client: MagicMock) -> None: + """Test that construction emits the updated class deprecation warning.""" + with pytest.deprecated_call(match="V1 Agents Service API and has no direct replacement"): + AzureAIAgentClient( + agents_client=mock_agents_client, + agent_id="test-agent", + ) + + def test_azure_ai_settings_init(azure_ai_unit_test_env: dict[str, str]) -> None: """Test AzureAISettings initialization.""" settings = load_settings(AzureAISettings, env_prefix="AZURE_AI_") diff --git a/python/packages/azure-ai/tests/test_foundry_agent.py b/python/packages/azure-ai/tests/test_foundry_agent.py index 3031eb4b98..795ea60f3c 100644 --- a/python/packages/azure-ai/tests/test_foundry_agent.py +++ b/python/packages/azure-ai/tests/test_foundry_agent.py @@ -69,6 +69,36 @@ def test_get_agent_reference_without_version(self) -> None: assert ref == {"name": "hosted-agent", "type": "agent_reference"} assert "version" not in ref + def test_as_agent_returns_foundry_agent_and_preserves_client_type(self) -> None: + """Test that as_agent() wraps the client in FoundryAgent using the same client class.""" + from agent_framework_azure_ai._foundry_agent import FoundryAgent + from agent_framework_azure_ai._foundry_agent_client import RawFoundryAgentChatClient + + class CustomClient(RawFoundryAgentChatClient): + pass + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = CustomClient( + project_client=mock_project, + agent_name="test-agent", + agent_version="1.0", + ) + + agent = client.as_agent(instructions="You are helpful.") + + assert isinstance(agent, FoundryAgent) + assert agent.name == "test-agent" + assert isinstance(agent.client, CustomClient) + assert agent.client.project_client is mock_project + assert agent.client.agent_name == "test-agent" + assert agent.client.agent_version == "1.0" + + named_agent = client.as_agent(name="display-name", instructions="You are helpful.") + assert named_agent.name == "display-name" + assert named_agent.client.agent_name == "test-agent" + async def test_prepare_options_validates_tools(self) -> None: """Test that _prepare_options rejects non-FunctionTool objects.""" from agent_framework import Message diff --git a/python/samples/02-agents/providers/azure/README.md b/python/samples/02-agents/providers/azure/README.md index 6d7b9e40c4..5bc87bc031 100644 --- a/python/samples/02-agents/providers/azure/README.md +++ b/python/samples/02-agents/providers/azure/README.md @@ -16,17 +16,17 @@ This folder contains examples demonstrating different ways to use Azure AI Found | File | Description | |------|-------------| -| [`foundry_chat_client.py`](foundry_chat_client.py) | Azure OpenAI Responses Client with Foundry Project Example | -| [`foundry_chat_client_basic.py`](foundry_chat_client_basic.py) | Azure OpenAI Chat Client Basic Example | -| [`foundry_chat_client_code_interpreter_files.py`](foundry_chat_client_code_interpreter_files.py) | Azure OpenAI Responses Client with Code Interpreter and Files Example | -| [`foundry_chat_client_image_analysis.py`](foundry_chat_client_image_analysis.py) | | -| [`foundry_chat_client_with_code_interpreter.py`](foundry_chat_client_with_code_interpreter.py) | """ | -| [`foundry_chat_client_with_explicit_settings.py`](foundry_chat_client_with_explicit_settings.py) | Azure OpenAI Chat Client with Explicit Settings Example | -| [`foundry_chat_client_with_file_search.py`](foundry_chat_client_with_file_search.py) | Azure OpenAI Responses Client with File Search Example | -| [`foundry_chat_client_with_function_tools.py`](foundry_chat_client_with_function_tools.py) | Azure OpenAI Chat Client with Function Tools Example | -| [`foundry_chat_client_with_hosted_mcp.py`](foundry_chat_client_with_hosted_mcp.py) | | -| [`foundry_chat_client_with_local_mcp.py`](foundry_chat_client_with_local_mcp.py) | | -| [`foundry_chat_client_with_session.py`](foundry_chat_client_with_session.py) | Azure OpenAI Chat Client with Session Management Example | +| [`foundry_chat_client.py`](foundry_chat_client.py) | Foundry Chat Client with Project Endpoint Example | +| [`foundry_chat_client_basic.py`](foundry_chat_client_basic.py) | Foundry Chat Client Basic Example | +| [`foundry_chat_client_code_interpreter_files.py`](foundry_chat_client_code_interpreter_files.py) | Foundry Chat Client with Code Interpreter and Files Example | +| [`foundry_chat_client_image_analysis.py`](foundry_chat_client_image_analysis.py) | Foundry Chat Client with Image Analysis Example | +| [`foundry_chat_client_with_code_interpreter.py`](foundry_chat_client_with_code_interpreter.py) | Foundry Chat Client with Code Interpreter Example | +| [`foundry_chat_client_with_explicit_settings.py`](foundry_chat_client_with_explicit_settings.py) | Foundry Chat Client with Explicit Settings Example | +| [`foundry_chat_client_with_file_search.py`](foundry_chat_client_with_file_search.py) | Foundry Chat Client with File Search Example | +| [`foundry_chat_client_with_function_tools.py`](foundry_chat_client_with_function_tools.py) | Foundry Chat Client with Function Tools Example | +| [`foundry_chat_client_with_hosted_mcp.py`](foundry_chat_client_with_hosted_mcp.py) | Foundry Chat Client with Hosted MCP Example | +| [`foundry_chat_client_with_local_mcp.py`](foundry_chat_client_with_local_mcp.py) | Foundry Chat Client with Local MCP Example | +| [`foundry_chat_client_with_session.py`](foundry_chat_client_with_session.py) | Foundry Chat Client with Session Management Example | ## OpenAI ChatCompletionClient with Azure OpenAI Samples diff --git a/python/samples/02-agents/providers/azure/foundry_agent_basic.py b/python/samples/02-agents/providers/azure/foundry_agent_basic.py index ffc57fa110..5744477830 100644 --- a/python/samples/02-agents/providers/azure/foundry_agent_basic.py +++ b/python/samples/02-agents/providers/azure/foundry_agent_basic.py @@ -6,7 +6,7 @@ from azure.identity import AzureCliCredential """ -Foundry Agent — Connect to a pre-configured agent in Azure AI Foundry +Foundry Agent — Connect to a pre-configured agent in Microsoft Foundry This sample shows the simplest way to connect to an existing PromptAgent in Azure AI Foundry and run it. The agent's instructions, model, and hosted diff --git a/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py b/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py index 54d62176fb..2a74debe9e 100644 --- a/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py +++ b/python/samples/02-agents/providers/azure/foundry_agent_custom_client.py @@ -3,7 +3,7 @@ import asyncio from agent_framework import Agent -from agent_framework.azure import RawFoundryAgentChatClient +from agent_framework.azure import FoundryAgent, RawFoundryAgentChatClient from azure.identity import AzureCliCredential """ @@ -24,8 +24,6 @@ async def main() -> None: # Option 1: Default — full middleware on both agent and client - from agent_framework.azure import FoundryAgent - agent = FoundryAgent( project_endpoint="https://your-project.services.ai.azure.com", agent_name="my-agent", @@ -47,6 +45,7 @@ async def main() -> None: print(f"Raw client: {result}\n") # Option 3: Composition — use Agent(client=...) directly + # this will not run the checks that the `FoundryAgent` does on things like tools. client = RawFoundryAgentChatClient( project_endpoint="https://your-project.services.ai.azure.com", agent_name="my-agent", diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client.py b/python/samples/02-agents/providers/azure/foundry_chat_client.py index bd21725845..674d8561a4 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client.py @@ -15,10 +15,10 @@ load_dotenv() """ -Azure OpenAI Responses Client with Foundry Project Example +Foundry Chat Client with Project Endpoint Example -This sample demonstrates how to create an FoundryChatClient using an -Azure AI Foundry project endpoint. Instead of providing an Azure OpenAI endpoint +This sample demonstrates how to create a FoundryChatClient using a +Foundry project endpoint. Instead of providing a service endpoint directly, you provide a Foundry project endpoint and the client is created via the Azure AI Foundry project SDK. @@ -95,7 +95,7 @@ async def streaming_example() -> None: async def main() -> None: - print("=== Azure OpenAI Responses Client with Foundry Project Example ===") + print("=== Foundry Chat Client with Project Endpoint Example ===") await non_streaming_example() await streaming_example() @@ -107,7 +107,7 @@ async def main() -> None: """ Sample output: -=== Azure OpenAI Responses Client with Foundry Project Example === +=== Foundry Chat Client with Project Endpoint Example === === Non-streaming Response Example === User: What's the weather like in Seattle? Result: The weather in Seattle is cloudy with a high of 18°C. diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_basic.py b/python/samples/02-agents/providers/azure/foundry_chat_client_basic.py index c86e3bc27f..ad16ca74ba 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_basic.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_basic.py @@ -14,10 +14,13 @@ load_dotenv() """ -Azure OpenAI Chat Client Basic Example +Foundry Chat Client Basic Example -This sample demonstrates basic usage of OpenAIChatClient for structured +This sample demonstrates basic usage of FoundryChatClient for structured response generation, showing both streaming and non-streaming responses. + +This uses a deployed model in Foundry, with the Responses API endpoint of Foundry. +The client has full support for tools, response formats, etc. """ @@ -73,7 +76,7 @@ async def streaming_example() -> None: async def main() -> None: - print("=== Basic Azure OpenAI Chat Client Agent Example ===") + print("=== Foundry Chat Client Basic Example ===") await non_streaming_example() await streaming_example() diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_code_interpreter_files.py b/python/samples/02-agents/providers/azure/foundry_chat_client_code_interpreter_files.py index a8cb8d5a1c..744e6bdc3b 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_code_interpreter_files.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_code_interpreter_files.py @@ -14,9 +14,9 @@ load_dotenv() """ -Azure OpenAI Responses Client with Code Interpreter and Files Example +Foundry Chat Client with Code Interpreter and Files Example -This sample demonstrates using get_code_interpreter_tool() with Azure OpenAI Responses +This sample demonstrates using get_code_interpreter_tool() with Responses on Foundry for Python code execution and data analysis with uploaded files. """ @@ -24,7 +24,7 @@ async def create_sample_file_and_upload(openai_client: AsyncAzureOpenAI) -> tuple[str, str]: - """Create a sample CSV file and upload it to Azure OpenAI.""" + """Create a sample CSV file and upload it for Foundry code interpreter use.""" csv_data = """name,department,salary,years_experience Alice Johnson,Engineering,95000,5 Bob Smith,Sales,75000,3 @@ -39,8 +39,8 @@ async def create_sample_file_and_upload(openai_client: AsyncAzureOpenAI) -> tupl temp_file.write(csv_data) temp_file_path = temp_file.name - # Upload file to Azure OpenAI - print("Uploading file to Azure OpenAI...") + # Upload file for the code interpreter tool + print("Uploading file for code interpreter...") with open(temp_file_path, "rb") as file: uploaded_file = await openai_client.files.create( file=file, @@ -63,9 +63,9 @@ async def cleanup_files(openai_client: AsyncAzureOpenAI, temp_file_path: str, fi async def main() -> None: - print("=== Azure OpenAI Code Interpreter with File Upload ===") + print("=== Foundry Chat Client with Code Interpreter and File Upload ===") - # Initialize Azure OpenAI client for file operations + # Initialize the underlying OpenAI client for file operations credential = AzureCliCredential() async def get_token(): @@ -79,7 +79,7 @@ async def get_token(): temp_file_path, file_id = await create_sample_file_and_upload(openai_client) - # Create agent using Azure OpenAI Responses client + # Create agent using FoundryChatClient client = FoundryChatClient(credential=credential) # Create code interpreter tool with file access diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_image_analysis.py b/python/samples/02-agents/providers/azure/foundry_chat_client_image_analysis.py index 1876aa4aaf..c686de7f14 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_image_analysis.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_image_analysis.py @@ -11,17 +11,17 @@ load_dotenv() """ -Azure OpenAI Chat Client with Image Analysis Example +Foundry Chat Client with Image Analysis Example -This sample demonstrates using Azure OpenAI Responses for image analysis and vision tasks, +This sample demonstrates using FoundryChatClient for image analysis and vision tasks, showing multi-modal messages combining text and image content. """ async def main(): - print("=== Azure Responses Agent with Image Analysis ===") + print("=== Foundry Chat Client with Image Analysis ===") - # 1. Create an Azure Responses agent with vision capabilities + # 1. Create a Foundry-backed agent with vision capabilities agent = Agent( client=FoundryChatClient(credential=AzureCliCredential()), name="VisionAgent", diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_with_code_interpreter.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_code_interpreter.py index a3d32c451a..7203020c6f 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_with_code_interpreter.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_code_interpreter.py @@ -13,16 +13,16 @@ load_dotenv() """ -Azure OpenAI Responses Client with Code Interpreter Example +Foundry Chat Client with Code Interpreter Example -This sample demonstrates using get_code_interpreter_tool() with Azure OpenAI Responses +This sample demonstrates using get_code_interpreter_tool() with FoundryChatClient for Python code execution and mathematical problem solving. """ async def main() -> None: - """Example showing how to use the code interpreter tool with Azure OpenAI Responses.""" - print("=== Azure OpenAI Responses Agent with Code Interpreter Example ===") + """Example showing how to use the code interpreter tool with FoundryChatClient.""" + print("=== Foundry Chat Client with Code Interpreter Example ===") # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_with_explicit_settings.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_explicit_settings.py index e4a11e546b..7b6d54ec09 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_with_explicit_settings.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_explicit_settings.py @@ -15,10 +15,10 @@ load_dotenv() """ -Azure OpenAI Chat Client with Explicit Settings Example +Foundry Chat Client with Explicit Settings Example -This sample demonstrates creating Azure OpenAI Chat Client with explicit configuration -settings rather than relying on environment variable defaults. +This sample demonstrates creating FoundryChatClient with explicit project endpoint and +model settings rather than relying on environment variable defaults. """ @@ -35,13 +35,13 @@ def get_weather( async def main() -> None: - print("=== Azure Chat Client with Explicit Settings ===") + print("=== Foundry Chat Client with Explicit Settings ===") # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. _client = FoundryChatClient( model=os.environ["FOUNDRY_MODEL"], - endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=AzureCliCredential(), ) agent = Agent( diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_with_file_search.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_file_search.py index 08781ffa44..59d94ebaf7 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_with_file_search.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_file_search.py @@ -12,14 +12,14 @@ load_dotenv() """ -Azure OpenAI Responses Client with File Search Example +Foundry Chat Client with File Search Example -This sample demonstrates using get_file_search_tool() with Azure OpenAI Responses Client +This sample demonstrates using get_file_search_tool() with FoundryChatClient for direct document-based question answering and information retrieval. Prerequisites: - Set environment variables: - - AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint URL + - FOUNDRY_PROJECT_ENDPOINT: Your Foundry project endpoint URL - FOUNDRY_MODEL: Your Responses API deployment name - Authenticate via 'az login' for AzureCliCredential """ @@ -52,9 +52,9 @@ async def delete_vector_store(client: FoundryChatClient, file_id: str, vector_st async def main() -> None: - print("=== Azure OpenAI Responses Client with File Search Example ===\n") + print("=== Foundry Chat Client with File Search Example ===\n") - # Initialize Responses client + # Initialize the Foundry chat client # Make sure you're logged in via 'az login' before running this sample client = FoundryChatClient(credential=AzureCliCredential()) diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_with_function_tools.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_function_tools.py index 514e3b4ebe..6c598fb56b 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_with_function_tools.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_function_tools.py @@ -15,9 +15,9 @@ load_dotenv() """ -Azure OpenAI Chat Client with Function Tools Example +Foundry Chat Client with Function Tools Example -This sample demonstrates function tool integration with Azure OpenAI Chat Client, +This sample demonstrates function tool integration with FoundryChatClient, showing both agent-level and query-level tool configuration patterns. """ @@ -132,7 +132,7 @@ async def mixed_tools_example() -> None: async def main() -> None: - print("=== Azure Chat Client Agent with Function Tools Examples ===\n") + print("=== Foundry Chat Client with Function Tools Examples ===\n") await tools_on_agent_level() await tools_on_run_level() diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_with_hosted_mcp.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_hosted_mcp.py index b2a308e3db..79ce9a15c2 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_with_hosted_mcp.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_hosted_mcp.py @@ -12,10 +12,10 @@ load_dotenv() """ -Azure OpenAI Responses Client with Hosted MCP Example +Foundry Chat Client with Hosted MCP Example This sample demonstrates integrating hosted Model Context Protocol (MCP) tools with -Azure OpenAI Responses Client, including user approval workflows for function call security. +FoundryChatClient, including user approval workflows for function call security. """ if TYPE_CHECKING: @@ -253,7 +253,7 @@ async def run_hosted_mcp_with_session_streaming() -> None: async def main() -> None: - print("=== OpenAI Responses Client Agent with Hosted Mcp Tools Examples ===\n") + print("=== Foundry Chat Client with Hosted MCP Examples ===\n") await run_hosted_mcp_without_approval() await run_hosted_mcp_without_session_and_specific_approval() diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py index 1c3843e432..11d6cba8a6 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_local_mcp.py @@ -12,9 +12,9 @@ load_dotenv() """ -Azure OpenAI Responses Client with local Model Context Protocol (MCP) Example +Foundry Chat Client with Local Model Context Protocol (MCP) Example -This sample demonstrates integration of Azure OpenAI Responses Client with local Model Context Protocol (MCP) +This sample demonstrates integration of FoundryChatClient with local Model Context Protocol (MCP) servers. """ @@ -24,19 +24,18 @@ MCP_NAME = os.environ.get("MCP_NAME", "Microsoft Learn MCP") # example name MCP_URL = os.environ.get("MCP_URL", "https://learn.microsoft.com/api/mcp") # example endpoint -# Environment variables for Azure OpenAI Responses authentication -# AZURE_OPENAI_ENDPOINT="" +# Environment variables for FoundryChatClient authentication +# FOUNDRY_PROJECT_ENDPOINT="" # FOUNDRY_MODEL="" -# AZURE_OPENAI_API_VERSION="" # e.g. "2025-03-01-preview" async def main(): - """Example showing local MCP tools for a Azure OpenAI Responses Agent.""" + """Example showing local MCP tools for a Foundry Chat Client agent.""" # AuthN: use Azure CLI credential = AzureCliCredential() - # Build an agent backed by Azure OpenAI Responses - # (endpoint/deployment/api_version can also come from env vars above) + # Build an agent backed by FoundryChatClient + # (project endpoint and model can also come from env vars above) responses_client = FoundryChatClient( credential=credential, ) diff --git a/python/samples/02-agents/providers/azure/foundry_chat_client_with_session.py b/python/samples/02-agents/providers/azure/foundry_chat_client_with_session.py index e8d926fcfd..d318253db1 100644 --- a/python/samples/02-agents/providers/azure/foundry_chat_client_with_session.py +++ b/python/samples/02-agents/providers/azure/foundry_chat_client_with_session.py @@ -14,9 +14,9 @@ load_dotenv() """ -Azure OpenAI Chat Client with Session Management Example +Foundry Chat Client with Session Management Example -This sample demonstrates session management with Azure OpenAI Chat Client, comparing +This sample demonstrates session management with FoundryChatClient, comparing automatic session creation with explicit session management for persistent context. """ @@ -96,7 +96,7 @@ async def example_with_session_persistence() -> None: async def example_with_existing_session_messages() -> None: - """Example showing how to work with existing session messages for Azure.""" + """Example showing how to work with existing session messages for Foundry-backed agents.""" print("=== Existing Session Messages Example ===") # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred @@ -150,7 +150,7 @@ async def example_with_existing_session_messages() -> None: async def main() -> None: - print("=== Azure Chat Client Agent Session Management Examples ===\n") + print("=== Foundry Chat Client Session Management Examples ===\n") await example_with_automatic_session_creation() await example_with_session_persistence() From 6dac96aa3b25d89ea764c03b1b5b9a303d333728 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 23 Mar 2026 20:06:06 +0100 Subject: [PATCH 12/13] fix tests and pyprojects --- .../workflows/python-integration-tests.yml | 4 +- .github/workflows/python-merge-tests.yml | 12 +- python/packages/azure-ai/pyproject.toml | 1 + python/packages/devui/pyproject.toml | 1 + python/packages/foundry_local/pyproject.toml | 1 + python/pyproject.toml | 1 + python/uv.lock | 778 +++++++++--------- 7 files changed, 409 insertions(+), 389 deletions(-) diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index 8f17137569..2ee9eb1af1 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -81,7 +81,7 @@ jobs: - name: Test with pytest (OpenAI integration) run: > uv run pytest --import-mode=importlib - packages/core/tests/openai + packages/openai/tests -m integration -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread @@ -121,7 +121,7 @@ jobs: - name: Test with pytest (Azure OpenAI integration) run: > uv run pytest --import-mode=importlib - packages/core/tests/azure + packages/azure-ai/tests/azure_openai -m integration -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index bcf545beac..e680ee2f16 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -54,10 +54,13 @@ jobs: - 'python/packages/core/agent_framework/observability.py' openai: - 'python/packages/core/agent_framework/openai/**' - - 'python/packages/core/tests/openai/**' + - 'python/packages/openai/**' + - 'python/samples/**/providers/openai/**' azure: - 'python/packages/core/agent_framework/azure/**' - - 'python/packages/core/tests/azure/**' + - 'python/packages/azure-ai/agent_framework_azure_ai/_deprecated_azure_openai.py' + - 'python/packages/azure-ai/tests/azure_openai/**' + - 'python/samples/**/providers/azure/openai_chat_completion_client_azure*.py' misc: - 'python/packages/anthropic/**' - 'python/packages/ollama/**' @@ -68,6 +71,7 @@ jobs: - 'python/packages/durabletask/**' azure-ai: - 'python/packages/azure-ai/**' + - 'python/samples/**/providers/azure/foundry*.py' cosmos: - 'python/packages/azure-cosmos/**' # run only if 'python' files were changed @@ -146,7 +150,7 @@ jobs: - name: Test with pytest (OpenAI integration) run: > uv run pytest --import-mode=importlib - packages/core/tests/openai + packages/openai/tests -m integration -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread @@ -205,7 +209,7 @@ jobs: - name: Test with pytest (Azure OpenAI integration) run: > uv run pytest --import-mode=importlib - packages/core/tests/azure + packages/azure-ai/tests/azure_openai -m integration -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread diff --git a/python/packages/azure-ai/pyproject.toml b/python/packages/azure-ai/pyproject.toml index 4b0024fe96..e46b52e9ae 100644 --- a/python/packages/azure-ai/pyproject.toml +++ b/python/packages/azure-ai/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core>=1.0.0rc5", + "agent-framework-openai>=1.0.0rc5", "azure-ai-agents>=1.2.0b5,<1.2.0b6", "azure-ai-inference>=1.0.0b9,<1.0.0b10", "aiohttp>=3.7.0,<4", diff --git a/python/packages/devui/pyproject.toml b/python/packages/devui/pyproject.toml index 54cd91b04f..5dfb96d111 100644 --- a/python/packages/devui/pyproject.toml +++ b/python/packages/devui/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core>=1.0.0rc5", + "openai>=1.99.0,<3", "fastapi>=0.115.0,<0.133.1", "uvicorn[standard]>=0.30.0,<0.42.0" ] diff --git a/python/packages/foundry_local/pyproject.toml b/python/packages/foundry_local/pyproject.toml index 3f1a5846f3..0137a1e491 100644 --- a/python/packages/foundry_local/pyproject.toml +++ b/python/packages/foundry_local/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core>=1.0.0rc5", + "agent-framework-openai>=1.0.0rc5", "foundry-local-sdk>=0.5.1,<0.5.2", ] diff --git a/python/pyproject.toml b/python/pyproject.toml index f955062de1..6b11d39bfc 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -78,6 +78,7 @@ agent-framework-foundry-local = { workspace = true } agent-framework-lab = { workspace = true } agent-framework-mem0 = { workspace = true } agent-framework-ollama = { workspace = true } +agent-framework-openai = { workspace = true } agent-framework-purview = { workspace = true } agent-framework-redis = { workspace = true } agent-framework-github-copilot = { workspace = true } diff --git a/python/uv.lock b/python/uv.lock index a8175fb812..f19c80cd44 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -203,6 +203,7 @@ version = "1.0.0rc5" source = { editable = "packages/azure-ai" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "agent-framework-openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "azure-ai-agents", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "azure-ai-inference", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -211,6 +212,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, + { name = "agent-framework-openai", editable = "packages/openai" }, { name = "aiohttp", specifier = ">=3.7.0,<4" }, { name = "azure-ai-agents", specifier = ">=1.2.0b5,<1.2.0b6" }, { name = "azure-ai-inference", specifier = ">=1.0.0b9,<1.0.0b10" }, @@ -442,6 +444,7 @@ source = { editable = "packages/devui" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -461,6 +464,7 @@ requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, { name = "agent-framework-orchestrations", marker = "extra == 'dev'", editable = "packages/orchestrations" }, { name = "fastapi", specifier = ">=0.115.0,<0.133.1" }, + { name = "openai", specifier = ">=1.99.0,<3" }, { name = "pytest", marker = "extra == 'all'", specifier = "==9.0.2" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==9.0.2" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0,<0.42.0" }, @@ -502,12 +506,14 @@ version = "1.0.0b260319" source = { editable = "packages/foundry_local" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "agent-framework-openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "foundry-local-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, + { name = "agent-framework-openai", editable = "packages/openai" }, { name = "foundry-local-sdk", specifier = ">=0.5.1,<0.5.2" }, ] @@ -986,11 +992,11 @@ wheels = [ [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] @@ -1023,7 +1029,7 @@ wheels = [ [[package]] name = "azure-ai-projects" -version = "2.0.0" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1033,9 +1039,9 @@ dependencies = [ { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/3d/6a7d04f61f3befc74a6f09ad7a0c02e8c701fc6db91ad7151c46da44a902/azure_ai_projects-2.0.0.tar.gz", hash = "sha256:0892f075cf287d747be54c25bea93dc9406ad100d44efc2fdaadb26586ecf4ff", size = 491449, upload-time = "2026-03-06T05:59:51.645Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/f9/a15c8a16e35e6d620faebabc6cc4f9e2f4b7f1d962cc6f58931c46947e24/azure_ai_projects-2.0.1.tar.gz", hash = "sha256:c8c64870aa6b89903af69a4ff28b4eff3df9744f14615ea572cae87394946a0c", size = 491774, upload-time = "2026-03-12T19:59:02.712Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/af/7b218cccab8e22af44844bfc16275b55c1fa48ed494145614b9852950fe6/azure_ai_projects-2.0.0-py3-none-any.whl", hash = "sha256:e655e0e495d0c76077d95cc8e0d606fcdbf3f4dbdf1a8379cbd4bea1e34c401d", size = 236354, upload-time = "2026-03-06T05:59:53.536Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f7/290ca39501c06c6e23b46ba9f7f3dfb05ecc928cde105fed85d6845060dd/azure_ai_projects-2.0.1-py3-none-any.whl", hash = "sha256:dfda540d256e67a52bf81c75418b6bf92b811b96693fe45787e154a888ad2396", size = 236560, upload-time = "2026-03-12T19:59:04.249Z" }, ] [[package]] @@ -1049,15 +1055,15 @@ wheels = [ [[package]] name = "azure-core" -version = "1.38.2" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/fe/5c7710bc611a4070d06ba801de9a935cc87c3d4b689c644958047bdf2cba/azure_core-1.38.2.tar.gz", hash = "sha256:67562857cb979217e48dc60980243b61ea115b77326fa93d83b729e7ff0482e7", size = 363734, upload-time = "2026-02-18T19:33:05.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/83/bbde3faa84ddcb8eb0eca4b3ffb3221252281db4ce351300fe248c5c70b1/azure_core-1.39.0.tar.gz", hash = "sha256:8a90a562998dd44ce84597590fff6249701b98c0e8797c95fcdd695b54c35d74", size = 367531, upload-time = "2026-03-19T01:31:29.461Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/23/6371a551800d3812d6019cd813acd985f9fac0fedc1290129211a73da4ae/azure_core-1.38.2-py3-none-any.whl", hash = "sha256:074806c75cf239ea284a33a66827695ef7aeddac0b4e19dda266a93e4665ead9", size = 217957, upload-time = "2026-02-18T19:33:07.696Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d6/8ebcd05b01a580f086ac9a97fb9fac65c09a4b012161cc97c21a336e880b/azure_core-1.39.0-py3-none-any.whl", hash = "sha256:4ac7b70fab5438c3f68770649a78daf97833caa83827f91df9c14e0e0ea7d34f", size = 218318, upload-time = "2026-03-19T01:31:31.25Z" }, ] [[package]] @@ -1105,7 +1111,7 @@ wheels = [ [[package]] name = "azure-identity" -version = "1.25.2" +version = "1.25.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1114,9 +1120,9 @@ dependencies = [ { name = "msal-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/3a/439a32a5e23e45f6a91f0405949dc66cfe6834aba15a430aebfc063a81e7/azure_identity-1.25.2.tar.gz", hash = "sha256:030dbaa720266c796221c6cdbd1999b408c079032c919fef725fcc348a540fe9", size = 284709, upload-time = "2026-02-11T01:55:42.323Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/0e/3a63efb48aa4a5ae2cfca61ee152fbcb668092134d3eb8bfda472dd5c617/azure_identity-1.25.3.tar.gz", hash = "sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6", size = 286304, upload-time = "2026-03-13T01:12:20.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/77/f658c76f9e9a52c784bd836aaca6fd5b9aae176f1f53273e758a2bcda695/azure_identity-1.25.2-py3-none-any.whl", hash = "sha256:1b40060553d01a72ba0d708b9a46d0f61f56312e215d8896d836653ffdc6753d", size = 191423, upload-time = "2026-02-11T01:55:44.245Z" }, + { url = "https://files.pythonhosted.org/packages/49/9a/417b3a533e01953a7c618884df2cb05a71e7b68bdbce4fbdb62349d2a2e8/azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c", size = 192138, upload-time = "2026-03-13T01:12:22.951Z" }, ] [[package]] @@ -1178,30 +1184,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.66" +version = "1.42.73" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "jmespath", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "s3transfer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/2e/67206daa5acb6053157ae5241421713a84ed6015d33d0781985bd5558898/boto3-1.42.66.tar.gz", hash = "sha256:3bec5300fb2429c3be8e8961fdb1f11e85195922c8a980022332c20af05616d5", size = 112805, upload-time = "2026-03-11T19:58:19.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8b/d00575be514744ca4839e7d85bf4a8a3c7b6b4574433291e58d14c68ae09/boto3-1.42.73.tar.gz", hash = "sha256:d37b58d6cd452ca808dd6823ae19ca65b6244096c5125ef9052988b337298bae", size = 112775, upload-time = "2026-03-20T19:39:52.814Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/09/83224363c3f5e468e298e48beb577ffe8cb51f18c2116bc1ecf404796e60/boto3-1.42.66-py3-none-any.whl", hash = "sha256:7c6c60dc5500e8a2967a306372a5fdb4c7f9a5b8adc5eb9aa2ebb5081c51ff47", size = 140557, upload-time = "2026-03-11T19:58:17.61Z" }, + { url = "https://files.pythonhosted.org/packages/aa/05/1fcf03d90abaa3d0b42a6bfd10231dd709493ecbacf794aa2eea5eae6841/boto3-1.42.73-py3-none-any.whl", hash = "sha256:1f81b79b873f130eeab14bb556417a7c66d38f3396b7f2fe3b958b3f9094f455", size = 140556, upload-time = "2026-03-20T19:39:50.298Z" }, ] [[package]] name = "botocore" -version = "1.42.66" +version = "1.42.73" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/ef/1c8f89da69b0c3742120e19a6ea72ec46ac0596294466924fdd4cf0f36bb/botocore-1.42.66.tar.gz", hash = "sha256:39756a21142b646de552d798dde2105759b0b8fa0d881a34c26d15bd4c9448fa", size = 14977446, upload-time = "2026-03-11T19:58:07.714Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/23/0c88ca116ef63b1ae77c901cd5d2095d22a8dbde9e80df74545db4a061b4/botocore-1.42.73.tar.gz", hash = "sha256:575858641e4949aaf2af1ced145b8524529edf006d075877af6b82ff96ad854c", size = 15008008, upload-time = "2026-03-20T19:39:40.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/6f/7b45ed2ca300c1ad38ecfc82c1368546d4a90512d9dff589ebbd182a7317/botocore-1.42.66-py3-none-any.whl", hash = "sha256:ac48af1ab527dfa08c4617c387413ca56a7f87780d7bfc1da34ef847a59219a5", size = 14653886, upload-time = "2026-03-11T19:58:04.922Z" }, + { url = "https://files.pythonhosted.org/packages/8e/65/971f3d55015f4d133a6ff3ad74cd39f4b8dd8f53f7775a3c2ad378ea5145/botocore-1.42.73-py3-none-any.whl", hash = "sha256:7b62e2a12f7a1b08eb7360eecd23bb16fe3b7ab7f5617cf91b25476c6f86a0fe", size = 14681861, upload-time = "2026-03-20T19:39:35.341Z" }, ] [[package]] @@ -1297,91 +1303,107 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/21/a2b1505639008ba2e6ef03733a81fc6cfd6a07ea6139a2b76421230b8dad/charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765", size = 283319, upload-time = "2026-03-06T06:00:26.433Z" }, - { url = "https://files.pythonhosted.org/packages/70/67/df234c29b68f4e1e095885c9db1cb4b69b8aba49cf94fac041db4aaf1267/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990", size = 189974, upload-time = "2026-03-06T06:00:28.222Z" }, - { url = "https://files.pythonhosted.org/packages/df/7f/fc66af802961c6be42e2c7b69c58f95cbd1f39b0e81b3365d8efe2a02a04/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2", size = 207866, upload-time = "2026-03-06T06:00:29.769Z" }, - { url = "https://files.pythonhosted.org/packages/c9/23/404eb36fac4e95b833c50e305bba9a241086d427bb2167a42eac7c4f7da4/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765", size = 203239, upload-time = "2026-03-06T06:00:31.086Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2f/8a1d989bfadd120c90114ab33e0d2a0cbde05278c1fc15e83e62d570f50a/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d", size = 196529, upload-time = "2026-03-06T06:00:32.608Z" }, - { url = "https://files.pythonhosted.org/packages/a5/0c/c75f85ff7ca1f051958bb518cd43922d86f576c03947a050fbedfdfb4f15/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8", size = 184152, upload-time = "2026-03-06T06:00:33.93Z" }, - { url = "https://files.pythonhosted.org/packages/f9/20/4ed37f6199af5dde94d4aeaf577f3813a5ec6635834cda1d957013a09c76/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412", size = 195226, upload-time = "2026-03-06T06:00:35.469Z" }, - { url = "https://files.pythonhosted.org/packages/28/31/7ba1102178cba7c34dcc050f43d427172f389729e356038f0726253dd914/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2", size = 192933, upload-time = "2026-03-06T06:00:36.83Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/f86443ab3921e6a60b33b93f4a1161222231f6c69bc24fb18f3bee7b8518/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1", size = 185647, upload-time = "2026-03-06T06:00:38.367Z" }, - { url = "https://files.pythonhosted.org/packages/82/44/08b8be891760f1f5a6d23ce11d6d50c92981603e6eb740b4f72eea9424e2/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4", size = 209533, upload-time = "2026-03-06T06:00:41.931Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/df114f23406199f8af711ddccfbf409ffbc5b7cdc18fa19644997ff0c9bb/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f", size = 195901, upload-time = "2026-03-06T06:00:43.978Z" }, - { url = "https://files.pythonhosted.org/packages/07/83/71ef34a76fe8aa05ff8f840244bda2d61e043c2ef6f30d200450b9f6a1be/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550", size = 204950, upload-time = "2026-03-06T06:00:45.202Z" }, - { url = "https://files.pythonhosted.org/packages/58/40/0253be623995365137d7dc68e45245036207ab2227251e69a3d93ce43183/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2", size = 198546, upload-time = "2026-03-06T06:00:46.481Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5c/5f3cb5b259a130895ef5ae16b38eaf141430fa3f7af50cd06c5d67e4f7b2/charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475", size = 132516, upload-time = "2026-03-06T06:00:47.924Z" }, - { url = "https://files.pythonhosted.org/packages/a5/c3/84fb174e7770f2df2e1a2115090771bfbc2227fb39a765c6d00568d1aab4/charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05", size = 142906, upload-time = "2026-03-06T06:00:49.389Z" }, - { url = "https://files.pythonhosted.org/packages/d7/b2/6f852f8b969f2cbd0d4092d2e60139ab1af95af9bb651337cae89ec0f684/charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064", size = 133258, upload-time = "2026-03-06T06:00:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531, upload-time = "2026-03-06T06:00:52.252Z" }, - { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006, upload-time = "2026-03-06T06:00:53.8Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085, upload-time = "2026-03-06T06:00:55.311Z" }, - { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545, upload-time = "2026-03-06T06:00:56.532Z" }, - { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863, upload-time = "2026-03-06T06:00:57.823Z" }, - { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827, upload-time = "2026-03-06T06:00:59.323Z" }, - { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085, upload-time = "2026-03-06T06:01:00.546Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688, upload-time = "2026-03-06T06:01:02.479Z" }, - { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077, upload-time = "2026-03-06T06:01:04.231Z" }, - { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706, upload-time = "2026-03-06T06:01:05.773Z" }, - { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665, upload-time = "2026-03-06T06:01:07.473Z" }, - { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950, upload-time = "2026-03-06T06:01:08.973Z" }, - { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830, upload-time = "2026-03-06T06:01:10.155Z" }, - { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029, upload-time = "2026-03-06T06:01:11.706Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404, upload-time = "2026-03-06T06:01:12.865Z" }, - { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796, upload-time = "2026-03-06T06:01:14.106Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, - { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, - { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, - { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, - { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, - { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, - { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, - { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, - { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, - { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, - { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, - { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, - { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, - { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, - { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, - { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, - { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, - { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, - { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, - { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, - { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, - { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, - { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, - { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, - { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, - { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, - { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, - { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, - { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, - { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, - { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, - { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, - { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, - { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" }, + { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" }, + { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" }, + { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" }, + { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" }, + { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, + { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, + { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] [[package]] @@ -1418,7 +1440,7 @@ name = "clr-loader" version = "0.2.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, + { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } wheels = [ @@ -1604,115 +1626,115 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, - { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, - { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, - { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, - { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, - { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, - { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, - { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, - { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, - { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, - { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, - { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, - { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, - { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, - { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, - { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, - { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, - { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, - { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, - { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, - { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, - { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, - { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, - { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, - { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, - { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, - { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, - { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, - { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, - { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, - { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, - { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, - { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, - { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, - { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, - { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, - { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, - { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, - { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, - { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, - { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, - { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, - { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, - { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, - { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, - { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, - { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, - { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, - { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, - { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, - { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, - { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, - { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, - { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [package.optional-dependencies] @@ -1722,15 +1744,14 @@ toml = [ [[package]] name = "croniter" -version = "6.0.0" +version = "6.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "pytz", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/2f/44d1ae153a0e27be56be43465e5cb39b9650c781e001e7864389deb25090/croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577", size = 64481, upload-time = "2024-12-17T17:17:47.32Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/5832661ed55107b8a09af3f0a2e71e0957226a59eb1dcf0a445cce6daf20/croniter-6.2.2.tar.gz", hash = "sha256:ba60832a5ec8e12e51b8691c3309a113d1cf6526bdf1a48150ce8ec7a532d0ab", size = 113762, upload-time = "2026-03-15T08:43:48.112Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/4b/290b4c3efd6417a8b0c284896de19b1d5855e6dbdb97d2a35e68fa42de85/croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368", size = 25468, upload-time = "2024-12-17T17:17:45.359Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/783980e78cb92c2d7bdb1fc7dbc86e94ccc6d58224d76a7f1f51b6c51e30/croniter-6.2.2-py3-none-any.whl", hash = "sha256:a5d17b1060974d36251ea4faf388233eca8acf0d09cbd92d35f4c4ac8f279960", size = 45422, upload-time = "2026-03-15T08:43:46.626Z" }, ] [[package]] @@ -1804,14 +1825,14 @@ wheels = [ [[package]] name = "deepdiff" -version = "8.6.1" +version = "8.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "orderly-set", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/76/36c9aab3d5c19a94091f7c6c6e784efca50d87b124bf026c36e94719f33c/deepdiff-8.6.1.tar.gz", hash = "sha256:ec56d7a769ca80891b5200ec7bd41eec300ced91ebcc7797b41eb2b3f3ff643a", size = 634054, upload-time = "2025-09-03T19:40:41.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/50/767448e792d41bfb6094ee317a355c1cb221dca24b2e178e2203bbea2a77/deepdiff-8.6.2.tar.gz", hash = "sha256:186dcbd181e4d76cef11ab05f802d0056c5d6083c5a6748c1473e9d7481e183e", size = 634860, upload-time = "2026-03-18T17:16:33.785Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b", size = 91378, upload-time = "2025-09-03T19:40:39.679Z" }, + { url = "https://files.pythonhosted.org/packages/2b/5f/c52bd1255db763d0cdcb7084d2e90c42119cb229302c56bdf1d0aa78abd2/deepdiff-8.6.2-py3-none-any.whl", hash = "sha256:4d22034a866c3928303a9332c279362f714192d9305bac17c498720d095fd1b4", size = 91979, upload-time = "2026-03-18T17:16:32.171Z" }, ] [[package]] @@ -2059,59 +2080,59 @@ wheels = [ [[package]] name = "fonttools" -version = "4.62.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/96/686339e0fda8142b7ebed39af53f4a5694602a729662f42a6209e3be91d0/fonttools-4.62.0.tar.gz", hash = "sha256:0dc477c12b8076b4eb9af2e440421b0433ffa9e1dcb39e0640a6c94665ed1098", size = 3579521, upload-time = "2026-03-09T16:50:06.217Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/e0/9db48ec7f6b95bae7b20667ded54f18dba8e759ef66232c8683822ae26fc/fonttools-4.62.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:62b6a3d0028e458e9b59501cf7124a84cd69681c433570e4861aff4fb54a236c", size = 2873527, upload-time = "2026-03-09T16:48:12.416Z" }, - { url = "https://files.pythonhosted.org/packages/dd/45/86eccfdc922cb9fafc63189a9793fa9f6dd60e68a07be42e454ef2c0deae/fonttools-4.62.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:966557078b55e697f65300b18025c54e872d7908d1899b7314d7c16e64868cb2", size = 2417427, upload-time = "2026-03-09T16:48:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/d3/98/f547a1fceeae81a9a5c6461bde2badac8bf50bda7122a8012b32b1e65396/fonttools-4.62.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf34861145b516cddd19b07ae6f4a61ea1c6326031b960ec9ddce8ee815e888", size = 4934993, upload-time = "2026-03-09T16:48:18.186Z" }, - { url = "https://files.pythonhosted.org/packages/5c/57/a23a051fcff998fdfabdd33c6721b5bad499da08b586d3676993410071f0/fonttools-4.62.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e2ff573de2775508c8a366351fb901c4ced5dc6cf2d87dd15c973bedcdd5216", size = 4892154, upload-time = "2026-03-09T16:48:20.736Z" }, - { url = "https://files.pythonhosted.org/packages/e2/62/e27644b433dc6db1d47bc6028a27d772eec5cc8338e24a9a1fce5d7120aa/fonttools-4.62.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:55b189a1b3033860a38e4e5bd0626c5aa25c7ce9caee7bc784a8caec7a675401", size = 4911635, upload-time = "2026-03-09T16:48:23.174Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e2/1bf141911a5616bacfe9cf237c80ccd69d0d92482c38c0f7f6a55d063ad9/fonttools-4.62.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:825f98cd14907c74a4d0a3f7db8570886ffce9c6369fed1385020febf919abf6", size = 5031492, upload-time = "2026-03-09T16:48:25.095Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/790c292f4347ecfa77d9c7e0d1d91e04ab227f6e4a337ed4fe37ca388048/fonttools-4.62.0-cp310-cp310-win32.whl", hash = "sha256:c858030560f92a054444c6e46745227bfd3bb4e55383c80d79462cd47289e4b5", size = 1507656, upload-time = "2026-03-09T16:48:26.973Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ee/08c0b7f8bac6e44638de6fe9a3e710a623932f60eccd58912c4d4743516d/fonttools-4.62.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bf75eb69330e34ad2a096fac67887102c8537991eb6cac1507fc835bbb70e0a", size = 1556540, upload-time = "2026-03-09T16:48:30.359Z" }, - { url = "https://files.pythonhosted.org/packages/e4/33/63d79ca41020dd460b51f1e0f58ad1ff0a36b7bcbdf8f3971d52836581e9/fonttools-4.62.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:196cafef9aeec5258425bd31a4e9a414b2ee0d1557bca184d7923d3d3bcd90f9", size = 2870816, upload-time = "2026-03-09T16:48:32.39Z" }, - { url = "https://files.pythonhosted.org/packages/c0/7a/9aeec114bc9fc00d757a41f092f7107863d372e684a5b5724c043654477c/fonttools-4.62.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:153afc3012ff8761b1733e8fbe5d98623409774c44ffd88fbcb780e240c11d13", size = 2416127, upload-time = "2026-03-09T16:48:34.627Z" }, - { url = "https://files.pythonhosted.org/packages/5a/71/12cfd8ae0478b7158ffa8850786781f67e73c00fd897ef9d053415c5f88b/fonttools-4.62.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13b663fb197334de84db790353d59da2a7288fd14e9be329f5debc63ec0500a5", size = 5100678, upload-time = "2026-03-09T16:48:36.454Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d7/8e4845993ee233c2023d11babe9b3dae7d30333da1d792eeccebcb77baab/fonttools-4.62.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:591220d5333264b1df0d3285adbdfe2af4f6a45bbf9ca2b485f97c9f577c49ff", size = 5070859, upload-time = "2026-03-09T16:48:38.786Z" }, - { url = "https://files.pythonhosted.org/packages/ae/a0/287ae04cd883a52e7bb1d92dfc4997dcffb54173761c751106845fa9e316/fonttools-4.62.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:579f35c121528a50c96bf6fcb6a393e81e7f896d4326bf40e379f1c971603db9", size = 5076689, upload-time = "2026-03-09T16:48:41.886Z" }, - { url = "https://files.pythonhosted.org/packages/6d/4e/a2377ad26c36fcd3e671a1c316ea5ed83107de1588e2d897a98349363bc7/fonttools-4.62.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:44956b003151d5a289eba6c71fe590d63509267c37e26de1766ba15d9c589582", size = 5202053, upload-time = "2026-03-09T16:48:43.867Z" }, - { url = "https://files.pythonhosted.org/packages/44/2e/ad0472e69b02f83dc88983a9910d122178461606404be5b4838af6d1744a/fonttools-4.62.0-cp311-cp311-win32.whl", hash = "sha256:42c7848fa8836ab92c23b1617c407a905642521ff2d7897fe2bf8381530172f1", size = 2292852, upload-time = "2026-03-09T16:48:46.962Z" }, - { url = "https://files.pythonhosted.org/packages/77/ce/f5a4c42c117f8113ce04048053c128d17426751a508f26398110c993a074/fonttools-4.62.0-cp311-cp311-win_amd64.whl", hash = "sha256:4da779e8f342a32856075ddb193b2a024ad900bc04ecb744014c32409ae871ed", size = 2344367, upload-time = "2026-03-09T16:48:48.818Z" }, - { url = "https://files.pythonhosted.org/packages/ab/9d/7ad1ffc080619f67d0b1e0fa6a0578f0be077404f13fd8e448d1616a94a3/fonttools-4.62.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22bde4dc12a9e09b5ced77f3b5053d96cf10c4976c6ac0dee293418ef289d221", size = 2870004, upload-time = "2026-03-09T16:48:50.837Z" }, - { url = "https://files.pythonhosted.org/packages/4d/8b/ba59069a490f61b737e064c3129453dbd28ee38e81d56af0d04d7e6b4de4/fonttools-4.62.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7199c73b326bad892f1cb53ffdd002128bfd58a89b8f662204fbf1daf8d62e85", size = 2414662, upload-time = "2026-03-09T16:48:53.295Z" }, - { url = "https://files.pythonhosted.org/packages/8c/8c/c52a4310de58deeac7e9ea800892aec09b00bb3eb0c53265b31ec02be115/fonttools-4.62.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d732938633681d6e2324e601b79e93f7f72395ec8681f9cdae5a8c08bc167e72", size = 5032975, upload-time = "2026-03-09T16:48:55.718Z" }, - { url = "https://files.pythonhosted.org/packages/0b/a1/d16318232964d786907b9b3613b8409f74cf0be2da400854509d3a864e43/fonttools-4.62.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a804c16d76038cc4e3826e07678efb0a02dc4f15396ea8e07088adbfb2578e", size = 4988544, upload-time = "2026-03-09T16:48:57.715Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8d/7e745ca3e65852adc5e52a83dc213fe1b07d61cb5b394970fcd4b1199d1e/fonttools-4.62.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:090e74ac86e68c20150e665ef8e7e0c20cb9f8b395302c9419fa2e4d332c3b51", size = 4971296, upload-time = "2026-03-09T16:48:59.678Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d4/b717a4874175146029ca1517e85474b1af80c9d9a306fc3161e71485eea5/fonttools-4.62.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8f086120e8be9e99ca1288aa5ce519833f93fe0ec6ebad2380c1dee18781f0b5", size = 5122503, upload-time = "2026-03-09T16:49:02.464Z" }, - { url = "https://files.pythonhosted.org/packages/cb/4b/92cfcba4bf8373f51c49c5ae4b512ead6fbda7d61a0e8c35a369d0db40a0/fonttools-4.62.0-cp312-cp312-win32.whl", hash = "sha256:37a73e5e38fd05c637daede6ffed5f3496096be7df6e4a3198d32af038f87527", size = 2281060, upload-time = "2026-03-09T16:49:04.385Z" }, - { url = "https://files.pythonhosted.org/packages/cd/06/cc96468781a4dc8ae2f14f16f32b32f69bde18cb9384aad27ccc7adf76f7/fonttools-4.62.0-cp312-cp312-win_amd64.whl", hash = "sha256:658ab837c878c4d2a652fcbb319547ea41693890e6434cf619e66f79387af3b8", size = 2331193, upload-time = "2026-03-09T16:49:06.598Z" }, - { url = "https://files.pythonhosted.org/packages/82/c7/985c1670aa6d82ef270f04cde11394c168f2002700353bd2bde405e59b8f/fonttools-4.62.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:274c8b8a87e439faf565d3bcd3f9f9e31bca7740755776a4a90a4bfeaa722efa", size = 2864929, upload-time = "2026-03-09T16:49:09.331Z" }, - { url = "https://files.pythonhosted.org/packages/c1/dc/c409c8ceec0d3119e9ab0b7b1a2e3c76d1f4d66e4a9db5c59e6b7652e7df/fonttools-4.62.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93e27131a5a0ae82aaadcffe309b1bae195f6711689722af026862bede05c07c", size = 2412586, upload-time = "2026-03-09T16:49:11.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ac/8e300dbf7b4d135287c261ffd92ede02d9f48f0d2db14665fbc8b059588a/fonttools-4.62.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83c6524c5b93bad9c2939d88e619fedc62e913c19e673f25d5ab74e7a5d074e5", size = 5013708, upload-time = "2026-03-09T16:49:14.063Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bc/60d93477b653eeb1ddf5f9ec34be689b79234d82dbdded269ac0252715b8/fonttools-4.62.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:106aec9226f9498fc5345125ff7200842c01eda273ae038f5049b0916907acee", size = 4964355, upload-time = "2026-03-09T16:49:16.515Z" }, - { url = "https://files.pythonhosted.org/packages/cb/eb/6dc62bcc3c3598c28a3ecb77e69018869c3e109bd83031d4973c059d318b/fonttools-4.62.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15d86b96c79013320f13bc1b15f94789edb376c0a2d22fb6088f33637e8dfcbc", size = 4953472, upload-time = "2026-03-09T16:49:18.494Z" }, - { url = "https://files.pythonhosted.org/packages/82/b3/3af7592d9b254b7b7fec018135f8776bfa0d1ad335476c2791b1334dc5e4/fonttools-4.62.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f16c07e5250d5d71d0f990a59460bc5620c3cc456121f2cfb5b60475699905f", size = 5094701, upload-time = "2026-03-09T16:49:21.67Z" }, - { url = "https://files.pythonhosted.org/packages/31/3d/976645583ab567d3ee75ff87b33aa1330fa2baeeeae5fc46210b4274dd45/fonttools-4.62.0-cp313-cp313-win32.whl", hash = "sha256:d31558890f3fa00d4f937d12708f90c7c142c803c23eaeb395a71f987a77ebe3", size = 2279710, upload-time = "2026-03-09T16:49:23.812Z" }, - { url = "https://files.pythonhosted.org/packages/f5/7a/e25245a30457595740041dba9d0ea8ec1b2517f2f1a6a741f15eba1a4edc/fonttools-4.62.0-cp313-cp313-win_amd64.whl", hash = "sha256:6826a5aa53fb6def8a66bf423939745f415546c4e92478a7c531b8b6282b6c3b", size = 2330291, upload-time = "2026-03-09T16:49:26.237Z" }, - { url = "https://files.pythonhosted.org/packages/1a/64/61f69298aa6e7c363dcf00dd6371a654676900abe27d1effd1a74b43e5d0/fonttools-4.62.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4fa5a9c716e2f75ef34b5a5c2ca0ee4848d795daa7e6792bf30fd4abf8993449", size = 2864222, upload-time = "2026-03-09T16:49:28.285Z" }, - { url = "https://files.pythonhosted.org/packages/c6/57/6b08756fe4455336b1fe160ab3c11fccc90768ccb6ee03fb0b45851aace4/fonttools-4.62.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:625f5cbeb0b8f4e42343eaeb4bc2786718ddd84760a2f5e55fdd3db049047c00", size = 2410674, upload-time = "2026-03-09T16:49:30.504Z" }, - { url = "https://files.pythonhosted.org/packages/6f/86/db65b63bb1b824b63e602e9be21b18741ddc99bcf5a7850f9181159ae107/fonttools-4.62.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6247e58b96b982709cd569a91a2ba935d406dccf17b6aa615afaed37ac3856aa", size = 4999387, upload-time = "2026-03-09T16:49:32.593Z" }, - { url = "https://files.pythonhosted.org/packages/86/c8/c6669e42d2f4efd60d38a3252cebbb28851f968890efb2b9b15f9d1092b0/fonttools-4.62.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:840632ea9c1eab7b7f01c369e408c0721c287dfd7500ab937398430689852fd1", size = 4912506, upload-time = "2026-03-09T16:49:34.927Z" }, - { url = "https://files.pythonhosted.org/packages/2e/49/0ae552aa098edd0ec548413fbf818f52ceb70535016215094a5ce9bf8f70/fonttools-4.62.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:28a9ea2a7467a816d1bec22658b0cce4443ac60abac3e293bdee78beb74588f3", size = 4951202, upload-time = "2026-03-09T16:49:37.1Z" }, - { url = "https://files.pythonhosted.org/packages/71/65/ae38fc8a4cea6f162d74cf11f58e9aeef1baa7d0e3d1376dabd336c129e5/fonttools-4.62.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ae611294f768d413949fd12693a8cba0e6332fbc1e07aba60121be35eac68d0", size = 5060758, upload-time = "2026-03-09T16:49:39.464Z" }, - { url = "https://files.pythonhosted.org/packages/db/3d/bb797496f35c60544cd5af71ffa5aad62df14ef7286908d204cb5c5096fe/fonttools-4.62.0-cp314-cp314-win32.whl", hash = "sha256:273acb61f316d07570a80ed5ff0a14a23700eedbec0ad968b949abaa4d3f6bb5", size = 2283496, upload-time = "2026-03-09T16:49:42.448Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9f/91081ffe5881253177c175749cce5841f5ec6e931f5d52f4a817207b7429/fonttools-4.62.0-cp314-cp314-win_amd64.whl", hash = "sha256:a5f974006d14f735c6c878fc4b117ad031dc93638ddcc450ca69f8fd64d5e104", size = 2335426, upload-time = "2026-03-09T16:49:44.228Z" }, - { url = "https://files.pythonhosted.org/packages/f8/65/f47f9b3db1ec156a1f222f1089ba076b2cc9ee1d024a8b0a60c54258517e/fonttools-4.62.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0361a7d41d86937f1f752717c19f719d0fde064d3011038f9f19bdf5fc2f5c95", size = 2947079, upload-time = "2026-03-09T16:49:46.471Z" }, - { url = "https://files.pythonhosted.org/packages/52/73/bc62e5058a0c22cf02b1e0169ef0c3ca6c3247216d719f95bead3c05a991/fonttools-4.62.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4108c12773b3c97aa592311557c405d5b4fc03db2b969ed928fcf68e7b3c887", size = 2448802, upload-time = "2026-03-09T16:49:48.328Z" }, - { url = "https://files.pythonhosted.org/packages/2b/df/bfaa0e845884935355670e6e68f137185ab87295f8bc838db575e4a66064/fonttools-4.62.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b448075f32708e8fb377fe7687f769a5f51a027172c591ba9a58693631b077a8", size = 5137378, upload-time = "2026-03-09T16:49:50.223Z" }, - { url = "https://files.pythonhosted.org/packages/32/32/04f616979a18b48b52e634988b93d847b6346260faf85ecccaf7e2e9057f/fonttools-4.62.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5f1fa8cc9f1a56a3e33ee6b954d6d9235e6b9d11eb7a6c9dfe2c2f829dc24db", size = 4920714, upload-time = "2026-03-09T16:49:53.172Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2e/274e16689c1dfee5c68302cd7c444213cfddd23cf4620374419625037ec6/fonttools-4.62.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f8c8ea812f82db1e884b9cdb663080453e28f0f9a1f5027a5adb59c4cc8d38d1", size = 5016012, upload-time = "2026-03-09T16:49:55.762Z" }, - { url = "https://files.pythonhosted.org/packages/7f/0c/b08117270626e7117ac2f89d732fdd4386ec37d2ab3a944462d29e6f89a1/fonttools-4.62.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:03c6068adfdc67c565d217e92386b1cdd951abd4240d65180cec62fa74ba31b2", size = 5042766, upload-time = "2026-03-09T16:49:57.726Z" }, - { url = "https://files.pythonhosted.org/packages/11/83/a48b73e54efa272ee65315a6331b30a9b3a98733310bc11402606809c50e/fonttools-4.62.0-cp314-cp314t-win32.whl", hash = "sha256:d28d5baacb0017d384df14722a63abe6e0230d8ce642b1615a27d78ffe3bc983", size = 2347785, upload-time = "2026-03-09T16:49:59.698Z" }, - { url = "https://files.pythonhosted.org/packages/f8/27/c67eab6dc3525bdc39586511b1b3d7161e972dacc0f17476dbaf932e708b/fonttools-4.62.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3f9e20c4618f1e04190c802acae6dc337cb6db9fa61e492fd97cd5c5a9ff6d07", size = 2413914, upload-time = "2026-03-09T16:50:02.251Z" }, - { url = "https://files.pythonhosted.org/packages/9c/57/c2487c281dde03abb2dec244fd67059b8d118bd30a653cbf69e94084cb23/fonttools-4.62.0-py3-none-any.whl", hash = "sha256:75064f19a10c50c74b336aa5ebe7b1f89fd0fb5255807bfd4b0c6317098f4af3", size = 1152427, upload-time = "2026-03-09T16:50:04.074Z" }, +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" }, + { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" }, + { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" }, + { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, ] [[package]] @@ -2319,16 +2340,15 @@ wheels = [ [[package]] name = "google-auth" -version = "2.49.0" +version = "2.49.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pyasn1-modules", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "rsa", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/59/7371175bfd949abfb1170aa076352131d7281bd9449c0f978604fc4431c3/google_auth-2.49.0.tar.gz", hash = "sha256:9cc2d9259d3700d7a257681f81052db6737495a1a46b610597f4b8bafe5286ae", size = 333444, upload-time = "2026-03-06T21:53:06.07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/45/de64b823b639103de4b63dd193480dce99526bd36be6530c2dba85bf7817/google_auth-2.49.0-py3-none-any.whl", hash = "sha256:f893ef7307f19cf53700b7e2f61b5a6affe3aa0edf9943b13788920ab92d8d87", size = 240676, upload-time = "2026-03-06T21:52:38.304Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, ] [[package]] @@ -2515,34 +2535,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/01/928fd82663fb0ab455551a178303a2960e65029da66b21974594f3a20a94/hf_xet-1.4.0.tar.gz", hash = "sha256:48e6ba7422b0885c9bbd8ac8fdf5c4e1306c3499b82d489944609cc4eae8ecbd", size = 660350, upload-time = "2026-03-11T18:50:03.354Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/4b/2351e30dddc6f3b47b3da0a0693ec1e82f8303b1a712faa299cf3552002b/hf_xet-1.4.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:76725fcbc5f59b23ac778f097d3029d6623e3cf6f4057d99d1fce1a7e3cff8fc", size = 3796397, upload-time = "2026-03-11T18:49:47.382Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/3db90ec0afb4e26e3330b1346b89fe0e9a3b7bfc2d6a2b2262787790d25f/hf_xet-1.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:76f1f73bee81a6e6f608b583908aa24c50004965358ac92c1dc01080a21bcd09", size = 3556235, upload-time = "2026-03-11T18:49:45.785Z" }, - { url = "https://files.pythonhosted.org/packages/57/6e/2a662af2cbc6c0a64ebe9fcdb8faf05b5205753d45a75a3011bb2209d0b4/hf_xet-1.4.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1818c2e5d6f15354c595d5111c6eb0e5a30a6c5c1a43eeaec20f19607cff0b34", size = 4213145, upload-time = "2026-03-11T18:49:38.009Z" }, - { url = "https://files.pythonhosted.org/packages/b9/4a/47c129affb540767e0e3e101039a95f4a73a292ec689c26e8f0c5b633f9d/hf_xet-1.4.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:70764d295f485db9cc9a6af76634ea00ec4f96311be7485f8f2b6144739b4ccf", size = 3991951, upload-time = "2026-03-11T18:49:36.396Z" }, - { url = "https://files.pythonhosted.org/packages/76/81/ec516cfc6281cfeef027b0919166b2fe11ab61fbe6131a2c43fafbed8b68/hf_xet-1.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d3bd2a1e289f772c715ca88cdca8ceb3d8b5c9186534d5925410e531d849a3e", size = 4193205, upload-time = "2026-03-11T18:49:54.415Z" }, - { url = "https://files.pythonhosted.org/packages/49/48/0945b5e542ed6c6ce758b589b27895a449deab630dfcdee5a6ee0f699d21/hf_xet-1.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:06da3797f1fdd9a8f8dbc8c1bddfa0b914789b14580c375d29c32ee35c2c66ca", size = 4431022, upload-time = "2026-03-11T18:49:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ad/a4859c55ab4b67a4fde2849be8bde81917f54062050419b821071f199a9c/hf_xet-1.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:30b9d8f384ccec848124d51d883e91f3c88d430589e02a7b6d867730ab8d53ac", size = 3674977, upload-time = "2026-03-11T18:50:06.369Z" }, - { url = "https://files.pythonhosted.org/packages/4b/17/5bf3791e3a53e597913c2a775a48a98aaded9c2ddb5d1afaedabb55e2ed8/hf_xet-1.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:07ffdbf7568fa3245b24d949f0f3790b5276fb7293a5554ac4ec02e5f7e2b38d", size = 3536778, upload-time = "2026-03-11T18:50:04.974Z" }, - { url = "https://files.pythonhosted.org/packages/3f/a1/05a7f9d6069bf78405d3fc2464b6c76b167128501e13b4f1d6266e1d1f54/hf_xet-1.4.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e2731044f3a18442f9f7a3dcf03b96af13dee311f03846a1df1f0553a3ea0fc6", size = 3796727, upload-time = "2026-03-11T18:49:52.889Z" }, - { url = "https://files.pythonhosted.org/packages/ac/8a/67abc642c2b32efcb7a257cdad8555c2904e23f18a1b4fec3aef1ebfe0fc/hf_xet-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b6f3729335fbc4baef60fe14fe32ef13ac9d377bdc898148c541e20c6056b504", size = 3555869, upload-time = "2026-03-11T18:49:51.313Z" }, - { url = "https://files.pythonhosted.org/packages/19/3d/4765367c64ee70db15fa771d5b94bf12540b85076a1d3210ebbfec42d477/hf_xet-1.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c0c9f052738a024073d332c573275c8e33697a3ef3f5dd2fb4ef98216e1e74a", size = 4212980, upload-time = "2026-03-11T18:49:44.21Z" }, - { url = "https://files.pythonhosted.org/packages/0e/bf/6ad99ee0e7ca2318f912a87318e493d82d8f9aace6be81f774bd14b996df/hf_xet-1.4.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f44b2324be75bfa399735996ac299fd478684c48ce47d12a42b5f24b1a99ccb8", size = 3991136, upload-time = "2026-03-11T18:49:42.512Z" }, - { url = "https://files.pythonhosted.org/packages/50/aa/932e25c69699076088f57e3c14f83ccae87bac25e755994f3362acc908d5/hf_xet-1.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:01de78b1ceddf8b38da001f7cc728b3bc3eb956948b18e8a1997ad6fc80fbe9d", size = 4192676, upload-time = "2026-03-11T18:50:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/5c/0a/5e41339a294fd3450948989a47ecba9824d5bc1950cf767f928ecaf53a55/hf_xet-1.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cac8616e7a974105c3494735313f5ab0fb79b5accadec1a7a992859a15536a9", size = 4430729, upload-time = "2026-03-11T18:50:01.923Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c1/c3d8ed9b7118e9166b0cf71dfd501da82f1abe306387e34e0f3ee59553ec/hf_xet-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3a5d9cb25095ceb3beab4843ae2d1b3e5746371ddbf2e5849f7be6a7d6f44df4", size = 3674989, upload-time = "2026-03-11T18:50:12.633Z" }, - { url = "https://files.pythonhosted.org/packages/65/bc/ea26cf774063cb09d7aaaa6cba9d341fb72b42ea99b8a94ca254dbafbbb0/hf_xet-1.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9b777674499dc037317db372c90a2dd91329b5f1ee93c645bb89155bb974f5bf", size = 3536805, upload-time = "2026-03-11T18:50:11.082Z" }, - { url = "https://files.pythonhosted.org/packages/9f/f9/a0b01945726aea81d2f213457cd5f5102a51e6fd1ca9f9769f561fb57501/hf_xet-1.4.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:981d2b5222c3baadf9567c135cf1d1073786f546b7745686978d46b5df179e16", size = 3799223, upload-time = "2026-03-11T18:49:49.884Z" }, - { url = "https://files.pythonhosted.org/packages/5d/30/ee62b0c00412f49a7e6f509f0104ee8808692278d247234336df48029349/hf_xet-1.4.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:cc8bd050349d0d7995ce7b3a3a18732a2a8062ce118a82431602088abb373428", size = 3560682, upload-time = "2026-03-11T18:49:48.633Z" }, - { url = "https://files.pythonhosted.org/packages/93/d0/0fe5c44dbced465a651a03212e1135d0d7f95d19faada692920cb56f8e38/hf_xet-1.4.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5d0c38d2a280d814280b8c15eead4a43c9781e7bf6fc37843cffab06dcdc76b9", size = 4218323, upload-time = "2026-03-11T18:49:40.921Z" }, - { url = "https://files.pythonhosted.org/packages/73/df/7b3c99a4e50442039eae498e5c23db634538eb3e02214109880cf1165d4c/hf_xet-1.4.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6a883f0250682ea888a1bd0af0631feda377e59ad7aae6fb75860ecee7ae0f93", size = 3997156, upload-time = "2026-03-11T18:49:39.634Z" }, - { url = "https://files.pythonhosted.org/packages/a9/26/47dfedf271c21d95346660ae1698e7ece5ab10791fa6c4f20c59f3713083/hf_xet-1.4.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:99e1d9255fe8ecdf57149bb0543d49e7b7bd8d491ddf431eb57e114253274df5", size = 4199052, upload-time = "2026-03-11T18:49:57.097Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c0/346b9aad1474e881e65f998d5c1981695f0af045bc7a99204d9d86759a89/hf_xet-1.4.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b25f06ce42bd2d5f2e79d4a2d72f783d3ac91827c80d34a38cf8e5290dd717b0", size = 4434346, upload-time = "2026-03-11T18:49:58.67Z" }, - { url = "https://files.pythonhosted.org/packages/2e/d6/88ce9d6caa397c3b935263d5bcbe3ebf6c443f7c76098b8c523d206116b9/hf_xet-1.4.0-cp37-abi3-win_amd64.whl", hash = "sha256:8d6d7816d01e0fa33f315c8ca21b05eca0ce4cdc314f13b81d953e46cc6db11d", size = 3678921, upload-time = "2026-03-11T18:50:09.496Z" }, - { url = "https://files.pythonhosted.org/packages/65/eb/17d99ed253b28a9550ca479867c66a8af4c9bcd8cdc9a26b0c8007c2000a/hf_xet-1.4.0-cp37-abi3-win_arm64.whl", hash = "sha256:cb8d9549122b5b42f34b23b14c6b662a88a586a919d418c774d8dbbc4b3ce2aa", size = 3541054, upload-time = "2026-03-11T18:50:07.963Z" }, +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/08/23c84a26716382c89151b5b447b4beb19e3345f3a93d3b73009a71a57ad3/hf_xet-1.4.2.tar.gz", hash = "sha256:b7457b6b482d9e0743bd116363239b1fa904a5e65deede350fbc0c4ea67c71ea", size = 672357, upload-time = "2026-03-13T06:58:51.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/06/e8cf74c3c48e5485c7acc5a990d0d8516cdfb5fdf80f799174f1287cc1b5/hf_xet-1.4.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ac8202ae1e664b2c15cdfc7298cbb25e80301ae596d602ef7870099a126fcad4", size = 3796125, upload-time = "2026-03-13T06:58:33.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/d4/b73ebab01cbf60777323b7de9ef05550790451eb5172a220d6b9845385ec/hf_xet-1.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d2f8ee39fa9fba9af929f8c0d0482f8ee6e209179ad14a909b6ad78ffcb7c81", size = 3555985, upload-time = "2026-03-13T06:58:31.797Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e7/ded6d1bd041c3f2bca9e913a0091adfe32371988e047dd3a68a2463c15a2/hf_xet-1.4.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4642a6cf249c09da8c1f87fe50b24b2a3450b235bf8adb55700b52f0ea6e2eb6", size = 4212085, upload-time = "2026-03-13T06:58:24.323Z" }, + { url = "https://files.pythonhosted.org/packages/97/c1/a0a44d1f98934f7bdf17f7a915b934f9fca44bb826628c553589900f6df8/hf_xet-1.4.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:769431385e746c92dc05492dde6f687d304584b89c33d79def8367ace06cb555", size = 3988266, upload-time = "2026-03-13T06:58:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/7a/82/be713b439060e7d1f1d93543c8053d4ef2fe7e6922c5b31642eaa26f3c4b/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c9dd1c1bc4cc56168f81939b0e05b4c36dd2d28c13dc1364b17af89aa0082496", size = 4188513, upload-time = "2026-03-13T06:58:40.858Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/cbd4188b22abd80ebd0edbb2b3e87f2633e958983519980815fb8314eae5/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fca58a2ae4e6f6755cc971ac6fcdf777ea9284d7e540e350bb000813b9a3008d", size = 4428287, upload-time = "2026-03-13T06:58:42.601Z" }, + { url = "https://files.pythonhosted.org/packages/b2/4e/84e45b25e2e3e903ed3db68d7eafa96dae9a1d1f6d0e7fc85120347a852f/hf_xet-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:163aab46854ccae0ab6a786f8edecbbfbaa38fcaa0184db6feceebf7000c93c0", size = 3665574, upload-time = "2026-03-13T06:58:53.881Z" }, + { url = "https://files.pythonhosted.org/packages/ee/71/c5ac2b9a7ae39c14e91973035286e73911c31980fe44e7b1d03730c00adc/hf_xet-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:09b138422ecbe50fd0c84d4da5ff537d27d487d3607183cd10e3e53f05188e82", size = 3528760, upload-time = "2026-03-13T06:58:52.187Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/fcd2504015eab26358d8f0f232a1aed6b8d363a011adef83fe130bff88f7/hf_xet-1.4.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:949dcf88b484bb9d9276ca83f6599e4aa03d493c08fc168c124ad10b2e6f75d7", size = 3796493, upload-time = "2026-03-13T06:58:39.267Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/19c25105ff81731ca6d55a188b5de2aa99d7a2644c7aa9de1810d5d3b726/hf_xet-1.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41659966020d59eb9559c57de2cde8128b706a26a64c60f0531fa2318f409418", size = 3555797, upload-time = "2026-03-13T06:58:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/8933c073186849b5e06762aa89847991d913d10a95d1603eb7f2c3834086/hf_xet-1.4.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c588e21d80010119458dd5d02a69093f0d115d84e3467efe71ffb2c67c19146", size = 4212127, upload-time = "2026-03-13T06:58:30.539Z" }, + { url = "https://files.pythonhosted.org/packages/eb/01/f89ebba4e369b4ed699dcb60d3152753870996f41c6d22d3d7cac01310e1/hf_xet-1.4.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a296744d771a8621ad1d50c098d7ab975d599800dae6d48528ba3944e5001ba0", size = 3987788, upload-time = "2026-03-13T06:58:29.139Z" }, + { url = "https://files.pythonhosted.org/packages/84/4d/8a53e5ffbc2cc33bbf755382ac1552c6d9af13f623ed125fe67cc3e6772f/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f563f7efe49588b7d0629d18d36f46d1658fe7e08dce3fa3d6526e1c98315e2d", size = 4188315, upload-time = "2026-03-13T06:58:48.017Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b8/b7a1c1b5592254bd67050632ebbc1b42cc48588bf4757cb03c2ef87e704a/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5b2e0132c56d7ee1bf55bdb638c4b62e7106f6ac74f0b786fed499d5548c5570", size = 4428306, upload-time = "2026-03-13T06:58:49.502Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/40779e45b20e11c7c5821a94135e0207080d6b3d76e7b78ccb413c6f839b/hf_xet-1.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2f45c712c2fa1215713db10df6ac84b49d0e1c393465440e9cb1de73ecf7bbf6", size = 3665826, upload-time = "2026-03-13T06:58:59.88Z" }, + { url = "https://files.pythonhosted.org/packages/51/4c/e2688c8ad1760d7c30f7c429c79f35f825932581bc7c9ec811436d2f21a0/hf_xet-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6d53df40616f7168abfccff100d232e9d460583b9d86fa4912c24845f192f2b8", size = 3529113, upload-time = "2026-03-13T06:58:58.491Z" }, + { url = "https://files.pythonhosted.org/packages/b4/86/b40b83a2ff03ef05c4478d2672b1fc2b9683ff870e2b25f4f3af240f2e7b/hf_xet-1.4.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:71f02d6e4cdd07f344f6844845d78518cc7186bd2bc52d37c3b73dc26a3b0bc5", size = 3800339, upload-time = "2026-03-13T06:58:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/af4475c32b4378b0e92a587adb1aa3ec53e3450fd3e5fe0372a874531c00/hf_xet-1.4.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e9b38d876e94d4bdcf650778d6ebbaa791dd28de08db9736c43faff06ede1b5a", size = 3559664, upload-time = "2026-03-13T06:58:34.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4c/781267da3188db679e601de18112021a5cb16506fe86b246e22c5401a9c4/hf_xet-1.4.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:77e8c180b7ef12d8a96739a4e1e558847002afe9ea63b6f6358b2271a8bdda1c", size = 4217422, upload-time = "2026-03-13T06:58:27.472Z" }, + { url = "https://files.pythonhosted.org/packages/68/47/d6cf4a39ecf6c7705f887a46f6ef5c8455b44ad9eb0d391aa7e8a2ff7fea/hf_xet-1.4.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3b3c6a882016b94b6c210957502ff7877802d0dbda8ad142c8595db8b944271", size = 3992847, upload-time = "2026-03-13T06:58:25.989Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ef/e80815061abff54697239803948abc665c6b1d237102c174f4f7a9a5ffc5/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d9a634cc929cfbaf2e1a50c0e532ae8c78fa98618426769480c58501e8c8ac2", size = 4193843, upload-time = "2026-03-13T06:58:44.59Z" }, + { url = "https://files.pythonhosted.org/packages/54/75/07f6aa680575d9646c4167db6407c41340cbe2357f5654c4e72a1b01ca14/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b0932eb8b10317ea78b7da6bab172b17be03bbcd7809383d8d5abd6a2233e04", size = 4432751, upload-time = "2026-03-13T06:58:46.533Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/193eabd7e7d4b903c4aa983a215509c6114915a5a237525ec562baddb868/hf_xet-1.4.2-cp37-abi3-win_amd64.whl", hash = "sha256:ad185719fb2e8ac26f88c8100562dbf9dbdcc3d9d2add00faa94b5f106aea53f", size = 3671149, upload-time = "2026-03-13T06:58:57.07Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7e/ccf239da366b37ba7f0b36095450efae4a64980bdc7ec2f51354205fdf39/hf_xet-1.4.2-cp37-abi3-win_arm64.whl", hash = "sha256:32c012286b581f783653e718c1862aea5b9eb140631685bb0c5e7012c8719a87", size = 3533426, upload-time = "2026-03-13T06:58:55.46Z" }, ] [[package]] @@ -2650,7 +2670,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.6.0" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -2663,9 +2683,9 @@ dependencies = [ { name = "typer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/7a/304cec37112382c4fe29a43bcb0d5891f922785d18745883d2aa4eb74e4b/huggingface_hub-1.6.0.tar.gz", hash = "sha256:d931ddad8ba8dfc1e816bf254810eb6f38e5c32f60d4184b5885662a3b167325", size = 717071, upload-time = "2026-03-06T14:19:18.524Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/15/eafc1c57bf0f8afffb243dcd4c0cceb785e956acc17bba4d9bf2ae21fc9c/huggingface_hub-1.7.2.tar.gz", hash = "sha256:7f7e294e9bbb822e025bdb2ada025fa4344d978175a7f78e824d86e35f7ab43b", size = 724684, upload-time = "2026-03-20T10:36:08.767Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/e3/e3a44f54c8e2f28983fcf07f13d4260b37bd6a0d3a081041bc60b91d230e/huggingface_hub-1.6.0-py3-none-any.whl", hash = "sha256:ef40e2d5cb85e48b2c067020fa5142168342d5108a1b267478ed384ecbf18961", size = 612874, upload-time = "2026-03-06T14:19:16.844Z" }, + { url = "https://files.pythonhosted.org/packages/08/de/3ad061a05f74728927ded48c90b73521b9a9328c85d841bdefb30e01fb85/huggingface_hub-1.7.2-py3-none-any.whl", hash = "sha256:288f33a0a17b2a73a1359e2a5fd28d1becb2c121748c6173ab8643fb342c850e", size = 618036, upload-time = "2026-03-20T10:36:06.824Z" }, ] [[package]] @@ -3014,7 +3034,7 @@ wheels = [ [[package]] name = "langfuse" -version = "4.0.0" +version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3027,9 +3047,9 @@ dependencies = [ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/4b/8df7cd1684b46b6760d9c03893cbbc9ddbdd3f72eaf003b3859cec308587/langfuse-4.0.0.tar.gz", hash = "sha256:10df126c8d68e5746ff39a0a5100233f9f29446626c478e7770b1c775e4c4e17", size = 271030, upload-time = "2026-03-10T16:21:51.748Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/94/ab00e21fa5977d6b9c68fb3a95de2aa1a1e586964ff2af3e37405bf65d9f/langfuse-4.0.1.tar.gz", hash = "sha256:40a6daf3ab505945c314246d5b577d48fcfde0a47e8c05267ea6bd494ae9608e", size = 272749, upload-time = "2026-03-19T14:03:34.508Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/56/7f14cbe189e8a10c805609d52a4578b5f1bca3d4060b531baf920827d4f5/langfuse-4.0.0-py3-none-any.whl", hash = "sha256:4afe6a114937fa544e7f5f86c34533c711f0f12ebf77480239b626da28bbae68", size = 462159, upload-time = "2026-03-10T16:21:49.701Z" }, + { url = "https://files.pythonhosted.org/packages/27/8f/3145ef00940f9c29d7e0200fd040f35616eac21c6ab4610a1ba14f3a04c1/langfuse-4.0.1-py3-none-any.whl", hash = "sha256:e22f49ea31304f97fc31a97c014ba63baa8802d9568295d54f06b00b43c30524", size = 465049, upload-time = "2026-03-19T14:03:32.527Z" }, ] [[package]] @@ -3119,7 +3139,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.82.1" +version = "1.82.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3135,9 +3155,9 @@ dependencies = [ { name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/bd/6251e9a965ae2d7bc3342ae6c1a2d25dd265d354c502e63225451b135016/litellm-1.82.1.tar.gz", hash = "sha256:bc8427cdccc99e191e08e36fcd631c93b27328d1af789839eb3ac01a7d281890", size = 17197496, upload-time = "2026-03-10T09:10:04.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/75/1c537aa458426a9127a92bc2273787b2f987f4e5044e21f01f2eed5244fd/litellm-1.82.6.tar.gz", hash = "sha256:2aa1c2da21fe940c33613aa447119674a3ad4d2ad5eb064e4d5ce5ee42420136", size = 17414147, upload-time = "2026-03-22T06:36:00.452Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/77/0c6eca2cb049793ddf8ce9cdcd5123a35666c4962514788c4fc90edf1d3b/litellm-1.82.1-py3-none-any.whl", hash = "sha256:a9ec3fe42eccb1611883caaf8b1bf33c9f4e12163f94c7d1004095b14c379eb2", size = 15341896, upload-time = "2026-03-10T09:10:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/02/6c/5327667e6dbe9e98cbfbd4261c8e91386a52e38f41419575854248bbab6a/litellm-1.82.6-py3-none-any.whl", hash = "sha256:164a3ef3e19f309e3cabc199bef3d2045212712fefdfa25fc7f75884a5b5b205", size = 15591595, upload-time = "2026-03-22T06:35:56.795Z" }, ] [package.optional-dependencies] @@ -3171,20 +3191,20 @@ proxy = [ [[package]] name = "litellm-enterprise" -version = "0.1.34" +version = "0.1.35" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/ca/1c0bf58bbce062ad53d8f6ba85bc56e92a869b969f8ad7cd68d50423f42a/litellm_enterprise-0.1.34.tar.gz", hash = "sha256:d6fe43ef28728c1a6c131ba22667f1a8304035b70b435aa3e6cf1c2b91e84657", size = 57609, upload-time = "2026-03-09T11:12:12.162Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/5f/e593f335698a5c70d7e96e8ab9fdc4cfd4cc9249c524723fe64ed7f00cbb/litellm_enterprise-0.1.35.tar.gz", hash = "sha256:b752d07e538424743fcc08ba0d3d9d83d1f04a45c115811ac7828d789b6d87cc", size = 58817, upload-time = "2026-03-21T15:06:16.519Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/ad/23143b786081c8ebe1481a97e08058a6a5e9d5fc7fc4507256040aebcd42/litellm_enterprise-0.1.34-py3-none-any.whl", hash = "sha256:e2e8d084055f8c96e646d906d7dbee8bafee03d343247a349e8ccf2745fb7822", size = 121091, upload-time = "2026-03-09T11:12:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fa/39efe3dfa680ca5bc5795b9c904c914b09a65278c2970c8fece6e0e30e47/litellm_enterprise-0.1.35-py3-none-any.whl", hash = "sha256:8d2d9c925de8ee35e308c0f4975483b60f5e22beb50506e261e555e466f019c5", size = 122659, upload-time = "2026-03-21T15:06:15.586Z" }, ] [[package]] name = "litellm-proxy-extras" -version = "0.4.54" +version = "0.4.60" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/b8/21a14fc27fb6d10f22c0758db63f4c1224fe8ea8aa4c7d0a26d5fa9da7b2/litellm_proxy_extras-0.4.54.tar.gz", hash = "sha256:2c777ecdf39901c4007ade4466eb6398985ed4000afe3fc2cac997e1169e8cee", size = 31265, upload-time = "2026-03-12T01:08:08.86Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/00/828092491c0106657f9cb9ee43ac6ed71d13e9eba627d1e81c0c68b6126d/litellm_proxy_extras-0.4.60.tar.gz", hash = "sha256:1c122f2a7e0eb58fa4c6d8da9da82ac1fe2869de3510bcfade5c2932af202328", size = 32034, upload-time = "2026-03-22T05:54:55.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7e/8dd3378eba2c7116562b6bd823fa929872e20bdcab93757358e6f3b4c9e3/litellm_proxy_extras-0.4.54-py3-none-any.whl", hash = "sha256:6621cf529f7f3647eb2dd0d2c417d91db8c7a05c3c592bef251887a122928837", size = 73661, upload-time = "2026-03-12T01:08:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e8/828213b07512e673403da306a804dbe9b2965fcb7286d746c4bbff585b61/litellm_proxy_extras-0.4.60-py3-none-any.whl", hash = "sha256:7abcc811f7430e4b24e7a8ba7186219a4845a955ae7a71d8822bd03fd9fc3393", size = 76605, upload-time = "2026-03-22T05:54:54.41Z" }, ] [[package]] @@ -3413,7 +3433,7 @@ wheels = [ [[package]] name = "mem0ai" -version = "1.0.5" +version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3424,9 +3444,9 @@ dependencies = [ { name = "qdrant-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "sqlalchemy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/79/2307e5fe1610d2ad0d08688af10cd5163861390deeb070f83449c0b65417/mem0ai-1.0.5.tar.gz", hash = "sha256:0835a0001ecac40ba2667bbf17629329c1b2f33eaa585e93a6be54d868a82f79", size = 182982, upload-time = "2026-03-03T22:27:09.488Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/a7/ad272ecb8e67690d08f6fb59d7162260dbbbaf9f1b9336c52bef254bfbcd/mem0ai-1.0.7.tar.gz", hash = "sha256:57ec923d9703cd0f9bc0ffac511d2839ed99448a28c9ad8c017335083846d338", size = 188641, upload-time = "2026-03-20T22:46:24.734Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/0e/43ec9f125ebe6e8390805aa56237ee7165fc4f2b796122644cb0043e6631/mem0ai-1.0.5-py3-none-any.whl", hash = "sha256:0526814d2ec9134e21a628cc04ae0e6dc1779a579af92c481cb9fd7f7b8d17aa", size = 275991, upload-time = "2026-03-03T22:27:07.73Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8e/c7a6dbf2dfadd603fbfb17cd9c201b26a98382cb5c5e4c07b101978faf09/mem0ai-1.0.7-py3-none-any.whl", hash = "sha256:a0a2e9d75ef0e98f9b2579e2d40beb05b87b9e05b65304084c13b0d408ee5f1c", size = 287025, upload-time = "2026-03-20T22:46:23.238Z" }, ] [[package]] @@ -3945,7 +3965,7 @@ wheels = [ [[package]] name = "openai" -version = "2.26.0" +version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3957,14 +3977,14 @@ dependencies = [ { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/91/2a06c4e9597c338cac1e5e5a8dd6f29e1836fc229c4c523529dca387fda8/openai-2.26.0.tar.gz", hash = "sha256:b41f37c140ae0034a6e92b0c509376d907f3a66109935fba2c1b471a7c05a8fb", size = 666702, upload-time = "2026-03-05T23:17:35.874Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/15/203d537e58986b5673e7f232453a2a2f110f22757b15921cbdeea392e520/openai-2.29.0.tar.gz", hash = "sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec", size = 671128, upload-time = "2026-03-17T17:53:49.599Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/2e/3f73e8ca53718952222cacd0cf7eecc9db439d020f0c1fe7ae717e4e199a/openai-2.26.0-py3-none-any.whl", hash = "sha256:6151bf8f83802f036117f06cc8a57b3a4da60da9926826cc96747888b57f394f", size = 1136409, upload-time = "2026-03-05T23:17:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533, upload-time = "2026-03-17T17:53:47.348Z" }, ] [[package]] name = "openai-agents" -version = "0.12.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3975,9 +3995,9 @@ dependencies = [ { name = "types-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/2e/402d3bfd6432c503bab699ece49e6febe38c64ade3365ae4fe31e7b3cba1/openai_agents-0.12.0.tar.gz", hash = "sha256:086d5cd16815d40a88231cbfd9dcca594cdf8596c6efd4859dcbafdfb31068ba", size = 2604305, upload-time = "2026-03-12T08:52:42.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/df/68927da38588f7b9c418754f2a0c30c9cda1d8621b035906faf85767dda5/openai_agents-0.13.0.tar.gz", hash = "sha256:90ac13697dec3c110c3ed9893629e01b6fc178ae410a7f0e39f387be408e8715", size = 2660070, upload-time = "2026-03-23T06:20:23.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/2c/8f03b5a56329559573e692d6dc2f02c3cbbe4fcd07f9c5d81b3c280e80e7/openai_agents-0.12.0-py3-none-any.whl", hash = "sha256:24f5cc5d6213dfcda42188918ad0a739861aa505f4ef738ee07b69169faf5c09", size = 446876, upload-time = "2026-03-12T08:52:40.779Z" }, + { url = "https://files.pythonhosted.org/packages/e0/8d/62cf7374a2050daa6b7605c12a9085fa528d493f9cf076826c0c78ac16f7/openai_agents-0.13.0-py3-none-any.whl", hash = "sha256:d1077e71e9c7461f9098922bbc63a1f2a1244c93fd3dc24249882b3130eccd55", size = 454617, upload-time = "2026-03-23T06:20:22.068Z" }, ] [[package]] @@ -4556,30 +4576,30 @@ wheels = [ [[package]] name = "polars" -version = "1.38.1" +version = "1.39.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "polars-runtime-32", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/5e/208a24471a433bcd0e9a6889ac49025fd4daad2815c8220c5bd2576e5f1b/polars-1.38.1.tar.gz", hash = "sha256:803a2be5344ef880ad625addfb8f641995cfd777413b08a10de0897345778239", size = 717667, upload-time = "2026-02-06T18:13:23.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/ab/f19e592fce9e000da49c96bf35e77cef67f9cb4b040bfa538a2764c0263e/polars-1.39.3.tar.gz", hash = "sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c", size = 728987, upload-time = "2026-03-20T11:16:24.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl", hash = "sha256:a29479c48fed4984d88b656486d221f638cba45d3e961631a50ee5fdde38cb2c", size = 810368, upload-time = "2026-02-06T18:11:55.819Z" }, + { url = "https://files.pythonhosted.org/packages/b4/db/08f4ca10c5018813e7e0b59e4472302328b3d2ab1512f5a2157a814540e0/polars-1.39.3-py3-none-any.whl", hash = "sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56", size = 823985, upload-time = "2026-03-20T11:14:23.619Z" }, ] [[package]] name = "polars-runtime-32" -version = "1.38.1" +version = "1.39.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/4b/04d6b3fb7cf336fbe12fbc4b43f36d1783e11bb0f2b1e3980ec44878df06/polars_runtime_32-1.38.1.tar.gz", hash = "sha256:04f20ed1f5c58771f34296a27029dc755a9e4b1390caeaef8f317e06fdfce2ec", size = 2812631, upload-time = "2026-02-06T18:13:25.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/39/c8688696bc22b6c501e3b82ef3be10e543c07a785af5660f30997cd22dd2/polars_runtime_32-1.39.3.tar.gz", hash = "sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d", size = 2872335, upload-time = "2026-03-20T11:16:26.581Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/a2/a00defbddadd8cf1042f52380dcba6b6592b03bac8e3b34c436b62d12d3b/polars_runtime_32-1.38.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:18154e96044724a0ac38ce155cf63aa03c02dd70500efbbf1a61b08cadd269ef", size = 44108001, upload-time = "2026-02-06T18:11:58.127Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fb/599ff3709e6a303024efd7edfd08cf8de55c6ac39527d8f41cbc4399385f/polars_runtime_32-1.38.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c49acac34cc4049ed188f1eb67d6ff3971a39b4af7f7b734b367119970f313ac", size = 40230140, upload-time = "2026-02-06T18:12:01.181Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8c/3ac18d6f89dc05fe2c7c0ee1dc5b81f77a5c85ad59898232c2500fe2ebbf/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fef2ef2626a954e010e006cc8e4de467ecf32d08008f130cea1c78911f545323", size = 41994039, upload-time = "2026-02-06T18:12:04.332Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5a/61d60ec5cc0ab37cbd5a699edb2f9af2875b7fdfdfb2a4608ca3cc5f0448/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5f7a8125e2d50e2e060296551c929aec09be23a9edcb2b12ca923f555a5ba", size = 45755804, upload-time = "2026-02-06T18:12:07.846Z" }, - { url = "https://files.pythonhosted.org/packages/91/54/02cd4074c98c361ccd3fec3bcb0bd68dbc639c0550c42a4436b0ff0f3ccf/polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:10d19cd9863e129273b18b7fcaab625b5c8143c2d22b3e549067b78efa32e4fa", size = 42159605, upload-time = "2026-02-06T18:12:10.919Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f3/b2a5e720cc56eaa38b4518e63aa577b4bbd60e8b05a00fe43ca051be5879/polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61e8d73c614b46a00d2f853625a7569a2e4a0999333e876354ac81d1bf1bb5e2", size = 45336615, upload-time = "2026-02-06T18:12:14.074Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8d/ee2e4b7de948090cfb3df37d401c521233daf97bfc54ddec5d61d1d31618/polars_runtime_32-1.38.1-cp310-abi3-win_amd64.whl", hash = "sha256:08c2b3b93509c1141ac97891294ff5c5b0c548a373f583eaaea873a4bf506437", size = 45680732, upload-time = "2026-02-06T18:12:19.097Z" }, - { url = "https://files.pythonhosted.org/packages/bf/18/72c216f4ab0c82b907009668f79183ae029116ff0dd245d56ef58aac48e7/polars_runtime_32-1.38.1-cp310-abi3-win_arm64.whl", hash = "sha256:6d07d0cc832bfe4fb54b6e04218c2c27afcfa6b9498f9f6bbf262a00d58cc7c4", size = 41639413, upload-time = "2026-02-06T18:12:22.044Z" }, + { url = "https://files.pythonhosted.org/packages/3b/74/1b41205f7368c9375ab1dea91178eaa20435fe3eff036390a53a7660b416/polars_runtime_32-1.39.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9", size = 45273243, upload-time = "2026-03-20T11:14:26.691Z" }, + { url = "https://files.pythonhosted.org/packages/90/bf/297716b3095fe719be20fcf7af1d2b6ab069c38199bbace2469608a69b3a/polars_runtime_32-1.39.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562", size = 40842924, upload-time = "2026-03-20T11:14:31.154Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/e65236d9d0d9babfa0ecba593413c06530fca60a8feb8f66243aa5dba92e/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14", size = 43220650, upload-time = "2026-03-20T11:14:35.458Z" }, + { url = "https://files.pythonhosted.org/packages/b0/15/fc3e43f3fdf3f20b7dfb5abe871ab6162cf8fb4aeabf4cfad822d5dc4c79/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed", size = 46877498, upload-time = "2026-03-20T11:14:40.14Z" }, + { url = "https://files.pythonhosted.org/packages/3c/81/bd5f895919e32c6ab0a7786cd0c0ca961cb03152c47c3645808b54383f31/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2", size = 43380176, upload-time = "2026-03-20T11:14:45.566Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3e/c86433c3b5ec0315bdfc7640d0c15d41f1216c0103a0eab9a9b5147d6c4c/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4", size = 46485933, upload-time = "2026-03-20T11:14:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/200b310cf91f98e652eb6ea09fdb3a9718aa0293ebf113dce325797c8572/polars_runtime_32-1.39.3-cp310-abi3-win_amd64.whl", hash = "sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339", size = 46995458, upload-time = "2026-03-20T11:14:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/da/76/2d48927e0aa2abbdde08cbf4a2536883b73277d47fbeca95e952de86df34/polars_runtime_32-1.39.3-cp310-abi3-win_arm64.whl", hash = "sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860", size = 41857648, upload-time = "2026-03-20T11:15:01.142Z" }, ] [[package]] @@ -4616,8 +4636,8 @@ name = "powerfx" version = "0.0.34" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, - { name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, + { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" } wheels = [ @@ -4776,16 +4796,17 @@ wheels = [ [[package]] name = "protobuf" -version = "5.29.6" +version = "6.33.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, - { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, - { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, - { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[package]] @@ -4862,11 +4883,11 @@ wheels = [ [[package]] name = "pyasn1" -version = "0.6.2" +version = "0.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, ] [[package]] @@ -5065,11 +5086,14 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.12.0" +version = "2.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a8/10/e8192be5f38f3e8e7e046716de4cae33d56fd5ae08927a823bb916be36c1/pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02", size = 102511, upload-time = "2026-03-12T17:15:30.831Z" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/70/70f895f404d363d291dcf62c12c85fdd47619ad9674ac0f53364d035925a/pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e", size = 29700, upload-time = "2026-03-12T17:15:29.257Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, ] [package.optional-dependencies] @@ -5280,7 +5304,7 @@ name = "pythonnet" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, + { name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } wheels = [ @@ -5384,7 +5408,7 @@ wheels = [ [[package]] name = "qdrant-client" -version = "1.17.0" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -5396,21 +5420,21 @@ dependencies = [ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/fb/c9c4cecf6e7fdff2dbaeee0de40e93fe495379eb5fe2775b184ea45315da/qdrant_client-1.17.0.tar.gz", hash = "sha256:47eb033edb9be33a4babb4d87b0d8d5eaf03d52112dca0218db7f2030bf41ba9", size = 344839, upload-time = "2026-02-19T16:03:17.069Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/dd/f8a8261b83946af3cd65943c93c4f83e044f01184e8525404989d22a81a5/qdrant_client-1.17.1.tar.gz", hash = "sha256:22f990bbd63485ed97ba551a4c498181fcb723f71dcab5d6e4e43fe1050a2bc0", size = 344979, upload-time = "2026-03-13T17:13:44.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/15/dfadbc9d8c9872e8ac45fa96f5099bb2855f23426bfea1bbcdc85e64ef6e/qdrant_client-1.17.0-py3-none-any.whl", hash = "sha256:f5b452c68c42b3580d3d266446fb00d3c6e3aae89c916e16585b3c704e108438", size = 390381, upload-time = "2026-02-19T16:03:15.486Z" }, + { url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" }, ] [[package]] name = "redis" -version = "6.4.0" +version = "7.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "(python_full_version < '3.11.3' and sys_platform == 'darwin') or (python_full_version < '3.11.3' and sys_platform == 'linux') or (python_full_version < '3.11.3' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/80/2971931d27651affa88a44c0ad7b8c4a19dc29c998abb20b23868d319b59/redis-7.1.1.tar.gz", hash = "sha256:a2814b2bda15b39dad11391cc48edac4697214a8a5a4bd10abe936ab4892eb43", size = 4800064, upload-time = "2026-02-09T18:39:40.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/1de1d812ba1481fa4b37fb03b4eec0fcb71b6a0d44c04ea3482eb017600f/redis-7.1.1-py3-none-any.whl", hash = "sha256:f77817f16071c2950492c67d40b771fa493eb3fccc630a424a10976dbb794b7a", size = 356057, upload-time = "2026-02-09T18:39:38.602Z" }, ] [[package]] @@ -5732,18 +5756,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/1a/3b64696bc0c33aa1d86d3e6add03c4e0afe51110264fd41208bd95c2665c/rq-2.7.0-py3-none-any.whl", hash = "sha256:4b320e95968208d2e249fa0d3d90ee309478e2d7ea60a116f8ff9aa343a4c117", size = 115728, upload-time = "2026-02-22T11:10:48.401Z" }, ] -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - [[package]] name = "ruff" version = "0.15.5" @@ -6256,28 +6268,28 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.3.2" +version = "3.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/2f/9223c24f568bb7a0c03d751e609844dce0968f13b39a3f73fbb3a96cd27a/sse_starlette-3.3.3.tar.gz", hash = "sha256:72a95d7575fd5129bd0ae15275ac6432bb35ac542fdebb82889c24bb9f3f4049", size = 32420, upload-time = "2026-03-17T20:05:55.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, + { url = "https://files.pythonhosted.org/packages/78/e2/b8cff57a67dddf9a464d7e943218e031617fb3ddc133aeeb0602ff5f6c85/sse_starlette-3.3.3-py3-none-any.whl", hash = "sha256:c5abb5082a1cc1c6294d89c5290c46b5f67808cfdb612b7ec27e8ba061c22e8d", size = 14329, upload-time = "2026-03-17T20:05:54.35Z" }, ] [[package]] name = "starlette" -version = "0.52.1" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, ] [[package]] From 78e6c2f76273805d66c80552833926fb8952c58b Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 23 Mar 2026 20:31:50 +0100 Subject: [PATCH 13/13] fix test vars --- .github/workflows/python-integration-tests.yml | 4 ++++ .github/workflows/python-merge-tests.yml | 4 ++++ .github/workflows/python-sample-validation.yml | 3 +++ .../azure-ai/tests/assets/sample_image.jpg | Bin 0 -> 182161 bytes 4 files changed, 11 insertions(+) create mode 100644 python/packages/azure-ai/tests/assets/sample_image.jpg diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index 2ee9eb1af1..1f17313431 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -63,6 +63,8 @@ jobs: OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} OPENAI_EMBEDDINGS_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }} + OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }} + OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} defaults: run: @@ -172,7 +174,9 @@ jobs: UV_PYTHON: "3.11" OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} + OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} + OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }} AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }} AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }} AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }} diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index e680ee2f16..2b5d0fe8bf 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -135,6 +135,8 @@ jobs: OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} OPENAI_EMBEDDINGS_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }} + OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }} + OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} defaults: run: @@ -294,7 +296,9 @@ jobs: UV_PYTHON: "3.11" OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} + OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} + OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }} AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }} AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }} AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }} diff --git a/.github/workflows/python-sample-validation.yml b/.github/workflows/python-sample-validation.yml index 4a14e6b41b..a98c796445 100644 --- a/.github/workflows/python-sample-validation.yml +++ b/.github/workflows/python-sample-validation.yml @@ -68,6 +68,7 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} + OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }} # Observability ENABLE_INSTRUMENTATION: "true" defaults: @@ -230,6 +231,7 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} + OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }} defaults: run: working-directory: python @@ -271,6 +273,7 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} + OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }} # Copilot Studio COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }} COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }} diff --git a/python/packages/azure-ai/tests/assets/sample_image.jpg b/python/packages/azure-ai/tests/assets/sample_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ea6486656fd5b603af043e29b941c99845baea7a GIT binary patch literal 182161 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%0Dk-Pm;7s}kHuGa7vE${d#SHv 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 UcKRcD&9zq&ZLV4!^WzTS~D|(tTOER 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 B_m}_x literal 0 HcmV?d00001