From cd7661bbb7847c014702c1bcb006ec44f5687f6e Mon Sep 17 00:00:00 2001 From: Laveesh Rohra Date: Fri, 30 Jan 2026 08:38:49 -0800 Subject: [PATCH 01/20] Python: Fix dependencies for durabletask (#3493) * Fix dependencies * Use local packages * fix typing --- python/packages/azurefunctions/pyproject.toml | 4 +- python/packages/durabletask/pyproject.toml | 3 +- .../01_single_agent/requirements.txt | 13 +- .../02_multi_agent/requirements.txt | 15 +- .../03_reliable_streaming/requirements.txt | 15 +- .../requirements.txt | 15 +- .../requirements.txt | 15 +- .../requirements.txt | 15 +- .../requirements.txt | 15 +- .../08_mcp_server/requirements.txt | 13 +- .../01_single_agent/requirements.txt | 12 +- .../02_multi_agent/requirements.txt | 12 +- .../requirements.txt | 12 +- .../requirements.txt | 12 +- .../requirements.txt | 12 +- .../requirements.txt | 12 +- .../requirements.txt | 12 +- python/uv.lock | 275 +++++++++--------- 18 files changed, 308 insertions(+), 174 deletions(-) diff --git a/python/packages/azurefunctions/pyproject.toml b/python/packages/azurefunctions/pyproject.toml index 816a7a0ee1..6000408247 100644 --- a/python/packages/azurefunctions/pyproject.toml +++ b/python/packages/azurefunctions/pyproject.toml @@ -29,9 +29,7 @@ dependencies = [ ] [dependency-groups] -dev = [ - "types-python-dateutil>=2.9.0", -] +dev = [] [tool.uv] prerelease = "if-necessary-or-explicit" diff --git a/python/packages/durabletask/pyproject.toml b/python/packages/durabletask/pyproject.toml index 34bc458c25..3c854f7243 100644 --- a/python/packages/durabletask/pyproject.toml +++ b/python/packages/durabletask/pyproject.toml @@ -24,7 +24,8 @@ classifiers = [ dependencies = [ "agent-framework-core", "durabletask>=1.3.0", - "durabletask-azuremanaged>=1.3.0" + "durabletask-azuremanaged>=1.3.0", + "python-dateutil>=2.8.0", ] [dependency-groups] diff --git a/python/samples/getting_started/azure_functions/01_single_agent/requirements.txt b/python/samples/getting_started/azure_functions/01_single_agent/requirements.txt index 39ad8a124f..fc4ff0244e 100644 --- a/python/samples/getting_started/azure_functions/01_single_agent/requirements.txt +++ b/python/samples/getting_started/azure_functions/01_single_agent/requirements.txt @@ -1,2 +1,13 @@ -agent-framework-azurefunctions +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-azurefunctions + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - dependency of azurefunctions +-e ../../../../packages/azurefunctions # Azure Functions integration - the main package for this sample + +# Azure authentication azure-identity diff --git a/python/samples/getting_started/azure_functions/02_multi_agent/requirements.txt b/python/samples/getting_started/azure_functions/02_multi_agent/requirements.txt index 8aa2c75d80..fc4ff0244e 100644 --- a/python/samples/getting_started/azure_functions/02_multi_agent/requirements.txt +++ b/python/samples/getting_started/azure_functions/02_multi_agent/requirements.txt @@ -1,2 +1,13 @@ -agent-framework-azurefunctions -azure-identity \ No newline at end of file +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-azurefunctions + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - dependency of azurefunctions +-e ../../../../packages/azurefunctions # Azure Functions integration - the main package for this sample + +# Azure authentication +azure-identity diff --git a/python/samples/getting_started/azure_functions/03_reliable_streaming/requirements.txt b/python/samples/getting_started/azure_functions/03_reliable_streaming/requirements.txt index 8b3943b92f..c5fc4f2ec6 100644 --- a/python/samples/getting_started/azure_functions/03_reliable_streaming/requirements.txt +++ b/python/samples/getting_started/azure_functions/03_reliable_streaming/requirements.txt @@ -1,3 +1,16 @@ -agent-framework-azurefunctions +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-azurefunctions + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - dependency of azurefunctions +-e ../../../../packages/azurefunctions # Azure Functions integration - the main package for this sample + +# Azure authentication azure-identity + +# Redis client redis diff --git a/python/samples/getting_started/azure_functions/04_single_agent_orchestration_chaining/requirements.txt b/python/samples/getting_started/azure_functions/04_single_agent_orchestration_chaining/requirements.txt index 8aa2c75d80..fc4ff0244e 100644 --- a/python/samples/getting_started/azure_functions/04_single_agent_orchestration_chaining/requirements.txt +++ b/python/samples/getting_started/azure_functions/04_single_agent_orchestration_chaining/requirements.txt @@ -1,2 +1,13 @@ -agent-framework-azurefunctions -azure-identity \ No newline at end of file +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-azurefunctions + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - dependency of azurefunctions +-e ../../../../packages/azurefunctions # Azure Functions integration - the main package for this sample + +# Azure authentication +azure-identity diff --git a/python/samples/getting_started/azure_functions/05_multi_agent_orchestration_concurrency/requirements.txt b/python/samples/getting_started/azure_functions/05_multi_agent_orchestration_concurrency/requirements.txt index 8aa2c75d80..fc4ff0244e 100644 --- a/python/samples/getting_started/azure_functions/05_multi_agent_orchestration_concurrency/requirements.txt +++ b/python/samples/getting_started/azure_functions/05_multi_agent_orchestration_concurrency/requirements.txt @@ -1,2 +1,13 @@ -agent-framework-azurefunctions -azure-identity \ No newline at end of file +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-azurefunctions + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - dependency of azurefunctions +-e ../../../../packages/azurefunctions # Azure Functions integration - the main package for this sample + +# Azure authentication +azure-identity diff --git a/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/requirements.txt b/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/requirements.txt index 8aa2c75d80..fc4ff0244e 100644 --- a/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/requirements.txt +++ b/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/requirements.txt @@ -1,2 +1,13 @@ -agent-framework-azurefunctions -azure-identity \ No newline at end of file +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-azurefunctions + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - dependency of azurefunctions +-e ../../../../packages/azurefunctions # Azure Functions integration - the main package for this sample + +# Azure authentication +azure-identity diff --git a/python/samples/getting_started/azure_functions/07_single_agent_orchestration_hitl/requirements.txt b/python/samples/getting_started/azure_functions/07_single_agent_orchestration_hitl/requirements.txt index 8aa2c75d80..fc4ff0244e 100644 --- a/python/samples/getting_started/azure_functions/07_single_agent_orchestration_hitl/requirements.txt +++ b/python/samples/getting_started/azure_functions/07_single_agent_orchestration_hitl/requirements.txt @@ -1,2 +1,13 @@ -agent-framework-azurefunctions -azure-identity \ No newline at end of file +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-azurefunctions + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - dependency of azurefunctions +-e ../../../../packages/azurefunctions # Azure Functions integration - the main package for this sample + +# Azure authentication +azure-identity diff --git a/python/samples/getting_started/azure_functions/08_mcp_server/requirements.txt b/python/samples/getting_started/azure_functions/08_mcp_server/requirements.txt index 39ad8a124f..fc4ff0244e 100644 --- a/python/samples/getting_started/azure_functions/08_mcp_server/requirements.txt +++ b/python/samples/getting_started/azure_functions/08_mcp_server/requirements.txt @@ -1,2 +1,13 @@ -agent-framework-azurefunctions +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-azurefunctions + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - dependency of azurefunctions +-e ../../../../packages/azurefunctions # Azure Functions integration - the main package for this sample + +# Azure authentication azure-identity diff --git a/python/samples/getting_started/durabletask/01_single_agent/requirements.txt b/python/samples/getting_started/durabletask/01_single_agent/requirements.txt index 371b9e3b79..09ed7d18ad 100644 --- a/python/samples/getting_started/durabletask/01_single_agent/requirements.txt +++ b/python/samples/getting_started/durabletask/01_single_agent/requirements.txt @@ -1,6 +1,12 @@ -# Agent Framework packages (installing from local package until a package is published) --e ../../../../ --e ../../../../packages/durabletask +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-durabletask + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - the main package for this sample # Azure authentication azure-identity diff --git a/python/samples/getting_started/durabletask/02_multi_agent/requirements.txt b/python/samples/getting_started/durabletask/02_multi_agent/requirements.txt index 371b9e3b79..09ed7d18ad 100644 --- a/python/samples/getting_started/durabletask/02_multi_agent/requirements.txt +++ b/python/samples/getting_started/durabletask/02_multi_agent/requirements.txt @@ -1,6 +1,12 @@ -# Agent Framework packages (installing from local package until a package is published) --e ../../../../ --e ../../../../packages/durabletask +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-durabletask + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - the main package for this sample # Azure authentication azure-identity diff --git a/python/samples/getting_started/durabletask/03_single_agent_streaming/requirements.txt b/python/samples/getting_started/durabletask/03_single_agent_streaming/requirements.txt index 047a5d36f1..f8843a12ba 100644 --- a/python/samples/getting_started/durabletask/03_single_agent_streaming/requirements.txt +++ b/python/samples/getting_started/durabletask/03_single_agent_streaming/requirements.txt @@ -1,6 +1,12 @@ -# Agent Framework packages (installing from local package until a package is published) --e ../../../../ --e ../../../../packages/durabletask +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-durabletask + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - the main package for this sample # Azure authentication azure-identity diff --git a/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/requirements.txt b/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/requirements.txt index 371b9e3b79..09ed7d18ad 100644 --- a/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/requirements.txt +++ b/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/requirements.txt @@ -1,6 +1,12 @@ -# Agent Framework packages (installing from local package until a package is published) --e ../../../../ --e ../../../../packages/durabletask +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-durabletask + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - the main package for this sample # Azure authentication azure-identity diff --git a/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/requirements.txt b/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/requirements.txt index 371b9e3b79..09ed7d18ad 100644 --- a/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/requirements.txt +++ b/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/requirements.txt @@ -1,6 +1,12 @@ -# Agent Framework packages (installing from local package until a package is published) --e ../../../../ --e ../../../../packages/durabletask +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-durabletask + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - the main package for this sample # Azure authentication azure-identity diff --git a/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/requirements.txt b/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/requirements.txt index 371b9e3b79..09ed7d18ad 100644 --- a/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/requirements.txt +++ b/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/requirements.txt @@ -1,6 +1,12 @@ -# Agent Framework packages (installing from local package until a package is published) --e ../../../../ --e ../../../../packages/durabletask +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-durabletask + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - the main package for this sample # Azure authentication azure-identity diff --git a/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/requirements.txt b/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/requirements.txt index 371b9e3b79..09ed7d18ad 100644 --- a/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/requirements.txt +++ b/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/requirements.txt @@ -1,6 +1,12 @@ -# Agent Framework packages (installing from local package until a package is published) --e ../../../../ --e ../../../../packages/durabletask +# Agent Framework packages +# To use the deployed version, uncomment the line below and comment out the local installation lines +# agent-framework-durabletask + +# Local installation (for development and testing) +# Each package must be listed explicitly because pip doesn't resolve uv workspace sources. +# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source. +-e ../../../../packages/core # Core framework - base dependency for all packages +-e ../../../../packages/durabletask # Durable Task support - the main package for this sample # Azure authentication azure-identity diff --git a/python/uv.lock b/python/uv.lock index 83a8bed32f..0d7436c449 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -264,11 +264,6 @@ dependencies = [ { name = "azure-functions-durable", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -[package.dev-dependencies] -dev = [ - { name = "types-python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, -] - [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, @@ -278,7 +273,7 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "types-python-dateutil", specifier = ">=2.9.0" }] +dev = [] [[package]] name = "agent-framework-bedrock" @@ -464,6 +459,7 @@ dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "durabletask", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "durabletask-azuremanaged", 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'" }, ] [package.dev-dependencies] @@ -476,6 +472,7 @@ requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, { name = "durabletask", specifier = ">=1.3.0" }, { name = "durabletask-azuremanaged", specifier = ">=1.3.0" }, + { name = "python-dateutil", specifier = ">=2.8.0" }, ] [package.metadata.requires-dev] @@ -901,7 +898,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.76.0" +version = "0.77.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -913,9 +910,9 @@ dependencies = [ { name = "sniffio", 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/6e/be/d11abafaa15d6304826438170f7574d750218f49a106c54424a40cef4494/anthropic-0.76.0.tar.gz", hash = "sha256:e0cae6a368986d5cf6df743dfbb1b9519e6a9eee9c6c942ad8121c0b34416ffe", size = 495483, upload-time = "2026-01-13T18:41:14.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/85/6cb5da3cf91de2eeea89726316e8c5c8c31e2d61ee7cb1233d7e95512c31/anthropic-0.77.0.tar.gz", hash = "sha256:ce36efeb80cb1e25430a88440dc0f9aa5c87f10d080ab70a1bdfd5c2c5fbedb4", size = 504575, upload-time = "2026-01-29T18:20:41.507Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/70/7b0fd9c1a738f59d3babe2b4212031c34ab7d0fda4ffef15b58a55c5bcea/anthropic-0.76.0-py3-none-any.whl", hash = "sha256:81efa3113901192af2f0fe977d3ec73fdadb1e691586306c4256cd6d5ccc331c", size = 390309, upload-time = "2026-01-13T18:41:13.483Z" }, + { url = "https://files.pythonhosted.org/packages/ac/27/9df785d3f94df9ac72f43ee9e14b8120b37d992b18f4952774ed46145022/anthropic-0.77.0-py3-none-any.whl", hash = "sha256:65cc83a3c82ce622d5c677d0d7706c77d29dc83958c6b10286e12fda6ffb2651", size = 397867, upload-time = "2026-01-29T18:20:39.481Z" }, ] [[package]] @@ -1798,31 +1795,31 @@ wheels = [ [[package]] name = "debugpy" -version = "1.8.19" +version = "1.8.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/75/9e12d4d42349b817cd545b89247696c67917aab907012ae5b64bbfea3199/debugpy-1.8.19.tar.gz", hash = "sha256:eea7e5987445ab0b5ed258093722d5ecb8bb72217c5c9b1e21f64efe23ddebdb", size = 1644590, upload-time = "2025-12-15T21:53:28.044Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/98/d57054371887f37d3c959a7a8dc3c76b763acb65f5e78d849d7db7cadc5b/debugpy-1.8.19-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:fce6da15d73be5935b4438435c53adb512326a3e11e4f90793ea87cd9f018254", size = 2098493, upload-time = "2025-12-15T21:53:30.149Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dd/c517b9aa3500157a30e4f4c4f5149f880026bd039d2b940acd2383a85d8e/debugpy-1.8.19-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:e24b1652a1df1ab04d81e7ead446a91c226de704ff5dde6bd0a0dbaab07aa3f2", size = 3087875, upload-time = "2025-12-15T21:53:31.511Z" }, - { url = "https://files.pythonhosted.org/packages/d8/57/3d5a5b0da9b63445253107ead151eff29190c6ad7440c68d1a59d56613aa/debugpy-1.8.19-cp310-cp310-win32.whl", hash = "sha256:327cb28c3ad9e17bc925efc7f7018195fd4787c2fe4b7af1eec11f1d19bdec62", size = 5239378, upload-time = "2025-12-15T21:53:32.979Z" }, - { url = "https://files.pythonhosted.org/packages/a6/36/7f9053c4c549160c87ae7e43800138f2695578c8b65947114c97250983b6/debugpy-1.8.19-cp310-cp310-win_amd64.whl", hash = "sha256:b7dd275cf2c99e53adb9654f5ae015f70415bbe2bacbe24cfee30d54b6aa03c5", size = 5271129, upload-time = "2025-12-15T21:53:35.085Z" }, - { url = "https://files.pythonhosted.org/packages/80/e2/48531a609b5a2aa94c6b6853afdfec8da05630ab9aaa96f1349e772119e9/debugpy-1.8.19-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:c5dcfa21de1f735a4f7ced4556339a109aa0f618d366ede9da0a3600f2516d8b", size = 2207620, upload-time = "2025-12-15T21:53:37.1Z" }, - { url = "https://files.pythonhosted.org/packages/1b/d4/97775c01d56071969f57d93928899e5616a4cfbbf4c8cc75390d3a51c4a4/debugpy-1.8.19-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:806d6800246244004625d5222d7765874ab2d22f3ba5f615416cf1342d61c488", size = 3170796, upload-time = "2025-12-15T21:53:38.513Z" }, - { url = "https://files.pythonhosted.org/packages/8d/7e/8c7681bdb05be9ec972bbb1245eb7c4c7b0679bb6a9e6408d808bc876d3d/debugpy-1.8.19-cp311-cp311-win32.whl", hash = "sha256:783a519e6dfb1f3cd773a9bda592f4887a65040cb0c7bd38dde410f4e53c40d4", size = 5164287, upload-time = "2025-12-15T21:53:40.857Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a8/aaac7ff12ddf5d68a39e13a423a8490426f5f661384f5ad8d9062761bd8e/debugpy-1.8.19-cp311-cp311-win_amd64.whl", hash = "sha256:14035cbdbb1fe4b642babcdcb5935c2da3b1067ac211c5c5a8fdc0bb31adbcaa", size = 5188269, upload-time = "2025-12-15T21:53:42.359Z" }, - { url = "https://files.pythonhosted.org/packages/4a/15/d762e5263d9e25b763b78be72dc084c7a32113a0bac119e2f7acae7700ed/debugpy-1.8.19-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:bccb1540a49cde77edc7ce7d9d075c1dbeb2414751bc0048c7a11e1b597a4c2e", size = 2549995, upload-time = "2025-12-15T21:53:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/a7/88/f7d25c68b18873b7c53d7c156ca7a7ffd8e77073aa0eac170a9b679cf786/debugpy-1.8.19-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:e9c68d9a382ec754dc05ed1d1b4ed5bd824b9f7c1a8cd1083adb84b3c93501de", size = 4309891, upload-time = "2025-12-15T21:53:45.26Z" }, - { url = "https://files.pythonhosted.org/packages/c5/4f/a65e973aba3865794da65f71971dca01ae66666132c7b2647182d5be0c5f/debugpy-1.8.19-cp312-cp312-win32.whl", hash = "sha256:6599cab8a783d1496ae9984c52cb13b7c4a3bd06a8e6c33446832a5d97ce0bee", size = 5286355, upload-time = "2025-12-15T21:53:46.763Z" }, - { url = "https://files.pythonhosted.org/packages/d8/3a/d3d8b48fec96e3d824e404bf428276fb8419dfa766f78f10b08da1cb2986/debugpy-1.8.19-cp312-cp312-win_amd64.whl", hash = "sha256:66e3d2fd8f2035a8f111eb127fa508469dfa40928a89b460b41fd988684dc83d", size = 5328239, upload-time = "2025-12-15T21:53:48.868Z" }, - { url = "https://files.pythonhosted.org/packages/71/3d/388035a31a59c26f1ecc8d86af607d0c42e20ef80074147cd07b180c4349/debugpy-1.8.19-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:91e35db2672a0abaf325f4868fcac9c1674a0d9ad9bb8a8c849c03a5ebba3e6d", size = 2538859, upload-time = "2025-12-15T21:53:50.478Z" }, - { url = "https://files.pythonhosted.org/packages/4a/19/c93a0772d0962294f083dbdb113af1a7427bb632d36e5314297068f55db7/debugpy-1.8.19-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:85016a73ab84dea1c1f1dcd88ec692993bcbe4532d1b49ecb5f3c688ae50c606", size = 4292575, upload-time = "2025-12-15T21:53:51.821Z" }, - { url = "https://files.pythonhosted.org/packages/5c/56/09e48ab796b0a77e3d7dc250f95251832b8bf6838c9632f6100c98bdf426/debugpy-1.8.19-cp313-cp313-win32.whl", hash = "sha256:b605f17e89ba0ecee994391194285fada89cee111cfcd29d6f2ee11cbdc40976", size = 5286209, upload-time = "2025-12-15T21:53:53.602Z" }, - { url = "https://files.pythonhosted.org/packages/fb/4e/931480b9552c7d0feebe40c73725dd7703dcc578ba9efc14fe0e6d31cfd1/debugpy-1.8.19-cp313-cp313-win_amd64.whl", hash = "sha256:c30639998a9f9cd9699b4b621942c0179a6527f083c72351f95c6ab1728d5b73", size = 5328206, upload-time = "2025-12-15T21:53:55.433Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b9/cbec520c3a00508327476c7fce26fbafef98f412707e511eb9d19a2ef467/debugpy-1.8.19-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:1e8c4d1bd230067bf1bbcdbd6032e5a57068638eb28b9153d008ecde288152af", size = 2537372, upload-time = "2025-12-15T21:53:57.318Z" }, - { url = "https://files.pythonhosted.org/packages/88/5e/cf4e4dc712a141e10d58405c58c8268554aec3c35c09cdcda7535ff13f76/debugpy-1.8.19-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d40c016c1f538dbf1762936e3aeb43a89b965069d9f60f9e39d35d9d25e6b809", size = 4268729, upload-time = "2025-12-15T21:53:58.712Z" }, - { url = "https://files.pythonhosted.org/packages/82/a3/c91a087ab21f1047db328c1d3eb5d1ff0e52de9e74f9f6f6fa14cdd93d58/debugpy-1.8.19-cp314-cp314-win32.whl", hash = "sha256:0601708223fe1cd0e27c6cce67a899d92c7d68e73690211e6788a4b0e1903f5b", size = 5286388, upload-time = "2025-12-15T21:54:00.687Z" }, - { url = "https://files.pythonhosted.org/packages/17/b8/bfdc30b6e94f1eff09f2dc9cc1f9cd1c6cde3d996bcbd36ce2d9a4956e99/debugpy-1.8.19-cp314-cp314-win_amd64.whl", hash = "sha256:8e19a725f5d486f20e53a1dde2ab8bb2c9607c40c00a42ab646def962b41125f", size = 5327741, upload-time = "2025-12-15T21:54:02.148Z" }, - { url = "https://files.pythonhosted.org/packages/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.py3-none-any.whl", hash = "sha256:360ffd231a780abbc414ba0f005dad409e71c78637efe8f2bd75837132a41d38", size = 5292321, upload-time = "2025-12-15T21:54:16.024Z" }, + { url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, ] [[package]] @@ -2318,16 +2315,16 @@ wheels = [ [[package]] name = "github-copilot-sdk" -version = "0.1.19" +version = "0.1.20" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", 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 = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/e7/87955115ae99668a7dc1d1314c2ac07a7d5e8d621c52ce322090616d342e/github_copilot_sdk-0.1.19.tar.gz", hash = "sha256:2bba9db1ee0b3b6ff751568489777224e381c15427e253f98731c01a936833c2", size = 81767, upload-time = "2026-01-27T17:51:02.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/7d/afde0ec85815a558612130dc5ff79536299f411e672410c3edc0c1edeb2a/github_copilot_sdk-0.1.20.tar.gz", hash = "sha256:9e89cd46577fd18dd808d7113b7e20e021c4f944121a0a4891945460fb26c53c", size = 92207, upload-time = "2026-01-30T00:25:20.509Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/8d/f5b0ddab9b3c4e9d0a8b5233fd12543f07f2fb65ab593652826c1ff96359/github_copilot_sdk-0.1.19-py3-none-any.whl", hash = "sha256:98d2e6ce65b88b470756d0d2ced12714fd76d3394c245e29a42185e3a4e83d0b", size = 34243, upload-time = "2026-01-27T17:50:59.566Z" }, + { url = "https://files.pythonhosted.org/packages/55/91/f8cfa809184988a273af58824b312d31a532ee3ee70875100b5061540178/github_copilot_sdk-0.1.20-py3-none-any.whl", hash = "sha256:e7fa1bb843e2494930126551b80f3a035f36c47a05f9173ad0cdfb4151ad9346", size = 40306, upload-time = "2026-01-30T00:25:19.184Z" }, ] [[package]] @@ -2390,6 +2387,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/65/5b235b40581ad75ab97dcd8b4218022ae8e3ab77c13c919f1a1dfe9171fd/greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13", size = 273723, upload-time = "2026-01-23T15:30:37.521Z" }, { url = "https://files.pythonhosted.org/packages/ce/ad/eb4729b85cba2d29499e0a04ca6fbdd8f540afd7be142fd571eea43d712f/greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4", size = 574874, upload-time = "2026-01-23T16:00:54.551Z" }, { url = "https://files.pythonhosted.org/packages/87/32/57cad7fe4c8b82fdaa098c89498ef85ad92dfbb09d5eb713adedfc2ae1f5/greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5", size = 586309, upload-time = "2026-01-23T16:05:25.18Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/f041005cb87055e62b0d68680e88ec1a57f4688523d5e2fb305841bc8307/greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5", size = 597461, upload-time = "2026-01-23T16:15:51.943Z" }, { url = "https://files.pythonhosted.org/packages/87/eb/8a1ec2da4d55824f160594a75a9d8354a5fe0a300fb1c48e7944265217e1/greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe", size = 586985, upload-time = "2026-01-23T15:32:47.968Z" }, { url = "https://files.pythonhosted.org/packages/15/1c/0621dd4321dd8c351372ee8f9308136acb628600658a49be1b7504208738/greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729", size = 1547271, upload-time = "2026-01-23T16:04:18.977Z" }, { url = "https://files.pythonhosted.org/packages/9d/53/24047f8924c83bea7a59c8678d9571209c6bfe5f4c17c94a78c06024e9f2/greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4", size = 1613427, upload-time = "2026-01-23T15:33:44.428Z" }, @@ -2397,6 +2395,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, @@ -2405,6 +2404,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, @@ -2413,6 +2413,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, @@ -2421,6 +2422,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, @@ -2429,6 +2431,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, @@ -2696,7 +2699,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.3.4" +version = "1.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -2710,9 +2713,9 @@ dependencies = [ { name = "typer-slim", 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/af/25/74af9d16cd59ae15b12467a79a84aa0fe24be4aba68fc4da0c1864d49c17/huggingface_hub-1.3.4.tar.gz", hash = "sha256:c20d5484a611b7b7891d272e8fc9f77d5de025b0480bdacfa858efb3780b455f", size = 627683, upload-time = "2026-01-26T14:05:10.656Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/e9/2658cb9bc4c72a67b7f87650e827266139befaf499095883d30dabc4d49f/huggingface_hub-1.3.5.tar.gz", hash = "sha256:8045aca8ddab35d937138f3c386c6d43a275f53437c5c64cdc9aa8408653b4ed", size = 627456, upload-time = "2026-01-29T10:34:19.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/07/3d0c34c345043c6a398a5882e196b2220dc5861adfa18322448b90908f26/huggingface_hub-1.3.4-py3-none-any.whl", hash = "sha256:a0c526e76eb316e96a91e8a1a7a93cf66b0dd210be1a17bd5fc5ae53cba76bfd", size = 536611, upload-time = "2026-01-26T14:05:08.549Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/a579b95c46fe8e319f89dc700c087596f665141575f4dcf136aaa97d856f/huggingface_hub-1.3.5-py3-none-any.whl", hash = "sha256:fe332d7f86a8af874768452295c22cd3f37730fb2463cf6cc3295e26036f8ef9", size = 536675, upload-time = "2026-01-29T10:34:17.713Z" }, ] [[package]] @@ -3169,7 +3172,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.81.4" +version = "1.81.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3185,9 +3188,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/30/3a/7cd3a6bbf14a97ffdabedc73c6c74ab691317469e9e6dd03866f94173571/litellm-1.81.4.tar.gz", hash = "sha256:bd64fd4f11fe39c0c12fbb4a062c040bfb5c966f58717f38780d8a6b1e15169b", size = 13615134, upload-time = "2026-01-27T22:50:57.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/f4/c109bc5504520baa7b96a910b619d1b1b5af6cb5c28053e53adfed83e3ab/litellm-1.81.5.tar.gz", hash = "sha256:599994651cbb64b8ee7cd3b4979275139afc6e426bdd4aa840a61121bb3b04c9", size = 13615436, upload-time = "2026-01-29T01:37:54.817Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/98/3a29bf2381ffd6d67a7cab68ad668a43960e0eceadb785cb0f16127a27f7/litellm-1.81.4-py3-none-any.whl", hash = "sha256:be8ebc00ce08589a6851c88a2cb5fb399206844f31d107e37c9852013dd2333d", size = 11949804, upload-time = "2026-01-27T22:50:54.164Z" }, + { url = "https://files.pythonhosted.org/packages/74/0f/5312b944208efeec5dcbf8e0ed956f8f7c430b0c6458301d206380c90b56/litellm-1.81.5-py3-none-any.whl", hash = "sha256:206505c5a0c6503e465154b9c979772be3ede3f5bf746d15b37dca5ae54d239f", size = 11950016, upload-time = "2026-01-29T01:37:52.6Z" }, ] [package.optional-dependencies] @@ -4215,83 +4218,83 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.5" +version = "3.11.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/a3/4e09c61a5f0c521cba0bb433639610ae037437669f1a4cbc93799e731d78/orjson-3.11.6.tar.gz", hash = "sha256:0a54c72259f35299fd033042367df781c2f66d10252955ca1efb7db309b954cb", size = 6175856, upload-time = "2026-01-29T15:13:07.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" }, - { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" }, - { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" }, - { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" }, - { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" }, - { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" }, - { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" }, - { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" }, - { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" }, - { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" }, - { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" }, - { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" }, - { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, - { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, - { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, - { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, - { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, - { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, - { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, - { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, - { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, - { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, - { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, - { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, - { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, - { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, - { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, - { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, - { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, - { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, - { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, - { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, - { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, - { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, - { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, - { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, - { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, - { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, - { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, - { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, - { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, - { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, - { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, - { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, - { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, - { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, - { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, - { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, - { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, - { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, - { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, - { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, - { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, - { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, - { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, + { url = "https://files.pythonhosted.org/packages/30/3c/098ed0e49c565fdf1ccc6a75b190115d1ca74148bf5b6ab036554a550650/orjson-3.11.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a613fc37e007143d5b6286dccb1394cd114b07832417006a02b620ddd8279e37", size = 250411, upload-time = "2026-01-29T15:11:17.941Z" }, + { url = "https://files.pythonhosted.org/packages/15/7c/cb11a360fd228ceebade03b1e8e9e138dd4b1b3b11602b72dbdad915aded/orjson-3.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46ebee78f709d3ba7a65384cfe285bb0763157c6d2f836e7bde2f12d33a867a2", size = 138147, upload-time = "2026-01-29T15:11:19.659Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4b/e57b5c45ffe69fbef7cbd56e9f40e2dc0d5de920caafefcc6981d1a7efc5/orjson-3.11.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a726fa86d2368cd57990f2bd95ef5495a6e613b08fc9585dfe121ec758fb08d1", size = 135110, upload-time = "2026-01-29T15:11:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6e/4f21c6256f8cee3c0c69926cf7ac821cfc36f218512eedea2e2dc4a490c8/orjson-3.11.6-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:150f12e59d6864197770c78126e1a6e07a3da73d1728731bf3bc1e8b96ffdbe6", size = 140995, upload-time = "2026-01-29T15:11:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d0/78/92c36205ba2f6094ba1eea60c8e646885072abe64f155196833988c14b74/orjson-3.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a2d9746a5b5ce20c0908ada451eb56da4ffa01552a50789a0354d8636a02953", size = 144435, upload-time = "2026-01-29T15:11:24.124Z" }, + { url = "https://files.pythonhosted.org/packages/4d/52/1b518d164005811eb3fea92650e76e7d9deadb0b41e92c483373b1e82863/orjson-3.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd177f5dd91666d31e9019f1b06d2fcdf8a409a1637ddcb5915085dede85680", size = 142734, upload-time = "2026-01-29T15:11:25.708Z" }, + { url = "https://files.pythonhosted.org/packages/4b/11/60ea7885a2b7c1bf60ed8b5982356078a73785bd3bab392041a5bcf8de7c/orjson-3.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d777ec41a327bd3b7de97ba7bce12cc1007815ca398e4e4de9ec56c022c090b", size = 145802, upload-time = "2026-01-29T15:11:26.917Z" }, + { url = "https://files.pythonhosted.org/packages/41/7f/15a927e7958fd4f7560fb6dbb9346bee44a168e40168093c46020d866098/orjson-3.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f3a135f83185c87c13ff231fcb7dbb2fa4332a376444bd65135b50ff4cc5265c", size = 147504, upload-time = "2026-01-29T15:11:28.07Z" }, + { url = "https://files.pythonhosted.org/packages/66/1f/cabb9132a533f4f913e29294d0a1ca818b1a9a52e990526fe3f7ddd75f1c/orjson-3.11.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:2a8eeed7d4544cf391a142b0dd06029dac588e96cc692d9ab1c3f05b1e57c7f6", size = 421408, upload-time = "2026-01-29T15:11:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b9/09bda9257a982e300313e4a9fc9b9c3aaff424d07bcf765bf045e4e3ed03/orjson-3.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9d576865a21e5cc6695be8fb78afc812079fd361ce6a027a7d41561b61b33a90", size = 155801, upload-time = "2026-01-29T15:11:30.575Z" }, + { url = "https://files.pythonhosted.org/packages/98/19/4e40ea3e5f4c6a8d51f31fd2382351ee7b396fecca915b17cd1af588175b/orjson-3.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:925e2df51f60aa50f8797830f2adfc05330425803f4105875bb511ced98b7f89", size = 147647, upload-time = "2026-01-29T15:11:31.856Z" }, + { url = "https://files.pythonhosted.org/packages/5a/73/ef4bd7dd15042cf33a402d16b87b9e969e71edb452b63b6e2b05025d1f7d/orjson-3.11.6-cp310-cp310-win32.whl", hash = "sha256:09dded2de64e77ac0b312ad59f35023548fb87393a57447e1bb36a26c181a90f", size = 139770, upload-time = "2026-01-29T15:11:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ac/daab6e10467f7fffd7081ba587b492505b49313130ff5446a6fe28bf076e/orjson-3.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:3a63b5e7841ca8635214c6be7c0bf0246aa8c5cd4ef0c419b14362d0b2fb13de", size = 136783, upload-time = "2026-01-29T15:11:34.686Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d6b0a36854179b93ed77839f107c4089d91cccc9f9ba1b752b6e3bac5f34/orjson-3.11.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e259e85a81d76d9665f03d6129e09e4435531870de5961ddcd0bf6e3a7fde7d7", size = 250029, upload-time = "2026-01-29T15:11:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bb/22902619826641cf3b627c24aab62e2ad6b571bdd1d34733abb0dd57f67a/orjson-3.11.6-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:52263949f41b4a4822c6b1353bcc5ee2f7109d53a3b493501d3369d6d0e7937a", size = 134518, upload-time = "2026-01-29T15:11:37.347Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/7a818da4bba1de711a9653c420749c0ac95ef8f8651cbc1dca551f462fe0/orjson-3.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6439e742fa7834a24698d358a27346bb203bff356ae0402e7f5df8f749c621a8", size = 137917, upload-time = "2026-01-29T15:11:38.511Z" }, + { url = "https://files.pythonhosted.org/packages/59/0f/02846c1cac8e205cb3822dd8aa8f9114acda216f41fd1999ace6b543418d/orjson-3.11.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b81ffd68f084b4e993e3867acb554a049fa7787cc8710bbcc1e26965580d99be", size = 134923, upload-time = "2026-01-29T15:11:39.711Z" }, + { url = "https://files.pythonhosted.org/packages/94/cf/aeaf683001b474bb3c3c757073a4231dfdfe8467fceaefa5bfd40902c99f/orjson-3.11.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5a5468e5e60f7ef6d7f9044b06c8f94a3c56ba528c6e4f7f06ae95164b595ec", size = 140752, upload-time = "2026-01-29T15:11:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fe/dad52d8315a65f084044a0819d74c4c9daf9ebe0681d30f525b0d29a31f0/orjson-3.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72c5005eb45bd2535632d4f3bec7ad392832cfc46b62a3021da3b48a67734b45", size = 144201, upload-time = "2026-01-29T15:11:42.537Z" }, + { url = "https://files.pythonhosted.org/packages/36/bc/ab070dd421565b831801077f1e390c4d4af8bfcecafc110336680a33866b/orjson-3.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b14dd49f3462b014455a28a4d810d3549bf990567653eb43765cd847df09145", size = 142380, upload-time = "2026-01-29T15:11:44.309Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d8/4b581c725c3a308717f28bf45a9fdac210bca08b67e8430143699413ff06/orjson-3.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bb2c1ea30ef302f0f89f9bf3e7f9ab5e2af29dc9f80eb87aa99788e4e2d65", size = 145582, upload-time = "2026-01-29T15:11:45.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a2/09aab99b39f9a7f175ea8fa29adb9933a3d01e7d5d603cdee7f1c40c8da2/orjson-3.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:825e0a85d189533c6bff7e2fc417a28f6fcea53d27125c4551979aecd6c9a197", size = 147270, upload-time = "2026-01-29T15:11:46.782Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/5ef8eaf7829dc50da3bf497c7775b21ee88437bc8c41f959aa3504ca6631/orjson-3.11.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:b04575417a26530637f6ab4b1f7b4f666eb0433491091da4de38611f97f2fcf3", size = 421222, upload-time = "2026-01-29T15:11:48.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b0/dd6b941294c2b5b13da5fdc7e749e58d0c55a5114ab37497155e83050e95/orjson-3.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b83eb2e40e8c4da6d6b340ee6b1d6125f5195eb1b0ebb7eac23c6d9d4f92d224", size = 155562, upload-time = "2026-01-29T15:11:49.408Z" }, + { url = "https://files.pythonhosted.org/packages/8e/09/43924331a847476ae2f9a16bd6d3c9dab301265006212ba0d3d7fd58763a/orjson-3.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1f42da604ee65a6b87eef858c913ce3e5777872b19321d11e6fc6d21de89b64f", size = 147432, upload-time = "2026-01-29T15:11:50.635Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e9/d9865961081816909f6b49d880749dbbd88425afd7c5bbce0549e2290d77/orjson-3.11.6-cp311-cp311-win32.whl", hash = "sha256:5ae45df804f2d344cffb36c43fdf03c82fb6cd247f5faa41e21891b40dfbf733", size = 139623, upload-time = "2026-01-29T15:11:51.82Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f9/6836edb92f76eec1082919101eb1145d2f9c33c8f2c5e6fa399b82a2aaa8/orjson-3.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:f4295948d65ace0a2d8f2c4ccc429668b7eb8af547578ec882e16bf79b0050b2", size = 136647, upload-time = "2026-01-29T15:11:53.454Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0c/4954082eea948c9ae52ee0bcbaa2f99da3216a71bcc314ab129bde22e565/orjson-3.11.6-cp311-cp311-win_arm64.whl", hash = "sha256:314e9c45e0b81b547e3a1cfa3df3e07a815821b3dac9fe8cb75014071d0c16a4", size = 135327, upload-time = "2026-01-29T15:11:56.616Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/759f2879f41910b7e5e0cdbd9cf82a4f017c527fb0e972e9869ca7fe4c8e/orjson-3.11.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6f03f30cd8953f75f2a439070c743c7336d10ee940da918d71c6f3556af3ddcf", size = 249988, upload-time = "2026-01-29T15:11:58.294Z" }, + { url = "https://files.pythonhosted.org/packages/f0/70/54cecb929e6c8b10104fcf580b0cc7dc551aa193e83787dd6f3daba28bb5/orjson-3.11.6-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:af44baae65ef386ad971469a8557a0673bb042b0b9fd4397becd9c2dfaa02588", size = 134445, upload-time = "2026-01-29T15:11:59.819Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6f/ec0309154457b9ba1ad05f11faa4441f76037152f75e1ac577db3ce7ca96/orjson-3.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c310a48542094e4f7dbb6ac076880994986dda8ca9186a58c3cb70a3514d3231", size = 137708, upload-time = "2026-01-29T15:12:01.488Z" }, + { url = "https://files.pythonhosted.org/packages/20/52/3c71b80840f8bab9cb26417302707b7716b7d25f863f3a541bcfa232fe6e/orjson-3.11.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8dfa7a5d387f15ecad94cb6b2d2d5f4aeea64efd8d526bfc03c9812d01e1cc0", size = 134798, upload-time = "2026-01-29T15:12:02.705Z" }, + { url = "https://files.pythonhosted.org/packages/30/51/b490a43b22ff736282360bd02e6bded455cf31dfc3224e01cd39f919bbd2/orjson-3.11.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba8daee3e999411b50f8b50dbb0a3071dd1845f3f9a1a0a6fa6de86d1689d84d", size = 140839, upload-time = "2026-01-29T15:12:03.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/bc/4bcfe4280c1bc63c5291bb96f98298845b6355da2226d3400e17e7b51e53/orjson-3.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89d104c974eafd7436d7a5fdbc57f7a1e776789959a2f4f1b2eab5c62a339f4", size = 144080, upload-time = "2026-01-29T15:12:05.151Z" }, + { url = "https://files.pythonhosted.org/packages/01/74/22970f9ead9ab1f1b5f8c227a6c3aa8d71cd2c5acd005868a1d44f2362fa/orjson-3.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e2e2456788ca5ea75616c40da06fc885a7dc0389780e8a41bf7c5389ba257b", size = 142435, upload-time = "2026-01-29T15:12:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/29/34/d564aff85847ab92c82ee43a7a203683566c2fca0723a5f50aebbe759603/orjson-3.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a42efebc45afabb1448001e90458c4020d5c64fbac8a8dc4045b777db76cb5a", size = 145631, upload-time = "2026-01-29T15:12:08.351Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ef/016957a3890752c4aa2368326ea69fa53cdc1fdae0a94a542b6410dbdf52/orjson-3.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71b7cbef8471324966c3738c90ba38775563ef01b512feb5ad4805682188d1b9", size = 147058, upload-time = "2026-01-29T15:12:10.023Z" }, + { url = "https://files.pythonhosted.org/packages/56/cc/9a899c3972085645b3225569f91a30e221f441e5dc8126e6d060b971c252/orjson-3.11.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f8515e5910f454fe9a8e13c2bb9dc4bae4c1836313e967e72eb8a4ad874f0248", size = 421161, upload-time = "2026-01-29T15:12:11.308Z" }, + { url = "https://files.pythonhosted.org/packages/21/a8/767d3fbd6d9b8fdee76974db40619399355fd49bf91a6dd2c4b6909ccf05/orjson-3.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:300360edf27c8c9bf7047345a94fddf3a8b8922df0ff69d71d854a170cb375cf", size = 155757, upload-time = "2026-01-29T15:12:12.776Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0b/205cd69ac87e2272e13ef3f5f03a3d4657e317e38c1b08aaa2ef97060bbc/orjson-3.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:caaed4dad39e271adfadc106fab634d173b2bb23d9cf7e67bd645f879175ebfc", size = 147446, upload-time = "2026-01-29T15:12:14.166Z" }, + { url = "https://files.pythonhosted.org/packages/de/c5/dd9f22aa9f27c54c7d05cc32f4580c9ac9b6f13811eeb81d6c4c3f50d6b1/orjson-3.11.6-cp312-cp312-win32.whl", hash = "sha256:955368c11808c89793e847830e1b1007503a5923ddadc108547d3b77df761044", size = 139717, upload-time = "2026-01-29T15:12:15.7Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/e62fc50d904486970315a1654b8cfb5832eb46abb18cd5405118e7e1fc79/orjson-3.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:2c68de30131481150073d90a5d227a4a421982f42c025ecdfb66157f9579e06f", size = 136711, upload-time = "2026-01-29T15:12:17.055Z" }, + { url = "https://files.pythonhosted.org/packages/04/3d/b4fefad8bdf91e0fe212eb04975aeb36ea92997269d68857efcc7eb1dda3/orjson-3.11.6-cp312-cp312-win_arm64.whl", hash = "sha256:65dfa096f4e3a5e02834b681f539a87fbe85adc82001383c0db907557f666bfc", size = 135212, upload-time = "2026-01-29T15:12:18.3Z" }, + { url = "https://files.pythonhosted.org/packages/ae/45/d9c71c8c321277bc1ceebf599bc55ba826ae538b7c61f287e9a7e71bd589/orjson-3.11.6-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e4ae1670caabb598a88d385798692ce2a1b2f078971b3329cfb85253c6097f5b", size = 249828, upload-time = "2026-01-29T15:12:20.14Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7e/4afcf4cfa9c2f93846d70eee9c53c3c0123286edcbeb530b7e9bd2aea1b2/orjson-3.11.6-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:2c6b81f47b13dac2caa5d20fbc953c75eb802543abf48403a4703ed3bff225f0", size = 134339, upload-time = "2026-01-29T15:12:22.01Z" }, + { url = "https://files.pythonhosted.org/packages/40/10/6d2b8a064c8d2411d3d0ea6ab43125fae70152aef6bea77bb50fa54d4097/orjson-3.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:647d6d034e463764e86670644bdcaf8e68b076e6e74783383b01085ae9ab334f", size = 137662, upload-time = "2026-01-29T15:12:23.307Z" }, + { url = "https://files.pythonhosted.org/packages/5a/50/5804ea7d586baf83ee88969eefda97a24f9a5bdba0727f73e16305175b26/orjson-3.11.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8523b9cc4ef174ae52414f7699e95ee657c16aa18b3c3c285d48d7966cce9081", size = 134626, upload-time = "2026-01-29T15:12:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/f0492ed43e376722bb4afd648e06cc1e627fc7ec8ff55f6ee739277813ea/orjson-3.11.6-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:313dfd7184cde50c733fc0d5c8c0e2f09017b573afd11dc36bd7476b30b4cb17", size = 140873, upload-time = "2026-01-29T15:12:26.369Z" }, + { url = "https://files.pythonhosted.org/packages/10/15/6f874857463421794a303a39ac5494786ad46a4ab46d92bda6705d78c5aa/orjson-3.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905ee036064ff1e1fd1fb800055ac477cdcb547a78c22c1bc2bbf8d5d1a6fb42", size = 144044, upload-time = "2026-01-29T15:12:28.082Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c7/b7223a3a70f1d0cc2d86953825de45f33877ee1b124a91ca1f79aa6e643f/orjson-3.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce374cb98411356ba906914441fc993f271a7a666d838d8de0e0900dd4a4bc12", size = 142396, upload-time = "2026-01-29T15:12:30.529Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/aa1b6d3ad3cd80f10394134f73ae92a1d11fdbe974c34aa199cc18bb5fcf/orjson-3.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cded072b9f65fcfd188aead45efa5bd528ba552add619b3ad2a81f67400ec450", size = 145600, upload-time = "2026-01-29T15:12:31.848Z" }, + { url = "https://files.pythonhosted.org/packages/f6/cf/e4aac5a46cbd39d7e769ef8650efa851dfce22df1ba97ae2b33efe893b12/orjson-3.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ab85bdbc138e1f73a234db6bb2e4cc1f0fcec8f4bd2bd2430e957a01aadf746", size = 146967, upload-time = "2026-01-29T15:12:33.203Z" }, + { url = "https://files.pythonhosted.org/packages/0b/04/975b86a4bcf6cfeda47aad15956d52fbeda280811206e9967380fa9355c8/orjson-3.11.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:351b96b614e3c37a27b8ab048239ebc1e0be76cc17481a430d70a77fb95d3844", size = 421003, upload-time = "2026-01-29T15:12:35.097Z" }, + { url = "https://files.pythonhosted.org/packages/28/d1/0369d0baf40eea5ff2300cebfe209883b2473ab4aa4c4974c8bd5ee42bb2/orjson-3.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f9959c85576beae5cdcaaf39510b15105f1ee8b70d5dacd90152617f57be8c83", size = 155695, upload-time = "2026-01-29T15:12:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1f/d10c6d6ae26ff1d7c3eea6fd048280ef2e796d4fb260c5424fd021f68ecf/orjson-3.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75682d62b1b16b61a30716d7a2ec1f4c36195de4a1c61f6665aedd947b93a5d5", size = 147392, upload-time = "2026-01-29T15:12:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/8d/43/7479921c174441a0aa5277c313732e20713c0969ac303be9f03d88d3db5d/orjson-3.11.6-cp313-cp313-win32.whl", hash = "sha256:40dc277999c2ef227dcc13072be879b4cfd325502daeb5c35ed768f706f2bf30", size = 139718, upload-time = "2026-01-29T15:12:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/88/bc/9ffe7dfbf8454bc4e75bb8bf3a405ed9e0598df1d3535bb4adcd46be07d0/orjson-3.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:f0f6e9f8ff7905660bc3c8a54cd4a675aa98f7f175cf00a59815e2ff42c0d916", size = 136635, upload-time = "2026-01-29T15:12:40.593Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/51fa90b451470447ea5023b20d83331ec741ae28d1e6d8ed547c24e7de14/orjson-3.11.6-cp313-cp313-win_arm64.whl", hash = "sha256:1608999478664de848e5900ce41f25c4ecdfc4beacbc632b6fd55e1a586e5d38", size = 135175, upload-time = "2026-01-29T15:12:41.997Z" }, + { url = "https://files.pythonhosted.org/packages/31/9f/46ca908abaeeec7560638ff20276ab327b980d73b3cc2f5b205b4a1c60b3/orjson-3.11.6-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6026db2692041d2a23fe2545606df591687787825ad5821971ef0974f2c47630", size = 249823, upload-time = "2026-01-29T15:12:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/ff/78/ca478089818d18c9cd04f79c43f74ddd031b63c70fa2a946eb5e85414623/orjson-3.11.6-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:132b0ab2e20c73afa85cf142e547511feb3d2f5b7943468984658f3952b467d4", size = 134328, upload-time = "2026-01-29T15:12:45.171Z" }, + { url = "https://files.pythonhosted.org/packages/39/5e/cbb9d830ed4e47f4375ad8eef8e4fff1bf1328437732c3809054fc4e80be/orjson-3.11.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b376fb05f20a96ec117d47987dd3b39265c635725bda40661b4c5b73b77b5fde", size = 137651, upload-time = "2026-01-29T15:12:46.602Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3a/35df6558c5bc3a65ce0961aefee7f8364e59af78749fc796ea255bfa0cf5/orjson-3.11.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:954dae4e080574672a1dfcf2a840eddef0f27bd89b0e94903dd0824e9c1db060", size = 134596, upload-time = "2026-01-29T15:12:47.95Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8e/3d32dd7b7f26a19cc4512d6ed0ae3429567c71feef720fe699ff43c5bc9e/orjson-3.11.6-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe515bb89d59e1e4b48637a964f480b35c0a2676de24e65e55310f6016cca7ce", size = 140923, upload-time = "2026-01-29T15:12:49.333Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/1efbf5c99b3304f25d6f0d493a8d1492ee98693637c10ce65d57be839d7b/orjson-3.11.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:380f9709c275917af28feb086813923251e11ee10687257cd7f1ea188bcd4485", size = 144068, upload-time = "2026-01-29T15:12:50.927Z" }, + { url = "https://files.pythonhosted.org/packages/82/83/0d19eeb5be797de217303bbb55dde58dba26f996ed905d301d98fd2d4637/orjson-3.11.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8173e0d3f6081e7034c51cf984036d02f6bab2a2126de5a759d79f8e5a140e7", size = 142493, upload-time = "2026-01-29T15:12:52.432Z" }, + { url = "https://files.pythonhosted.org/packages/32/a7/573fec3df4dc8fc259b7770dc6c0656f91adce6e19330c78d23f87945d1e/orjson-3.11.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dddf9ba706294906c56ef5150a958317b09aa3a8a48df1c52ccf22ec1907eac", size = 145616, upload-time = "2026-01-29T15:12:53.903Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0e/23551b16f21690f7fd5122e3cf40fdca5d77052a434d0071990f97f5fe2f/orjson-3.11.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cbae5c34588dc79938dffb0b6fbe8c531f4dc8a6ad7f39759a9eb5d2da405ef2", size = 146951, upload-time = "2026-01-29T15:12:55.698Z" }, + { url = "https://files.pythonhosted.org/packages/b8/63/5e6c8f39805c39123a18e412434ea364349ee0012548d08aa586e2bd6aa9/orjson-3.11.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f75c318640acbddc419733b57f8a07515e587a939d8f54363654041fd1f4e465", size = 421024, upload-time = "2026-01-29T15:12:57.434Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4d/724975cf0087f6550bd01fd62203418afc0ea33fd099aed318c5bcc52df8/orjson-3.11.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e0ab8d13aa2a3e98b4a43487c9205b2c92c38c054b4237777484d503357c8437", size = 155774, upload-time = "2026-01-29T15:12:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a3/f4c4e3f46b55db29e0a5f20493b924fc791092d9a03ff2068c9fe6c1002f/orjson-3.11.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f884c7fb1020d44612bd7ac0db0babba0e2f78b68d9a650c7959bf99c783773f", size = 147393, upload-time = "2026-01-29T15:13:00.769Z" }, + { url = "https://files.pythonhosted.org/packages/ee/86/6f5529dd27230966171ee126cecb237ed08e9f05f6102bfaf63e5b32277d/orjson-3.11.6-cp314-cp314-win32.whl", hash = "sha256:8d1035d1b25732ec9f971e833a3e299d2b1a330236f75e6fd945ad982c76aaf3", size = 139760, upload-time = "2026-01-29T15:13:02.173Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b5/91ae7037b2894a6b5002fb33f4fbccec98424a928469835c3837fbb22a9b/orjson-3.11.6-cp314-cp314-win_amd64.whl", hash = "sha256:931607a8865d21682bb72de54231655c86df1870502d2962dbfd12c82890d077", size = 136633, upload-time = "2026-01-29T15:13:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/74/f473a3ec7a0a7ebc825ca8e3c86763f7d039f379860c81ba12dcdd456547/orjson-3.11.6-cp314-cp314-win_arm64.whl", hash = "sha256:fe71f6b283f4f1832204ab8235ce07adad145052614f77c876fcf0dac97bc06f", size = 135168, upload-time = "2026-01-29T15:13:05.932Z" }, ] [[package]] @@ -6793,28 +6796,28 @@ wheels = [ [[package]] name = "uv" -version = "0.9.27" +version = "0.9.28" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/70/611bcee4385b7aa00cf7acf29bc51854c365ee09ddb2cdb14b07a36b4db8/uv-0.9.27.tar.gz", hash = "sha256:9147862902b5d40894f78339803225e39b0535c4c04537188de160eb7635e46b", size = 3830404, upload-time = "2026-01-26T23:27:43.442Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/7d/005ab1cab03ca928cef75b424284d14d62c5f18775cf8114a63f210a0c9c/uv-0.9.28.tar.gz", hash = "sha256:253c04b26fb40f74c56ead12ce83db3c018bdefde1fcd1a542bcb88fdca4189c", size = 3834456, upload-time = "2026-01-29T20:15:49.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/f1/9e9afb9fadb73f7914a0fc63b6f9d182b6fe6b1e55deb7cbdc29ff31299f/uv-0.9.27-py3-none-linux_armv6l.whl", hash = "sha256:ce3f16e66a96dcdc63f6ada9f7747686930986d2df104a9dd2d09664b2d870c8", size = 22011502, upload-time = "2026-01-26T23:27:31.714Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b2/c36a87f5c745d310b7d8a53df053d6a87864aa38e3a964b0845eb6de37cc/uv-0.9.27-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a662a7df5cc781ae7baa65171b5d488d946ea93e61b7bbeda5a24d21a0cd9003", size = 21081065, upload-time = "2026-01-26T23:28:11.895Z" }, - { url = "https://files.pythonhosted.org/packages/44/1d/be2d80573c531389933059f6e5ef265ef7324c268f3ade80e500aa627f6b/uv-0.9.27-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f00158023e77600da602c5f1fa97cd8c2eef987d6aba34c16cf04a3e5a932f4", size = 19844905, upload-time = "2026-01-26T23:27:34.486Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f7/59679af9f0446d8ffc1239e3356390c95925e0004549b64df3f189b1422b/uv-0.9.27-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5f2393051ed2023cc7d6ff99e41184b7c7bb7da001bc727cd4bee6da96f4a966", size = 21592623, upload-time = "2026-01-26T23:27:51.132Z" }, - { url = "https://files.pythonhosted.org/packages/e2/31/0faaad82951fc6b14dfad8e187e43747a528aa50ee283385f903e86d67d1/uv-0.9.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:3f8cf7a50a95ae5cb0366d24edf79d15b3ba380b462af49e3404f9f7b91101c7", size = 21636917, upload-time = "2026-01-26T23:27:46.252Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8a/e181c32b7f5309fd987667d368fb944822c713e92a7eba3c73d2eddec6cd/uv-0.9.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c331e0445465ea6029e2dd0f5499024e552136c10069fac0ca21708f2aeb1ce6", size = 21633082, upload-time = "2026-01-26T23:28:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1d44157bc8e5d1c382db087d9a30ab85fc7b5c2d610fb2e3d861c5a69d9b/uv-0.9.27-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a56e0c92da67060d4c86a3b92b2c69e8fb1d61933636764aba16531ddb13f6e3", size = 22843044, upload-time = "2026-01-26T23:27:29.091Z" }, - { url = "https://files.pythonhosted.org/packages/eb/76/7c1b13e4dc8237dd3721f4ec933bb2e5be400fd2812cf98dc2be645a0f7d/uv-0.9.27-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0c9b2e874f5207a50b852726f3a0740eadf30baf2c7973380d697f4e3166d347", size = 24141329, upload-time = "2026-01-26T23:27:39.705Z" }, - { url = "https://files.pythonhosted.org/packages/6f/04/551749fd863cb43290c9a3f4348ccdd88ec0445c26a00ba840d776572867/uv-0.9.27-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79841a2e4df4a32f22fbb0919c3e513226684572fba227b37467ba6404f3fafd", size = 23637517, upload-time = "2026-01-26T23:27:37.111Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c6/78b619a51a6646af4633714b161f980ab764cc892e0f79228162fba51fe8/uv-0.9.27-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33902c95b561ac67d15c339fe1eaf39e068372c7464c79c3bd0e2bf9ee914dcb", size = 22864516, upload-time = "2026-01-26T23:27:56.35Z" }, - { url = "https://files.pythonhosted.org/packages/15/19/b35928e55307beb69b60b88446df3cb8d7ff3ba0993fc2214a43266c17d1/uv-0.9.27-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79939f7e92d707fb84933509df747d1b88b00d94ebe41f3a1e30916cc33c7307", size = 22746151, upload-time = "2026-01-26T23:27:26.166Z" }, - { url = "https://files.pythonhosted.org/packages/9f/70/fbab20d40afe7ac9ec20011acec75f8bb3b9b83dfbe2cdb1405cad7a8cf2/uv-0.9.27-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7e2d4183a0dca7596ea6385e9d5a0a87ada4f71a70aa110e2b22234370b8d8ef", size = 21661188, upload-time = "2026-01-26T23:27:53.79Z" }, - { url = "https://files.pythonhosted.org/packages/44/02/4d4cf298bd22e53d6c289404b093cf876e64ee1fb946cc32a6f965030629/uv-0.9.27-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1555ab7bc8501144e8771e54a628eb02cb95f3612d54659bb7132576260feee5", size = 22397798, upload-time = "2026-01-26T23:28:07.102Z" }, - { url = "https://files.pythonhosted.org/packages/97/29/3acef6a0eea58afbf7f7a08e4258430e3c7394a6b1e28249450f4c0ddc60/uv-0.9.27-py3-none-musllinux_1_1_i686.whl", hash = "sha256:542731a6f53072e725959a9c839b195048715d840213d9834d36f74fa4249855", size = 22111665, upload-time = "2026-01-26T23:27:58.762Z" }, - { url = "https://files.pythonhosted.org/packages/13/15/1e7b34f02e8f53c9498311f991421e794ad57fa60a2d3e41b43485e914e4/uv-0.9.27-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:4f534ad701ca3fffac4a8e1df2a36930e6a0cbf4dad52aeabc2c3c9e2cbbe65e", size = 22951420, upload-time = "2026-01-26T23:27:48.818Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c5/c3bb3a5885891ed8fe7dcc897db03366f3e19da8bf48ae8f5ea4da34545d/uv-0.9.27-py3-none-win32.whl", hash = "sha256:18aab0e19634997366907a9b8a1648e79b0fa34d1b86d8e8ee1e7ba5b9faa6ae", size = 20817398, upload-time = "2026-01-26T23:28:14.265Z" }, - { url = "https://files.pythonhosted.org/packages/db/19/22d2671928f6d2fef1edfdbf758abbb5a0f218b69fd23bd5fd52bbe5b078/uv-0.9.27-py3-none-win_amd64.whl", hash = "sha256:f961c53f83ae6a01e3ffacc584c91044958bc6db003e803c490e106a79981222", size = 23412228, upload-time = "2026-01-26T23:28:01.547Z" }, - { url = "https://files.pythonhosted.org/packages/17/eb/9a71112e2266b79da552a4b2ffb52331ca7171437b901705427f8e54e77c/uv-0.9.27-py3-none-win_arm64.whl", hash = "sha256:463327fb343c3085a3333389d6e5908cb48203b327707790c06bdc8f2ca57b95", size = 21836206, upload-time = "2026-01-26T23:28:04.411Z" }, + { url = "https://files.pythonhosted.org/packages/77/dc/e70698756f1bb74c88bf1eaea63a114a580a38f296ea1567a01db9007490/uv-0.9.28-py3-none-linux_armv6l.whl", hash = "sha256:aede961243bb2c0ca09d0e04ea0bf580d7128dd3b14661b79d133be9a5b69894", size = 22040477, upload-time = "2026-01-29T20:16:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ed/77294752bf722e1d6b666bd6592b6ac975dabcf1fde49e98a75cac23d45c/uv-0.9.28-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fe9aa2822d24f6ecec035a06dfdd1fbed570ed40b83a864e71714bad37ddfd3", size = 21025194, upload-time = "2026-01-29T20:15:36.504Z" }, + { url = "https://files.pythonhosted.org/packages/b1/a9/78f2da6217c1bbae3371d68515fe747e1160bab049d6898a03e517802573/uv-0.9.28-py3-none-macosx_11_0_arm64.whl", hash = "sha256:58a36bf623c6d36b3d60d3c76eeb7275199d607938786e927d40ce213980059d", size = 19783994, upload-time = "2026-01-29T20:16:19.451Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/55639c444e91b96c81c326d39a0a06551d2e611be0cc917b89010ba9ba88/uv-0.9.28-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4d479a1d387b1464ad2c1f960b0b26a9ac1dfba67ea2c6789e9643fe6d1e7b9a", size = 21568230, upload-time = "2026-01-29T20:15:39.35Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/95d7992c0a39981cfbcf56ff8f069c09e0567feb0e70cb8b52bc8a2947a0/uv-0.9.28-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:29eefd4642f55954a2b9a40619cde3d02856300f59b8cf63ed1a161ca0ca9b77", size = 21633679, upload-time = "2026-01-29T20:15:52.363Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/b6778e03714b1f9da095c8bf0f8e5007f4867d9196c1ae8053504ddf2877/uv-0.9.28-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4155496f624deb753f5ddd80fbe3797587c8480d1250e83c9fd816b4b02e3a41", size = 21632238, upload-time = "2026-01-29T20:15:55.003Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/0db6ea9fd8f2752a8723a637e3ed881eb212516665ccb2e8066bbea62a52/uv-0.9.28-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dc98e2d6db0dc9a2f65ce4cda6a34283fa80f3fbfff129befdf40ad7a3d1615", size = 22779474, upload-time = "2026-01-29T20:15:33.513Z" }, + { url = "https://files.pythonhosted.org/packages/54/88/ef70e04113393f4e19e67281cae9f83c82030d14eb4eb811bda83fcd8f44/uv-0.9.28-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d267280b3878aa6ef8e00bff1f11bf61580d0a8bbb69fa95b5d3526d00f77485", size = 24124596, upload-time = "2026-01-29T20:16:05.062Z" }, + { url = "https://files.pythonhosted.org/packages/81/07/9fda9149bc57e79bde5f00cabcef323a68817c1cca9d44e2aa08d18c6b52/uv-0.9.28-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba2a320ff77996468789f4b2c573fd766f9330717c440335af8790043b2b3703", size = 23655701, upload-time = "2026-01-29T20:16:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/18/b5/1f1e910ca1a0aca0d0ede3ba0eaca867fd3c575f44b2fe103a5c9511f071/uv-0.9.28-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c8fd93c5bee89ed88908215f81a3baa0d2a98e35caf995b97e9c226c1c29340", size = 22856456, upload-time = "2026-01-29T20:16:16.582Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fd/82561751105ed232f1781747bc336b20e8d57ee07b4d2ed3fa6cf2718d71/uv-0.9.28-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b8460a2b624d8ab27cb293a2c9f2393f9efc4e36e0fb886a6c2360e23fb48be", size = 22685296, upload-time = "2026-01-29T20:16:13.857Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e4/b905daff0bfde347c49b9c9ba31d09d504c4b84f2749a07db77a9da16dba/uv-0.9.28-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3798c486ec627bbd7ca41fa219e997ad403b1f803371edf5c8e75893e46161ba", size = 21669854, upload-time = "2026-01-29T20:15:30.277Z" }, + { url = "https://files.pythonhosted.org/packages/9a/01/9a90574fe7290c775332e54f163cba58c767445b655e97646708f9c66050/uv-0.9.28-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e479cc5cbfd72ebdbea3c909d0ab997162e0dfa1ee622b50e2f9dc8d07d4eee3", size = 22388944, upload-time = "2026-01-29T20:15:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/cc35014bab3c17b4fe8f6bae84e640ce64d9bb4c8a24694a935e0c0af538/uv-0.9.28-py3-none-musllinux_1_1_i686.whl", hash = "sha256:97d61cdf2436e83a0f188d55d1974e46679d9a787c3a54cb0a40de717c6bf435", size = 22073327, upload-time = "2026-01-29T20:15:58.119Z" }, + { url = "https://files.pythonhosted.org/packages/26/cd/e848570be5c5be4e139b90237cc64f68d5d51e8e92c40a5ac7cf0c34ad4a/uv-0.9.28-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:cbfa56c833caa37b1f14166327fcaf8aa87290451406921eb07296ffef17fef1", size = 22915580, upload-time = "2026-01-29T20:15:42.468Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/6c3d839ea289bf8509da32f703a47accd63ab409b33627728aebcd2a1b65/uv-0.9.28-py3-none-win32.whl", hash = "sha256:d5cb780d5b821f837f63e7fd14e2bf75f01824b4575a1e89639888771bfd9efd", size = 20856809, upload-time = "2026-01-29T20:15:45.141Z" }, + { url = "https://files.pythonhosted.org/packages/06/a8/d72229dd90d1e5a3c8368d51a70219018d579380945e67c8dcffbe8e53c0/uv-0.9.28-py3-none-win_amd64.whl", hash = "sha256:203ab59710c0c1b3c5ecc684f9cfc9264340a69c8706aaa8aea75415779f0d74", size = 23447461, upload-time = "2026-01-29T20:16:22.563Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/5852eb0c59e5224f4cb0323906efae348f782f8a7f1069197e7cf6ec9b74/uv-0.9.28-py3-none-win_arm64.whl", hash = "sha256:c29406e1dc6b1b312c478c76b42b9f94b684855a4c001901b5488bab6ccf4ec7", size = 21860859, upload-time = "2026-01-29T20:16:00.764Z" }, ] [[package]] From 924211a5189d3e8c19217c79b4e955b37e23c561 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:48:19 -0800 Subject: [PATCH 02/20] .Net: Update Anthropic and Anthropic.Foundry package versions (#3517) * Initial plan * Update Anthropic packages to v12.3.0 and Anthropic.Foundry to v0.4.1 Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Fix AnthropicClient not being disposed in sample --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> --- dotnet/Directory.Packages.props | 6 +-- .../Agent_With_Anthropic/Program.cs | 53 ++----------------- .../Agent_Anthropic_Step01_Running/Program.cs | 2 +- .../Program.cs | 2 +- .../Program.cs | 2 +- .../05_MultiModelService/Program.cs | 2 +- .../AnthropicChatCompletionFixture.cs | 2 +- .../AnthropicBetaServiceExtensionsTests.cs | 15 +++--- .../AnthropicClientExtensionsTests.cs | 13 ++--- 9 files changed, 28 insertions(+), 69 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 00435037d1..98c7376aaf 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -11,8 +11,8 @@ - - + + @@ -42,7 +42,7 @@ - + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs index ad49c9229e..b281274051 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs @@ -2,15 +2,12 @@ // This sample shows how to create and use an AI agent with Anthropic as the backend. -using System.Net.Http.Headers; using Anthropic; using Anthropic.Foundry; -using Azure.Core; using Azure.Identity; using Microsoft.Agents.AI; -using Sample; -var deploymentName = Environment.GetEnvironmentVariable("ANTHROPIC_DEPLOYMENT_NAME") ?? "claude-haiku-4-5"; +string deploymentName = Environment.GetEnvironmentVariable("ANTHROPIC_DEPLOYMENT_NAME") ?? "claude-haiku-4-5"; // The resource is the subdomain name / first name coming before '.services.ai.azure.com' in the endpoint Uri // ie: https://(resource name).services.ai.azure.com/anthropic/v1/chat/completions @@ -20,55 +17,13 @@ string? apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY"); const string JokerInstructions = "You are good at telling jokes."; const string JokerName = "JokerAgent"; -AnthropicClient? client = (resource is null) - ? new AnthropicClient() { APIKey = apiKey ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is required when no ANTHROPIC_RESOURCE is provided") } // If no resource is provided, use Anthropic public API +using AnthropicClient client = (resource is null) + ? new AnthropicClient() { ApiKey = apiKey ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is required when no ANTHROPIC_RESOURCE is provided") } // If no resource is provided, use Anthropic public API : (apiKey is not null) ? new AnthropicFoundryClient(new AnthropicFoundryApiKeyCredentials(apiKey, resource)) // If an apiKey is provided, use Foundry with ApiKey authentication - : new AnthropicFoundryClient(new AnthropicAzureTokenCredential(new AzureCliCredential(), resource)); // Otherwise, use Foundry with Azure Client authentication + : new AnthropicFoundryClient(new AnthropicFoundryIdentityTokenCredentials(new AzureCliCredential(), resource, ["https://ai.azure.com/.default"])); // Otherwise, use Foundry with Azure TokenCredential authentication AIAgent agent = client.AsAIAgent(model: deploymentName, instructions: JokerInstructions, name: JokerName); // Invoke the agent and output the text result. Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); - -namespace Sample -{ - /// - /// Provides methods for invoking the Azure hosted Anthropic models using types. - /// - public sealed class AnthropicAzureTokenCredential : IAnthropicFoundryCredentials - { - private readonly TokenCredential _tokenCredential; - private readonly Lock _lock = new(); - private AccessToken? _cachedAccessToken; - - /// - public string ResourceName { get; } - - /// - /// Creates a new instance of the . - /// - /// The credential provider. Use any specialization of to get your access token in supported environments. - /// The service resource subdomain name to use in the anthropic azure endpoint - internal AnthropicAzureTokenCredential(TokenCredential tokenCredential, string resourceName) - { - this.ResourceName = resourceName ?? throw new ArgumentNullException(nameof(resourceName)); - this._tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential)); - } - - /// - public void Apply(HttpRequestMessage requestMessage) - { - lock (this._lock) - { - // Add a 5-minute buffer to avoid using tokens that are about to expire - if (this._cachedAccessToken is null || this._cachedAccessToken.Value.ExpiresOn <= DateTimeOffset.Now.AddMinutes(5)) - { - this._cachedAccessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), CancellationToken.None); - } - } - - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", this._cachedAccessToken.Value.Token); - } - } -} diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs index 085fbfd989..7aee814c0c 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.AI; var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set."); var model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-haiku-4-5"; -AIAgent agent = new AnthropicClient(new ClientOptions { APIKey = apiKey }) +AIAgent agent = new AnthropicClient(new ClientOptions { ApiKey = apiKey }) .AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker"); // Invoke the agent and output the text result. diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs index 120402ee14..78633cb7a8 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs @@ -13,7 +13,7 @@ var model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-hai var maxTokens = 4096; var thinkingTokens = 2048; -var agent = new AnthropicClient(new ClientOptions { APIKey = apiKey }) +var agent = new AnthropicClient(new ClientOptions { ApiKey = apiKey }) .AsAIAgent( model: model, clientFactory: (chatClient) => chatClient diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs index f9634d9212..6c32741c94 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs @@ -22,7 +22,7 @@ const string AssistantName = "WeatherAssistant"; AITool tool = AIFunctionFactory.Create(GetWeather); // Get anthropic client to create agents. -AIAgent agent = new AnthropicClient { APIKey = apiKey } +AIAgent agent = new AnthropicClient { ApiKey = apiKey } .AsAIAgent(model: model, instructions: AssistantInstructions, name: AssistantName, tools: [tool]); // Non-streaming agent interaction with function tools. diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/Program.cs b/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/Program.cs index 7d81d891f7..9eca1e3e4e 100644 --- a/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/Program.cs @@ -16,7 +16,7 @@ IChatClient aws = new AmazonBedrockRuntimeClient( .AsIChatClient("amazon.nova-pro-v1:0"); IChatClient anthropic = new Anthropic.AnthropicClient( - new() { APIKey = Environment.GetEnvironmentVariable("ANTHROPIC_APIKEY") }) + new() { ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_APIKEY") }) .AsIChatClient("claude-sonnet-4-20250514"); IChatClient openai = new OpenAI.OpenAIClient( diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs index 555576df9d..5f0fcbca2c 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs @@ -52,7 +52,7 @@ public class AnthropicChatCompletionFixture : IChatClientAgentFixture string instructions = "You are a helpful assistant.", IList? aiTools = null) { - var anthropicClient = new AnthropicClient() { APIKey = s_config.ApiKey }; + var anthropicClient = new AnthropicClient() { ApiKey = s_config.ApiKey }; IChatClient? chatClient = this._useBeta ? anthropicClient diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs index c26be5822c..738475da03 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs @@ -242,7 +242,7 @@ public sealed class AnthropicBetaServiceExtensionsTests var client = new AnthropicClient { HttpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }, - APIKey = "test-key" + ApiKey = "test-key" }; // Act @@ -436,13 +436,15 @@ public sealed class AnthropicBetaServiceExtensionsTests } public HttpClient HttpClient { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } - public Uri BaseUrl { get => new("http://localhost"); init => throw new NotImplementedException(); } + public string BaseUrl { get => "http://localhost"; init => throw new NotImplementedException(); } public bool ResponseValidation { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public int? MaxRetries { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public TimeSpan? Timeout { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } - public string? APIKey { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } + public string? ApiKey { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public string? AuthToken { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } + public IAnthropicClientWithRawResponse WithRawResponse => throw new NotImplementedException(); + public IMessageService Messages => throw new NotImplementedException(); public IModelService Models => throw new NotImplementedException(); @@ -453,14 +455,13 @@ public sealed class AnthropicBetaServiceExtensionsTests IMessageService IAnthropicClient.Messages => new Mock().Object; - public Task Execute(HttpRequest request, CancellationToken cancellationToken = default) where T : ParamsBase + public IAnthropicClient WithOptions(Func modifier) { throw new NotImplementedException(); } - public IAnthropicClient WithOptions(Func modifier) + public void Dispose() { - throw new NotImplementedException(); } private sealed class TestBetaService : IBetaService @@ -472,6 +473,8 @@ public sealed class AnthropicBetaServiceExtensionsTests this._client = client; } + public IBetaServiceWithRawResponse WithRawResponse => throw new NotImplementedException(); + public global::Anthropic.Services.Beta.IModelService Models => throw new NotImplementedException(); public global::Anthropic.Services.Beta.IFileService Files => throw new NotImplementedException(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs index 4a61a699fc..33cd212928 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs @@ -66,27 +66,28 @@ public sealed class AnthropicClientExtensionsTests } public HttpClient HttpClient { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } - public Uri BaseUrl { get => new("http://localhost"); init => throw new NotImplementedException(); } + public string BaseUrl { get => "http://localhost"; init => throw new NotImplementedException(); } public bool ResponseValidation { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public int? MaxRetries { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public TimeSpan? Timeout { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } - public string? APIKey { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } + public string? ApiKey { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public string? AuthToken { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } + public IAnthropicClientWithRawResponse WithRawResponse => throw new NotImplementedException(); + public IMessageService Messages => throw new NotImplementedException(); public IModelService Models => throw new NotImplementedException(); public IBetaService Beta => throw new NotImplementedException(); - public Task Execute(HttpRequest request, CancellationToken cancellationToken = default) where T : ParamsBase + public IAnthropicClient WithOptions(Func modifier) { throw new NotImplementedException(); } - public IAnthropicClient WithOptions(Func modifier) + public void Dispose() { - throw new NotImplementedException(); } } @@ -309,7 +310,7 @@ public sealed class AnthropicClientExtensionsTests var client = new AnthropicClient { HttpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }, - APIKey = "test-key" + ApiKey = "test-key" }; // Act From ff7041b9900c0e75fe3250dd8e9e17cbda0dedef Mon Sep 17 00:00:00 2001 From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Date: Fri, 30 Jan 2026 09:14:24 -0800 Subject: [PATCH 03/20] .NET: Workflows - Support fidelity when converting to and from ChatMessage in declarative workflows (#3505) * Builds locally and tests pass * Fix typo * Updated * Updated * Fixed tests failing on net472 but not on dotnet10 --------- Co-authored-by: Chris Rickman --- .../Extensions/ChatMessageExtensions.cs | 43 ++++++-- .../AddConversationMessageExecutor.cs | 2 +- .../MediaInputTest.cs | 51 +++++---- .../Workflows/MediaInputAutoSend.yaml | 15 +++ ...Input.yaml => MediaInputConversation.yaml} | 0 .../Extensions/ChatMessageExtensionsTests.cs | 104 ++++++++++++++++++ 6 files changed, 187 insertions(+), 28 deletions(-) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInputAutoSend.yaml rename dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/{MediaInput.yaml => MediaInputConversation.yaml} (100%) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/ChatMessageExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/ChatMessageExtensions.cs index fae51072ca..75a87fb8ee 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/ChatMessageExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/ChatMessageExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; @@ -83,7 +84,8 @@ internal static class ChatMessageExtensions public static ChatMessage ToChatMessage(this RecordDataValue message) => new(message.GetRole(), [.. message.GetContent()]) { - AdditionalProperties = message.GetProperty("metadata").ToMetadata() + MessageId = message.GetProperty(TypeSchema.Message.Fields.Id)?.Value, + AdditionalProperties = message.GetProperty(TypeSchema.Message.Fields.Metadata).ToMetadata() }; public static ChatMessage ToChatMessage(this StringDataValue message) => new(ChatRole.User, message.Value); @@ -118,7 +120,7 @@ internal static class ChatMessageExtensions public static ChatRole ToChatRole(this AgentMessageRole? role) => role?.ToChatRole() ?? ChatRole.User; - public static AIContent? ToContent(this AgentMessageContentType contentType, string? contentValue) + public static AIContent? ToContent(this AgentMessageContentType contentType, string? contentValue, string? mediaType = null) { if (string.IsNullOrEmpty(contentValue)) { @@ -128,7 +130,7 @@ internal static class ChatMessageExtensions return contentType switch { - AgentMessageContentType.ImageUrl => GetImageContent(contentValue), + AgentMessageContentType.ImageUrl => GetImageContent(contentValue, mediaType ?? InferMediaType(contentValue)), AgentMessageContentType.ImageFile => new HostedFileContent(contentValue), _ => new TextContent(contentValue) }; @@ -159,14 +161,16 @@ internal static class ChatMessageExtensions foreach (RecordDataValue contentItem in content.Values) { StringDataValue? contentValue = contentItem.GetProperty(TypeSchema.MessageContent.Fields.Value); + StringDataValue? mediaTypeValue = contentItem.GetProperty(TypeSchema.MessageContent.Fields.MediaType); if (contentValue is null || string.IsNullOrWhiteSpace(contentValue.Value)) { continue; } + yield return contentItem.GetProperty(TypeSchema.MessageContent.Fields.Type)?.Value switch { - TypeSchema.MessageContent.ContentTypes.ImageUrl => GetImageContent(contentValue.Value), + TypeSchema.MessageContent.ContentTypes.ImageUrl => GetImageContent(contentValue.Value, mediaTypeValue?.Value ?? InferMediaType(contentValue.Value)), TypeSchema.MessageContent.ContentTypes.ImageFile => new HostedFileContent(contentValue.Value), _ => new TextContent(contentValue.Value) }; @@ -174,10 +178,35 @@ internal static class ChatMessageExtensions } } - private static AIContent GetImageContent(string uriText) => + private static string InferMediaType(string value) + { + // Base64 encoded content includes media type + if (value.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + int semicolonIndex = value.IndexOf(';'); + if (semicolonIndex > 5) + { + return value.Substring(5, semicolonIndex - 5); + } + } + + // URL based input only supports image + string fileExtension = Path.GetExtension(value); + return + fileExtension.ToUpperInvariant() switch + { + ".JPG" or ".JPEG" => "image/jpeg", + ".PNG" => "image/png", + ".GIF" => "image/gif", + ".WEBP" => "image/webp", + _ => "image/*" + }; + } + + private static AIContent GetImageContent(string uriText, string mediaType) => uriText.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ? - new DataContent(uriText, "image/*") : - new UriContent(uriText, "image/*"); + new DataContent(uriText, mediaType) : + new UriContent(uriText, mediaType); private static TValue? GetProperty(this RecordDataValue record, string name) where TValue : DataValue diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs index 922f8bb32f..e1514f2240 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs @@ -40,7 +40,7 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode { foreach (AddConversationMessageContent content in this.Model.Content) { - AIContent? messageContent = content.Type.Value.ToContent(this.Engine.Format(content.Value)); + AIContent? messageContent = content.Type.Value.ToContent(this.Engine.Format(content.Value), content.MediaType); if (messageContent is not null) { yield return messageContent; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs index ae3cdfd7a9..ff61f1191b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs @@ -19,43 +19,48 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; /// public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(output) { - private const string WorkflowFileName = "MediaInput.yaml"; + private const string WorkflowWithConversationFileName = "MediaInputConversation.yaml"; + private const string WorkflowWithAutoSendFileName = "MediaInputAutoSend.yaml"; private const string PdfReference = "https://sample-files.com/downloads/documents/pdf/basic-text.pdf"; private const string ImageReference = "https://sample-files.com/downloads/images/jpg/web_optimized_1200x800_97kb.jpg"; [Theory] - [InlineData(ImageReference, "image/jpeg", Skip = "Failing consistently in the agent service api")] - [InlineData(PdfReference, "application/pdf", Skip = "Not currently supported by agent service api")] - public async Task ValidateFileUrlAsync(string fileSource, string mediaType) + [InlineData(ImageReference, "image/jpeg", true, Skip = "Failing due to agent service bug.")] + [InlineData(ImageReference, "image/jpeg", false, Skip = "Failing due to agent service bug.")] + public async Task ValidateFileUrlAsync(string fileSource, string mediaType, bool useConversation) { this.Output.WriteLine($"File: {ImageReference}"); - await this.ValidateFileAsync(new UriContent(fileSource, mediaType)); + await this.ValidateFileAsync(new UriContent(fileSource, mediaType), useConversation); } [Theory] - [InlineData(ImageReference, "image/jpeg")] - [InlineData(PdfReference, "application/pdf")] - public async Task ValidateFileDataAsync(string fileSource, string mediaType) + [InlineData(ImageReference, "image/jpeg", true)] + [InlineData(ImageReference, "image/jpeg", false, Skip = "Failing due to agent service bug.")] + [InlineData(PdfReference, "application/pdf", true)] + [InlineData(PdfReference, "application/pdf", false)] + public async Task ValidateFileDataAsync(string fileSource, string mediaType, bool useConversation) { byte[] fileData = await DownloadFileAsync(fileSource); string encodedData = Convert.ToBase64String(fileData); string fileUrl = $"data:{mediaType};base64,{encodedData}"; this.Output.WriteLine($"Content: {fileUrl.Substring(0, 112)}..."); - await this.ValidateFileAsync(new DataContent(fileUrl)); + await this.ValidateFileAsync(new DataContent(fileUrl), useConversation); } - [Fact(Skip = "Not currently supported by agent service api")] - public async Task ValidateFileUploadAsync() + [Theory] + [InlineData(PdfReference, "doc.pdf", true, Skip = "Failing due to agent service bug.")] + [InlineData(PdfReference, "doc.pdf", false, Skip = "Failing due to agent service bug.")] + public async Task ValidateFileUploadAsync(string fileSource, string documentName, bool useConversation) { - byte[] fileData = await DownloadFileAsync(PdfReference); + byte[] fileData = await DownloadFileAsync(fileSource); AIProjectClient client = new(this.TestEndpoint, new AzureCliCredential()); using MemoryStream contentStream = new(fileData); OpenAIFileClient fileClient = client.GetProjectOpenAIClient().GetOpenAIFileClient(); - OpenAIFile fileInfo = await fileClient.UploadFileAsync(contentStream, "basic-text.pdf", FileUploadPurpose.Assistants); + OpenAIFile fileInfo = await fileClient.UploadFileAsync(contentStream, documentName, FileUploadPurpose.Assistants); try { this.Output.WriteLine($"File: {fileInfo.Id}"); - await this.ValidateFileAsync(new HostedFileContent(fileInfo.Id)); + await this.ValidateFileAsync(new HostedFileContent(fileInfo.Id), useConversation); } finally { @@ -70,20 +75,26 @@ public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(o return await client.GetByteArrayAsync(new Uri(uri)); } - private async Task ValidateFileAsync(AIContent fileContent) + private async Task ValidateFileAsync(AIContent fileContent, bool useConversation) { AgentProvider agentProvider = AgentProvider.Create(this.Configuration, AgentProvider.Names.Vision); await agentProvider.CreateAgentsAsync().ConfigureAwait(false); - ChatMessage inputMessage = new(ChatRole.User, [new TextContent("I've provided a file:"), fileContent]); + ChatMessage inputMessage = + new(ChatRole.User, + [ + new TextContent("I've provided a file:"), + fileContent + ]); + string workflowFileName = useConversation ? WorkflowWithConversationFileName : WorkflowWithAutoSendFileName; DeclarativeWorkflowOptions options = await this.CreateOptionsAsync(); - Workflow workflow = DeclarativeWorkflowBuilder.Build(Path.Combine(Environment.CurrentDirectory, "Workflows", WorkflowFileName), options); + Workflow workflow = DeclarativeWorkflowBuilder.Build(Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName), options); - WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(WorkflowFileName)); + WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(workflowFileName)); WorkflowEvents workflowEvents = await harness.RunWorkflowAsync(inputMessage).ConfigureAwait(false); - ConversationUpdateEvent conversationEvent = Assert.Single(workflowEvents.ConversationEvents); - this.Output.WriteLine("CONVERSATION: " + conversationEvent.ConversationId); + Assert.Equal(useConversation ? 1 : 2, workflowEvents.ConversationEvents.Count); + this.Output.WriteLine("CONVERSATION: " + workflowEvents.ConversationEvents[0].ConversationId); AgentResponseEvent agentResponseEvent = Assert.Single(workflowEvents.AgentResponseEvents); this.Output.WriteLine("RESPONSE: " + agentResponseEvent.Response.Text); Assert.NotEmpty(agentResponseEvent.Response.Text); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInputAutoSend.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInputAutoSend.yaml new file mode 100644 index 0000000000..f0e54c638c --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInputAutoSend.yaml @@ -0,0 +1,15 @@ +kind: Workflow +trigger: + + kind: OnConversationStart + id: workflow_test + actions: + + - kind: InvokeAzureAgent + id: invoke_vision + agent: + name: VisionAgent + input: + messages: =System.LastMessage + output: + autoSend: true diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInput.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInputConversation.yaml similarity index 100% rename from dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInput.yaml rename to dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInputConversation.yaml diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/ChatMessageExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/ChatMessageExtensionsTests.cs index 983efefb3c..5dae26e348 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/ChatMessageExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/ChatMessageExtensionsTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Agents.AI.Workflows.Declarative.Extensions; @@ -666,4 +667,107 @@ public sealed class ChatMessageExtensionsTests RecordValue metadataRecord = Assert.IsType(metadataField, exactMatch: false); Assert.Equal(2, metadataRecord.Fields.Count()); } + + [Fact] + public void RoundTripChatMessageAsRecord() + { + // Arrange + ChatMessage message = + new(ChatRole.User, + [ + new TextContent("Test message"), + new UriContent("https://example.com/image.jpg", "image/jpeg"), + new HostedFileContent("file_123abc"), + new DataContent(new byte[] { 1, 2, 3, 4, 5 }, "application/pdf"), + ]) + { + MessageId = "msg-001" + }; + + // Act + RecordValue result = message.ToRecord(); + DataValue resultValue = result.ToDataValue(); + ChatMessage? messageCopy = resultValue.ToChatMessage(); + + // Assert + Assert.NotNull(messageCopy); + Assert.Equal(message.Role, messageCopy.Role); + Assert.Equal(message.MessageId, messageCopy.MessageId); + Assert.Equal(message.Contents.Count, messageCopy.Contents.Count); + foreach (AIContent contentCopy in messageCopy.Contents) + { + AIContent sourceContent = Assert.Single(message.Contents, c => c.GetType() == contentCopy.GetType()); + AssertAIContentEquivalent(sourceContent, contentCopy); + } + } + + [Fact] + public void RoundTripChatMessageAsTable() + { + // Arrange + ChatMessage message = + new(ChatRole.User, + [ + new TextContent("Test message"), + new UriContent("https://example.com/image.jpg", "image/jpeg"), + new HostedFileContent("file_123abc"), + new DataContent(new byte[] { 1, 2, 3, 4, 5 }, "application/pdf"), + ]) + { + MessageId = "msg-001" + }; + + IEnumerable messages = [message]; + + // Act + TableValue result = messages.ToTable(); + TableDataValue resultValue = result.ToTable(); + ChatMessage[] messagesCopy = resultValue.ToChatMessages().ToArray(); + + // Assert + Assert.NotNull(messagesCopy); + ChatMessage messageCopy = Assert.Single(messagesCopy); + Assert.Equal(message.Role, messageCopy.Role); + Assert.Equal(message.MessageId, messageCopy.MessageId); + Assert.Equal(message.Contents.Count, messageCopy.Contents.Count); + foreach (AIContent contentCopy in messageCopy.Contents) + { + AIContent sourceContent = Assert.Single(message.Contents, c => c.GetType() == contentCopy.GetType()); + AssertAIContentEquivalent(sourceContent, contentCopy); + } + } + + /// + /// Compares two AIContent instances for equivalence without using Assert.Equivalent, + /// which fails on .NET Framework 4.7.2 due to ReadOnlySpan.GetHashCode() not being supported. + /// + private static void AssertAIContentEquivalent(AIContent expected, AIContent actual) + { + Assert.Equal(expected.GetType(), actual.GetType()); + + switch (expected) + { + case TextContent expectedText: + TextContent actualText = Assert.IsType(actual); + Assert.Equal(expectedText.Text, actualText.Text); + break; + case UriContent expectedUri: + UriContent actualUri = Assert.IsType(actual); + Assert.Equal(expectedUri.Uri, actualUri.Uri); + Assert.Equal(expectedUri.MediaType, actualUri.MediaType); + break; + case HostedFileContent expectedFile: + HostedFileContent actualFile = Assert.IsType(actual); + Assert.Equal(expectedFile.FileId, actualFile.FileId); + break; + case DataContent expectedData: + DataContent actualData = Assert.IsType(actual); + Assert.Equal(expectedData.MediaType, actualData.MediaType); + Assert.Equal(expectedData.Data.ToArray(), actualData.Data.ToArray()); + break; + default: + Assert.Fail($"Unexpected AIContent type: {expected.GetType().Name}"); + break; + } + } } From 0fcf075ea7b3cd7b99d698361a3db4864b285e0a Mon Sep 17 00:00:00 2001 From: Tao Chen Date: Fri, 30 Jan 2026 10:07:58 -0800 Subject: [PATCH 04/20] Python: Add sample on how to share a thread between agents in a workflow (#3405) * Add sample on how to share a thread between agents in a workflow * Fix sample * Fix formatting * Comments * comment --- .../agents/azure_ai/azure_ai_with_thread.py | 11 +- .../getting_started/workflows/README.md | 186 +++++++++--------- .../azure_ai_agents_with_shared_thread.py | 103 ++++++++++ 3 files changed, 206 insertions(+), 94 deletions(-) create mode 100644 python/samples/getting_started/workflows/agents/azure_ai_agents_with_shared_thread.py diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py index ff2fa72a52..2330f9a19d 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py @@ -17,7 +17,10 @@ persistent conversation capabilities using service-managed threads as well as st """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production +# See: +# samples/getting_started/tools/function_tool_with_approval.py +# samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -78,19 +81,19 @@ async def example_with_thread_persistence_in_memory() -> None: # First conversation query1 = "What's the weather like in Tokyo?" print(f"User: {query1}") - result1 = await agent.run(query1, thread=thread, store=False) + result1 = await agent.run(query1, thread=thread, options={"store": False}) print(f"Agent: {result1.text}") # Second conversation using the same thread - maintains context query2 = "How about London?" print(f"\nUser: {query2}") - result2 = await agent.run(query2, thread=thread, store=False) + result2 = await agent.run(query2, thread=thread, options={"store": False}) 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, thread=thread, store=False) + result3 = await agent.run(query3, thread=thread, options={"store": False}) print(f"Agent: {result3.text}") print("Note: The agent remembers context from previous messages in the same thread.\n") diff --git a/python/samples/getting_started/workflows/README.md b/python/samples/getting_started/workflows/README.md index 3f1a328b46..524f93fd61 100644 --- a/python/samples/getting_started/workflows/README.md +++ b/python/samples/getting_started/workflows/README.md @@ -18,11 +18,11 @@ To export visualization images you also need to [install GraphViz](https://graph Begin with the `_start-here` folder in order. These three samples introduce the core ideas of executors, edges, agents in workflows, and streaming. -| Sample | File | Concepts | -|--------|------|----------| -| Executors and Edges | [_start-here/step1_executors_and_edges.py](./_start-here/step1_executors_and_edges.py) | Minimal workflow with basic executors and edges | -| Agents in a Workflow | [_start-here/step2_agents_in_a_workflow.py](./_start-here/step2_agents_in_a_workflow.py) | Introduces adding Agents as nodes; calling agents inside a workflow | -| Streaming (Basics) | [_start-here/step3_streaming.py](./_start-here/step3_streaming.py) | Extends workflows with event streaming | +| Sample | File | Concepts | +| -------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| Executors and Edges | [\_start-here/step1_executors_and_edges.py](./_start-here/step1_executors_and_edges.py) | Minimal workflow with basic executors and edges | +| Agents in a Workflow | [\_start-here/step2_agents_in_a_workflow.py](./_start-here/step2_agents_in_a_workflow.py) | Introduces adding Agents as nodes; calling agents inside a workflow | +| Streaming (Basics) | [\_start-here/step3_streaming.py](./_start-here/step3_streaming.py) | Extends workflows with event streaming | Once comfortable with these, explore the rest of the samples below. @@ -32,101 +32,102 @@ Once comfortable with these, explore the rest of the samples below. ### agents -| Sample | File | Concepts | -|---|---|---| -| Azure Chat Agents (Streaming) | [agents/azure_chat_agents_streaming.py](./agents/azure_chat_agents_streaming.py) | Add Azure Chat agents as edges and handle streaming events | -| Azure AI Chat Agents (Streaming) | [agents/azure_ai_agents_streaming.py](./agents/azure_ai_agents_streaming.py) | Add Azure AI agents as edges and handle streaming events | -| Azure Chat Agents (Function Bridge) | [agents/azure_chat_agents_function_bridge.py](./agents/azure_chat_agents_function_bridge.py) | Chain two agents with a function executor that injects external context | -| Azure Chat Agents (Tools + HITL) | [agents/azure_chat_agents_tool_calls_with_feedback.py](./agents/azure_chat_agents_tool_calls_with_feedback.py) | Tool-enabled writer/editor pipeline with human feedback gating | -| Custom Agent Executors | [agents/custom_agent_executors.py](./agents/custom_agent_executors.py) | Create executors to handle agent run methods | -| Sequential Workflow as Agent | [agents/sequential_workflow_as_agent.py](./agents/sequential_workflow_as_agent.py) | Build a sequential workflow orchestrating agents, then expose it as a reusable agent | -| Concurrent Workflow as Agent | [agents/concurrent_workflow_as_agent.py](./agents/concurrent_workflow_as_agent.py) | Build a concurrent fan-out/fan-in workflow, then expose it as a reusable agent | -| Magentic Workflow as Agent | [agents/magentic_workflow_as_agent.py](./agents/magentic_workflow_as_agent.py) | Configure Magentic orchestration with callbacks, then expose the workflow as an agent | -| Workflow as Agent (Reflection Pattern) | [agents/workflow_as_agent_reflection_pattern.py](./agents/workflow_as_agent_reflection_pattern.py) | Wrap a workflow so it can behave like an agent (reflection pattern) | -| Workflow as Agent + HITL | [agents/workflow_as_agent_human_in_the_loop.py](./agents/workflow_as_agent_human_in_the_loop.py) | Extend workflow-as-agent with human-in-the-loop capability | -| Workflow as Agent with Thread | [agents/workflow_as_agent_with_thread.py](./agents/workflow_as_agent_with_thread.py) | Use AgentThread to maintain conversation history across workflow-as-agent invocations | -| Workflow as Agent kwargs | [agents/workflow_as_agent_kwargs.py](./agents/workflow_as_agent_kwargs.py) | Pass custom context (data, user tokens) via kwargs through workflow.as_agent() to @tool functions | -| Handoff Workflow as Agent | [agents/handoff_workflow_as_agent.py](./agents/handoff_workflow_as_agent.py) | Use a HandoffBuilder workflow as an agent with HITL via FunctionCallContent/FunctionResultContent | +| Sample | File | Concepts | +| -------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| Azure Chat Agents (Streaming) | [agents/azure_chat_agents_streaming.py](./agents/azure_chat_agents_streaming.py) | Add Azure Chat agents as edges and handle streaming events | +| Azure AI Agents (Streaming) | [agents/azure_ai_agents_streaming.py](./agents/azure_ai_agents_streaming.py) | Add Azure AI agents as edges and handle streaming events | +| Azure AI Agents (Shared Thread) | [agents/azure_ai_agents_with_shared_thread.py](./agents/azure_ai_agents_with_shared_thread.py) | Share a common message thread between multiple Azure AI agents in a workflow | +| Azure Chat Agents (Function Bridge) | [agents/azure_chat_agents_function_bridge.py](./agents/azure_chat_agents_function_bridge.py) | Chain two agents with a function executor that injects external context | +| Azure Chat Agents (Tools + HITL) | [agents/azure_chat_agents_tool_calls_with_feedback.py](./agents/azure_chat_agents_tool_calls_with_feedback.py) | Tool-enabled writer/editor pipeline with human feedback gating | +| Custom Agent Executors | [agents/custom_agent_executors.py](./agents/custom_agent_executors.py) | Create executors to handle agent run methods | +| Sequential Workflow as Agent | [agents/sequential_workflow_as_agent.py](./agents/sequential_workflow_as_agent.py) | Build a sequential workflow orchestrating agents, then expose it as a reusable agent | +| Concurrent Workflow as Agent | [agents/concurrent_workflow_as_agent.py](./agents/concurrent_workflow_as_agent.py) | Build a concurrent fan-out/fan-in workflow, then expose it as a reusable agent | +| Magentic Workflow as Agent | [agents/magentic_workflow_as_agent.py](./agents/magentic_workflow_as_agent.py) | Configure Magentic orchestration with callbacks, then expose the workflow as an agent | +| Workflow as Agent (Reflection Pattern) | [agents/workflow_as_agent_reflection_pattern.py](./agents/workflow_as_agent_reflection_pattern.py) | Wrap a workflow so it can behave like an agent (reflection pattern) | +| Workflow as Agent + HITL | [agents/workflow_as_agent_human_in_the_loop.py](./agents/workflow_as_agent_human_in_the_loop.py) | Extend workflow-as-agent with human-in-the-loop capability | +| Workflow as Agent with Thread | [agents/workflow_as_agent_with_thread.py](./agents/workflow_as_agent_with_thread.py) | Use AgentThread to maintain conversation history across workflow-as-agent invocations | +| Workflow as Agent kwargs | [agents/workflow_as_agent_kwargs.py](./agents/workflow_as_agent_kwargs.py) | Pass custom context (data, user tokens) via kwargs through workflow.as_agent() to @ai_function tools | +| Handoff Workflow as Agent | [agents/handoff_workflow_as_agent.py](./agents/handoff_workflow_as_agent.py) | Use a HandoffBuilder workflow as an agent with HITL via FunctionCallContent/FunctionResultContent | ### checkpoint -| Sample | File | Concepts | -|---|---|---| -| Checkpoint & Resume | [checkpoint/checkpoint_with_resume.py](./checkpoint/checkpoint_with_resume.py) | Create checkpoints, inspect them, and resume execution | -| Checkpoint & HITL Resume | [checkpoint/checkpoint_with_human_in_the_loop.py](./checkpoint/checkpoint_with_human_in_the_loop.py) | Combine checkpointing with human approvals and resume pending HITL requests | -| Checkpointed Sub-Workflow | [checkpoint/sub_workflow_checkpoint.py](./checkpoint/sub_workflow_checkpoint.py) | Save and resume a sub-workflow that pauses for human approval | +| Sample | File | Concepts | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| Checkpoint & Resume | [checkpoint/checkpoint_with_resume.py](./checkpoint/checkpoint_with_resume.py) | Create checkpoints, inspect them, and resume execution | +| Checkpoint & HITL Resume | [checkpoint/checkpoint_with_human_in_the_loop.py](./checkpoint/checkpoint_with_human_in_the_loop.py) | Combine checkpointing with human approvals and resume pending HITL requests | +| Checkpointed Sub-Workflow | [checkpoint/sub_workflow_checkpoint.py](./checkpoint/sub_workflow_checkpoint.py) | Save and resume a sub-workflow that pauses for human approval | | Handoff + Tool Approval Resume | [checkpoint/handoff_with_tool_approval_checkpoint_resume.py](./checkpoint/handoff_with_tool_approval_checkpoint_resume.py) | Handoff workflow that captures tool-call approvals in checkpoints and resumes with human decisions | -| Workflow as Agent Checkpoint | [checkpoint/workflow_as_agent_checkpoint.py](./checkpoint/workflow_as_agent_checkpoint.py) | Enable checkpointing when using workflow.as_agent() with checkpoint_storage parameter | +| Workflow as Agent Checkpoint | [checkpoint/workflow_as_agent_checkpoint.py](./checkpoint/workflow_as_agent_checkpoint.py) | Enable checkpointing when using workflow.as_agent() with checkpoint_storage parameter | ### composition -| Sample | File | Concepts | -|---|---|---| -| Sub-Workflow (Basics) | [composition/sub_workflow_basics.py](./composition/sub_workflow_basics.py) | Wrap a workflow as an executor and orchestrate sub-workflows | -| Sub-Workflow: Request Interception | [composition/sub_workflow_request_interception.py](./composition/sub_workflow_request_interception.py) | Intercept and forward sub-workflow requests using @handler for SubWorkflowRequestMessage | -| Sub-Workflow: Parallel Requests | [composition/sub_workflow_parallel_requests.py](./composition/sub_workflow_parallel_requests.py) | Multiple specialized interceptors handling different request types from same sub-workflow | -| Sub-Workflow: kwargs Propagation | [composition/sub_workflow_kwargs.py](./composition/sub_workflow_kwargs.py) | Pass custom context (user tokens, config) from parent workflow through to sub-workflow agents | +| Sample | File | Concepts | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | +| Sub-Workflow (Basics) | [composition/sub_workflow_basics.py](./composition/sub_workflow_basics.py) | Wrap a workflow as an executor and orchestrate sub-workflows | +| Sub-Workflow: Request Interception | [composition/sub_workflow_request_interception.py](./composition/sub_workflow_request_interception.py) | Intercept and forward sub-workflow requests using @handler for SubWorkflowRequestMessage | +| Sub-Workflow: Parallel Requests | [composition/sub_workflow_parallel_requests.py](./composition/sub_workflow_parallel_requests.py) | Multiple specialized interceptors handling different request types from same sub-workflow | +| Sub-Workflow: kwargs Propagation | [composition/sub_workflow_kwargs.py](./composition/sub_workflow_kwargs.py) | Pass custom context (user tokens, config) from parent workflow through to sub-workflow agents | ### control-flow -| Sample | File | Concepts | -|---|---|---| -| Sequential Executors | [control-flow/sequential_executors.py](./control-flow/sequential_executors.py) | Sequential workflow with explicit executor setup | -| Sequential (Streaming) | [control-flow/sequential_streaming.py](./control-flow/sequential_streaming.py) | Stream events from a simple sequential run | -| Edge Condition | [control-flow/edge_condition.py](./control-flow/edge_condition.py) | Conditional routing based on agent classification | -| Switch-Case Edge Group | [control-flow/switch_case_edge_group.py](./control-flow/switch_case_edge_group.py) | Switch-case branching using classifier outputs | +| Sample | File | Concepts | +| -------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------- | +| Sequential Executors | [control-flow/sequential_executors.py](./control-flow/sequential_executors.py) | Sequential workflow with explicit executor setup | +| Sequential (Streaming) | [control-flow/sequential_streaming.py](./control-flow/sequential_streaming.py) | Stream events from a simple sequential run | +| Edge Condition | [control-flow/edge_condition.py](./control-flow/edge_condition.py) | Conditional routing based on agent classification | +| Switch-Case Edge Group | [control-flow/switch_case_edge_group.py](./control-flow/switch_case_edge_group.py) | Switch-case branching using classifier outputs | | Multi-Selection Edge Group | [control-flow/multi_selection_edge_group.py](./control-flow/multi_selection_edge_group.py) | Select one or many targets dynamically (subset fan-out) | -| Simple Loop | [control-flow/simple_loop.py](./control-flow/simple_loop.py) | Feedback loop where an agent judges ABOVE/BELOW/MATCHED | -| Workflow Cancellation | [control-flow/workflow_cancellation.py](./control-flow/workflow_cancellation.py) | Cancel a running workflow using asyncio tasks | +| Simple Loop | [control-flow/simple_loop.py](./control-flow/simple_loop.py) | Feedback loop where an agent judges ABOVE/BELOW/MATCHED | +| Workflow Cancellation | [control-flow/workflow_cancellation.py](./control-flow/workflow_cancellation.py) | Cancel a running workflow using asyncio tasks | ### human-in-the-loop -| Sample | File | Concepts | -|---|---|---| -| Human-In-The-Loop (Guessing Game) | [human-in-the-loop/guessing_game_with_human_input.py](./human-in-the-loop/guessing_game_with_human_input.py) | Interactive request/response prompts with a human via `ctx.request_info()` | -| Agents with Approval Requests in Workflows | [human-in-the-loop/agents_with_approval_requests.py](./human-in-the-loop/agents_with_approval_requests.py) | Agents that create approval requests during workflow execution and wait for human approval to proceed | -| SequentialBuilder Request Info | [human-in-the-loop/sequential_request_info.py](./human-in-the-loop/sequential_request_info.py) | Request info for agent responses mid-workflow using `.with_request_info()` on SequentialBuilder | -| ConcurrentBuilder Request Info | [human-in-the-loop/concurrent_request_info.py](./human-in-the-loop/concurrent_request_info.py) | Review concurrent agent outputs before aggregation using `.with_request_info()` on ConcurrentBuilder | -| GroupChatBuilder Request Info | [human-in-the-loop/group_chat_request_info.py](./human-in-the-loop/group_chat_request_info.py) | Steer group discussions with periodic guidance using `.with_request_info()` on GroupChatBuilder | +| Sample | File | Concepts | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | +| Human-In-The-Loop (Guessing Game) | [human-in-the-loop/guessing_game_with_human_input.py](./human-in-the-loop/guessing_game_with_human_input.py) | Interactive request/response prompts with a human via `ctx.request_info()` | +| Agents with Approval Requests in Workflows | [human-in-the-loop/agents_with_approval_requests.py](./human-in-the-loop/agents_with_approval_requests.py) | Agents that create approval requests during workflow execution and wait for human approval to proceed | +| SequentialBuilder Request Info | [human-in-the-loop/sequential_request_info.py](./human-in-the-loop/sequential_request_info.py) | Request info for agent responses mid-workflow using `.with_request_info()` on SequentialBuilder | +| ConcurrentBuilder Request Info | [human-in-the-loop/concurrent_request_info.py](./human-in-the-loop/concurrent_request_info.py) | Review concurrent agent outputs before aggregation using `.with_request_info()` on ConcurrentBuilder | +| GroupChatBuilder Request Info | [human-in-the-loop/group_chat_request_info.py](./human-in-the-loop/group_chat_request_info.py) | Steer group discussions with periodic guidance using `.with_request_info()` on GroupChatBuilder | ### tool-approval Tool approval samples demonstrate using `@tool(approval_mode="always_require")` to gate sensitive tool executions with human approval. These work with the high-level builder APIs. -| Sample | File | Concepts | -|---|---|---| +| Sample | File | Concepts | +| ------------------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | SequentialBuilder Tool Approval | [tool-approval/sequential_builder_tool_approval.py](./tool-approval/sequential_builder_tool_approval.py) | Sequential workflow with tool approval gates for sensitive operations | -| ConcurrentBuilder Tool Approval | [tool-approval/concurrent_builder_tool_approval.py](./tool-approval/concurrent_builder_tool_approval.py) | Concurrent workflow with tool approvals across parallel agents | -| GroupChatBuilder Tool Approval | [tool-approval/group_chat_builder_tool_approval.py](./tool-approval/group_chat_builder_tool_approval.py) | Group chat workflow with tool approval for multi-agent collaboration | +| ConcurrentBuilder Tool Approval | [tool-approval/concurrent_builder_tool_approval.py](./tool-approval/concurrent_builder_tool_approval.py) | Concurrent workflow with tool approvals across parallel agents | +| GroupChatBuilder Tool Approval | [tool-approval/group_chat_builder_tool_approval.py](./tool-approval/group_chat_builder_tool_approval.py) | Group chat workflow with tool approval for multi-agent collaboration | ### observability -| Sample | File | Concepts | -|---|---|---| +| Sample | File | Concepts | +| ------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Executor I/O Observation | [observability/executor_io_observation.py](./observability/executor_io_observation.py) | Observe executor input/output data via ExecutorInvokedEvent and ExecutorCompletedEvent without modifying executor code | For additional observability samples in Agent Framework, see the [observability getting started samples](../observability/README.md). The [sample](../observability/workflow_observability.py) demonstrates integrating observability into workflows. ### orchestration -| Sample | File | Concepts | -|---|---|---| -| Concurrent Orchestration (Default Aggregator) | [orchestration/concurrent_agents.py](./orchestration/concurrent_agents.py) | Fan-out to multiple agents; fan-in with default aggregator returning combined ChatMessages | -| Concurrent Orchestration (Custom Aggregator) | [orchestration/concurrent_custom_aggregator.py](./orchestration/concurrent_custom_aggregator.py) | Override aggregator via callback; summarize results with an LLM | -| Concurrent Orchestration (Custom Agent Executors) | [orchestration/concurrent_custom_agent_executors.py](./orchestration/concurrent_custom_agent_executors.py) | Child executors own ChatAgents; concurrent fan-out/fan-in via ConcurrentBuilder | -| Concurrent Orchestration (Participant Factory) | [orchestration/concurrent_participant_factory.py](./orchestration/concurrent_participant_factory.py) | Use participant factories for state isolation between workflow instances | -| Group Chat with Agent Manager | [orchestration/group_chat_agent_manager.py](./orchestration/group_chat_agent_manager.py) | Agent-based manager using `with_orchestrator(agent=)` to select next speaker | -| Group Chat Philosophical Debate | [orchestration/group_chat_philosophical_debate.py](./orchestration/group_chat_philosophical_debate.py) | Agent manager moderates long-form, multi-round debate across diverse participants | -| Group Chat with Simple Function Selector | [orchestration/group_chat_simple_selector.py](./orchestration/group_chat_simple_selector.py) | Group chat with a simple function selector for next speaker | -| Handoff (Simple) | [orchestration/handoff_simple.py](./orchestration/handoff_simple.py) | Single-tier routing: triage agent routes to specialists, control returns to user after each specialist response | -| Handoff (Autonomous) | [orchestration/handoff_autonomous.py](./orchestration/handoff_autonomous.py) | Autonomous mode: specialists iterate independently until invoking a handoff tool using `.with_autonomous_mode()` | -| Handoff (Participant Factory) | [orchestration/handoff_participant_factory.py](./orchestration/handoff_participant_factory.py) | Use participant factories for state isolation between workflow instances | -| Magentic Workflow (Multi-Agent) | [orchestration/magentic.py](./orchestration/magentic.py) | Orchestrate multiple agents with Magentic manager and streaming | -| Magentic + Human Plan Review | [orchestration/magentic_human_plan_review.py](./orchestration/magentic_human_plan_review.py) | Human reviews/updates the plan before execution | -| Magentic + Checkpoint Resume | [orchestration/magentic_checkpoint.py](./orchestration/magentic_checkpoint.py) | Resume Magentic orchestration from saved checkpoints | -| Sequential Orchestration (Agents) | [orchestration/sequential_agents.py](./orchestration/sequential_agents.py) | Chain agents sequentially with shared conversation context | -| Sequential Orchestration (Custom Executor) | [orchestration/sequential_custom_executors.py](./orchestration/sequential_custom_executors.py) | Mix agents with a summarizer that appends a compact summary | -| Sequential Orchestration (Participant Factories) | [orchestration/sequential_participant_factory.py](./orchestration/sequential_participant_factory.py) | Use participant factories for state isolation between workflow instances | +| Sample | File | Concepts | +| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Concurrent Orchestration (Default Aggregator) | [orchestration/concurrent_agents.py](./orchestration/concurrent_agents.py) | Fan-out to multiple agents; fan-in with default aggregator returning combined ChatMessages | +| Concurrent Orchestration (Custom Aggregator) | [orchestration/concurrent_custom_aggregator.py](./orchestration/concurrent_custom_aggregator.py) | Override aggregator via callback; summarize results with an LLM | +| Concurrent Orchestration (Custom Agent Executors) | [orchestration/concurrent_custom_agent_executors.py](./orchestration/concurrent_custom_agent_executors.py) | Child executors own ChatAgents; concurrent fan-out/fan-in via ConcurrentBuilder | +| Concurrent Orchestration (Participant Factory) | [orchestration/concurrent_participant_factory.py](./orchestration/concurrent_participant_factory.py) | Use participant factories for state isolation between workflow instances | +| Group Chat with Agent Manager | [orchestration/group_chat_agent_manager.py](./orchestration/group_chat_agent_manager.py) | Agent-based manager using `with_orchestrator(agent=)` to select next speaker | +| Group Chat Philosophical Debate | [orchestration/group_chat_philosophical_debate.py](./orchestration/group_chat_philosophical_debate.py) | Agent manager moderates long-form, multi-round debate across diverse participants | +| Group Chat with Simple Function Selector | [orchestration/group_chat_simple_selector.py](./orchestration/group_chat_simple_selector.py) | Group chat with a simple function selector for next speaker | +| Handoff (Simple) | [orchestration/handoff_simple.py](./orchestration/handoff_simple.py) | Single-tier routing: triage agent routes to specialists, control returns to user after each specialist response | +| Handoff (Autonomous) | [orchestration/handoff_autonomous.py](./orchestration/handoff_autonomous.py) | Autonomous mode: specialists iterate independently until invoking a handoff tool using `.with_autonomous_mode()` | +| Handoff (Participant Factory) | [orchestration/handoff_participant_factory.py](./orchestration/handoff_participant_factory.py) | Use participant factories for state isolation between workflow instances | +| Magentic Workflow (Multi-Agent) | [orchestration/magentic.py](./orchestration/magentic.py) | Orchestrate multiple agents with Magentic manager and streaming | +| Magentic + Human Plan Review | [orchestration/magentic_human_plan_review.py](./orchestration/magentic_human_plan_review.py) | Human reviews/updates the plan before execution | +| Magentic + Checkpoint Resume | [orchestration/magentic_checkpoint.py](./orchestration/magentic_checkpoint.py) | Resume Magentic orchestration from saved checkpoints | +| Sequential Orchestration (Agents) | [orchestration/sequential_agents.py](./orchestration/sequential_agents.py) | Chain agents sequentially with shared conversation context | +| Sequential Orchestration (Custom Executor) | [orchestration/sequential_custom_executors.py](./orchestration/sequential_custom_executors.py) | Mix agents with a summarizer that appends a compact summary | +| Sequential Orchestration (Participant Factories) | [orchestration/sequential_participant_factory.py](./orchestration/sequential_participant_factory.py) | Use participant factories for state isolation between workflow instances | **Magentic checkpointing tip**: Treat `MagenticBuilder.participants` keys as stable identifiers. When resuming from a checkpoint, the rebuilt workflow must reuse the same participant names; otherwise the checkpoint cannot be applied and the run will fail fast. @@ -137,40 +138,45 @@ to configure which agents can route to which others with a fluent, type-safe API ### parallelism -| Sample | File | Concepts | -|---|---|---| -| Concurrent (Fan-out/Fan-in) | [parallelism/fan_out_fan_in_edges.py](./parallelism/fan_out_fan_in_edges.py) | Dispatch to multiple executors and aggregate results | +| Sample | File | Concepts | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | +| Concurrent (Fan-out/Fan-in) | [parallelism/fan_out_fan_in_edges.py](./parallelism/fan_out_fan_in_edges.py) | Dispatch to multiple executors and aggregate results | | Aggregate Results of Different Types | [parallelism/aggregate_results_of_different_types.py](./parallelism/aggregate_results_of_different_types.py) | Handle results of different types from multiple concurrent executors | -| Map-Reduce with Visualization | [parallelism/map_reduce_and_visualization.py](./parallelism/map_reduce_and_visualization.py) | Fan-out/fan-in pattern with diagram export | +| Map-Reduce with Visualization | [parallelism/map_reduce_and_visualization.py](./parallelism/map_reduce_and_visualization.py) | Fan-out/fan-in pattern with diagram export | ### state-management +| Sample | File | Concepts | +| -------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | +| Shared States | [state-management/shared_states_with_agents.py](./state-management/shared_states_with_agents.py) | Store in shared state once and later reuse across agents | +| Workflow Kwargs (Custom Context) | [state-management/workflow_kwargs.py](./state-management/workflow_kwargs.py) | Pass custom context (data, user tokens) via kwargs to `@ai_function` tools | + +======= | Sample | File | Concepts | |---|---|---| | Shared States | [state-management/shared_states_with_agents.py](./state-management/shared_states_with_agents.py) | Store in shared state once and later reuse across agents | | Workflow Kwargs (Custom Context) | [state-management/workflow_kwargs.py](./state-management/workflow_kwargs.py) | Pass custom context (data, user tokens) via kwargs to `@tool` tools | - ### visualization -| Sample | File | Concepts | -|---|---|---| +| Sample | File | Concepts | +| ----------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------- | | Concurrent with Visualization | [visualization/concurrent_with_visualization.py](./visualization/concurrent_with_visualization.py) | Fan-out/fan-in workflow with diagram export | ### declarative YAML-based declarative workflows allow you to define multi-agent orchestration patterns without writing Python code. See the [declarative workflows README](./declarative/README.md) for more details on YAML workflow syntax and available actions. -| Sample | File | Concepts | -|---|---|---| -| Conditional Workflow | [declarative/conditional_workflow/](./declarative/conditional_workflow/) | Nested conditional branching based on user input | -| Customer Support | [declarative/customer_support/](./declarative/customer_support/) | Multi-agent customer support with routing | -| Deep Research | [declarative/deep_research/](./declarative/deep_research/) | Research workflow with planning, searching, and synthesis | -| Function Tools | [declarative/function_tools/](./declarative/function_tools/) | Invoking Python functions from declarative workflows | -| Human-in-Loop | [declarative/human_in_loop/](./declarative/human_in_loop/) | Interactive workflows that request user input | -| Marketing | [declarative/marketing/](./declarative/marketing/) | Marketing content generation workflow | -| Simple Workflow | [declarative/simple_workflow/](./declarative/simple_workflow/) | Basic workflow with variable setting, conditionals, and loops | -| Student Teacher | [declarative/student_teacher/](./declarative/student_teacher/) | Student-teacher interaction pattern | +| Sample | File | Concepts | +| -------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------- | +| Conditional Workflow | [declarative/conditional_workflow/](./declarative/conditional_workflow/) | Nested conditional branching based on user input | +| Customer Support | [declarative/customer_support/](./declarative/customer_support/) | Multi-agent customer support with routing | +| Deep Research | [declarative/deep_research/](./declarative/deep_research/) | Research workflow with planning, searching, and synthesis | +| Function Tools | [declarative/function_tools/](./declarative/function_tools/) | Invoking Python functions from declarative workflows | +| Human-in-Loop | [declarative/human_in_loop/](./declarative/human_in_loop/) | Interactive workflows that request user input | +| Marketing | [declarative/marketing/](./declarative/marketing/) | Marketing content generation workflow | +| Simple Workflow | [declarative/simple_workflow/](./declarative/simple_workflow/) | Basic workflow with variable setting, conditionals, and loops | +| Student Teacher | [declarative/student_teacher/](./declarative/student_teacher/) | Student-teacher interaction pattern | ### resources @@ -189,8 +195,8 @@ Sequential orchestration uses a few small adapter nodes for plumbing: - "input-conversation" normalizes input to `list[ChatMessage]` - "to-conversation:" converts agent responses into the shared conversation - "complete" publishes the final `WorkflowOutputEvent` -These may appear in event streams (ExecutorInvoke/Completed). They’re analogous to -concurrent’s dispatcher and aggregator and can be ignored if you only care about agent activity. + These may appear in event streams (ExecutorInvoke/Completed). They’re analogous to + concurrent’s dispatcher and aggregator and can be ignored if you only care about agent activity. ### Environment Variables diff --git a/python/samples/getting_started/workflows/agents/azure_ai_agents_with_shared_thread.py b/python/samples/getting_started/workflows/agents/azure_ai_agents_with_shared_thread.py new file mode 100644 index 0000000000..874cb2956a --- /dev/null +++ b/python/samples/getting_started/workflows/agents/azure_ai_agents_with_shared_thread.py @@ -0,0 +1,103 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import ( + AgentExecutorRequest, + AgentExecutorResponse, + ChatMessageStore, + WorkflowBuilder, + WorkflowContext, + WorkflowRunState, + executor, +) +from agent_framework.azure import AzureAIProjectAgentProvider +from azure.identity.aio import AzureCliCredential + +""" +Sample: Agents with a shared thread in a workflow + +A Writer agent generates content, then a Reviewer agent critiques it, sharing a common message thread. + +Purpose: +Show how to use a shared thread between multiple agents in a workflow. +By default, agents have individual threads, but sharing a thread allows them to share all messages. + +Notes: +- Not all agents can share threads; usually only the same type of agents can share threads. + +Demonstrate: +- Creating multiple agents with Azure AI Agent Service (V2 API). +- Setting up a shared thread between agents. + +Prerequisites: +- 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 agents, workflows, and executors in the agent framework. +""" + + +@executor(id="intercept_agent_response") +async def intercept_agent_response( + agent_response: AgentExecutorResponse, ctx: WorkflowContext[AgentExecutorRequest] +) -> None: + """This executor intercepts the agent response and sends a request without messages. + + This essentially prevents duplication of messages in the shared thread. Without this + executor, the response will be added to the thread as input of the next agent call. + """ + await ctx.send_message(AgentExecutorRequest(messages=[])) + + +async def main() -> None: + async with ( + AzureCliCredential() as credential, + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + writer = await provider.create_agent( + instructions=( + "You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt." + ), + name="writer", + ) + + reviewer = await provider.create_agent( + instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), + name="reviewer", + ) + + shared_thread = writer.get_new_thread() + # Set the message store to store messages in memory. + shared_thread.message_store = ChatMessageStore() + + workflow = ( + WorkflowBuilder() + .register_agent(factory_func=lambda: writer, name="writer", agent_thread=shared_thread) + .register_agent(factory_func=lambda: reviewer, name="reviewer", agent_thread=shared_thread) + .register_executor( + factory_func=lambda: intercept_agent_response, + name="intercept_agent_response", + ) + .add_chain(["writer", "intercept_agent_response", "reviewer"]) + .set_start_executor("writer") + .build() + ) + + result = await workflow.run( + "Write a tagline for a budget-friendly eBike.", + # Keyword arguments will be passed to each agent call. + # Setting store=False to avoid storing messages in the service for this example. + options={"store": False}, + ) + # The final state should be IDLE since the workflow no longer has messages to + # process after the reviewer agent responds. + assert result.get_final_state() == WorkflowRunState.IDLE + + # The shared thread now contains the conversation between the writer and reviewer. Print it out. + print("=== Shared Thread Conversation ===") + for message in shared_thread.message_store.messages: + print(f"{message.author_name or message.role}: {message.text}") + + +if __name__ == "__main__": + asyncio.run(main()) From 8b475afe170ffc36f64f27c249a0d55769ae4a75 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:11:41 -0800 Subject: [PATCH 05/20] Python: Add BaseAgent implementation for Claude Agent SDK (#3509) * Added ClaudeAgent implementation * Updated streaming logic * Small updates * Small update * Fixes * Small fix * Naming improvements * Updated imports * Addressed comments * Updated package versions --- python/CHANGELOG.md | 18 +- python/packages/a2a/pyproject.toml | 2 +- python/packages/ag-ui/pyproject.toml | 2 +- python/packages/anthropic/pyproject.toml | 2 +- .../packages/azure-ai-search/pyproject.toml | 2 +- python/packages/azure-ai/pyproject.toml | 2 +- python/packages/azurefunctions/pyproject.toml | 2 +- python/packages/bedrock/pyproject.toml | 2 +- python/packages/chatkit/pyproject.toml | 2 +- python/packages/claude/LICENSE | 21 + python/packages/claude/README.md | 11 + .../claude/agent_framework_claude/__init__.py | 18 + .../claude/agent_framework_claude/_agent.py | 645 ++++++++++++++++ .../agent_framework_claude/_settings.py | 51 ++ python/packages/claude/pyproject.toml | 89 +++ python/packages/claude/tests/__init__.py | 1 + .../claude/tests/test_claude_agent.py | 714 ++++++++++++++++++ python/packages/copilotstudio/pyproject.toml | 2 +- python/packages/core/pyproject.toml | 2 +- python/packages/declarative/pyproject.toml | 2 +- python/packages/devui/pyproject.toml | 2 +- python/packages/durabletask/pyproject.toml | 2 +- python/packages/foundry_local/pyproject.toml | 2 +- python/packages/github_copilot/pyproject.toml | 2 +- python/packages/lab/pyproject.toml | 2 +- python/packages/mem0/pyproject.toml | 2 +- python/packages/ollama/pyproject.toml | 2 +- python/packages/purview/pyproject.toml | 2 +- python/packages/redis/pyproject.toml | 2 +- python/pyproject.toml | 5 +- .../agents/anthropic/README.md | 28 +- .../anthropic/anthropic_claude_basic.py | 76 ++ .../anthropic/anthropic_claude_with_mcp.py | 81 ++ ...hropic_claude_with_multiple_permissions.py | 69 ++ .../anthropic_claude_with_session.py | 145 ++++ .../anthropic/anthropic_claude_with_shell.py | 57 ++ .../anthropic/anthropic_claude_with_tools.py | 40 + .../anthropic/anthropic_claude_with_url.py | 38 + python/uv.lock | 85 ++- 39 files changed, 2180 insertions(+), 52 deletions(-) create mode 100644 python/packages/claude/LICENSE create mode 100644 python/packages/claude/README.md create mode 100644 python/packages/claude/agent_framework_claude/__init__.py create mode 100644 python/packages/claude/agent_framework_claude/_agent.py create mode 100644 python/packages/claude/agent_framework_claude/_settings.py create mode 100644 python/packages/claude/pyproject.toml create mode 100644 python/packages/claude/tests/__init__.py create mode 100644 python/packages/claude/tests/test_claude_agent.py create mode 100644 python/samples/getting_started/agents/anthropic/anthropic_claude_basic.py create mode 100644 python/samples/getting_started/agents/anthropic/anthropic_claude_with_mcp.py create mode 100644 python/samples/getting_started/agents/anthropic/anthropic_claude_with_multiple_permissions.py create mode 100644 python/samples/getting_started/agents/anthropic/anthropic_claude_with_session.py create mode 100644 python/samples/getting_started/agents/anthropic/anthropic_claude_with_shell.py create mode 100644 python/samples/getting_started/agents/anthropic/anthropic_claude_with_tools.py create mode 100644 python/samples/getting_started/agents/anthropic/anthropic_claude_with_url.py diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 07126c38f4..af64583090 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0b260130] - 2026-01-30 + +### Added + +- **agent-framework-claude**: Add BaseAgent implementation for Claude Agent SDK ([#3509](https://github.com/microsoft/agent-framework/pull/3509)) +- **agent-framework-core**: Add core types and agents unit tests ([#3470](https://github.com/microsoft/agent-framework/pull/3470)) +- **agent-framework-core**: Add core utilities unit tests ([#3487](https://github.com/microsoft/agent-framework/pull/3487)) +- **agent-framework-core**: Add observability unit tests to improve coverage ([#3469](https://github.com/microsoft/agent-framework/pull/3469)) +- **agent-framework-azure-ai**: Improved AzureAI package test coverage ([#3452](https://github.com/microsoft/agent-framework/pull/3452)) + +### Changed + +- **agent-framework-core**: Added generic types to `ChatOptions` and `ChatResponse`/`AgentResponse` for Response Format ([#3305](https://github.com/microsoft/agent-framework/pull/3305)) +- **agent-framework-durabletask**: Update durabletask package ([#3492](https://github.com/microsoft/agent-framework/pull/3492)) + ## [1.0.0b260128] - 2026-01-28 ### Changed @@ -556,7 +571,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/). -[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260128...HEAD +[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260130...HEAD +[1.0.0b260130]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260128...python-1.0.0b260130 [1.0.0b260128]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260127...python-1.0.0b260128 [1.0.0b260127]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260123...python-1.0.0b260127 [1.0.0b260123]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260116...python-1.0.0b260123 diff --git a/python/packages/a2a/pyproject.toml b/python/packages/a2a/pyproject.toml index 07f0c05895..d4de542199 100644 --- a/python/packages/a2a/pyproject.toml +++ b/python/packages/a2a/pyproject.toml @@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/ag-ui/pyproject.toml b/python/packages/ag-ui/pyproject.toml index bf6643d565..627a71279c 100644 --- a/python/packages/ag-ui/pyproject.toml +++ b/python/packages/ag-ui/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-framework-ag-ui" -version = "1.0.0b260128" +version = "1.0.0b260130" description = "AG-UI protocol integration for Agent Framework" readme = "README.md" license-files = ["LICENSE"] diff --git a/python/packages/anthropic/pyproject.toml b/python/packages/anthropic/pyproject.toml index 0ce9371792..8935476ed5 100644 --- a/python/packages/anthropic/pyproject.toml +++ b/python/packages/anthropic/pyproject.toml @@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/azure-ai-search/pyproject.toml b/python/packages/azure-ai-search/pyproject.toml index df7d4b07b9..fb4763dfd8 100644 --- a/python/packages/azure-ai-search/pyproject.toml +++ b/python/packages/azure-ai-search/pyproject.toml @@ -4,7 +4,7 @@ description = "Azure AI Search integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/azure-ai/pyproject.toml b/python/packages/azure-ai/pyproject.toml index 8c762a1857..bf8e969519 100644 --- a/python/packages/azure-ai/pyproject.toml +++ b/python/packages/azure-ai/pyproject.toml @@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/azurefunctions/pyproject.toml b/python/packages/azurefunctions/pyproject.toml index 6000408247..be650a7516 100644 --- a/python/packages/azurefunctions/pyproject.toml +++ b/python/packages/azurefunctions/pyproject.toml @@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/bedrock/pyproject.toml b/python/packages/bedrock/pyproject.toml index 2da38343ba..aa864223a9 100644 --- a/python/packages/bedrock/pyproject.toml +++ b/python/packages/bedrock/pyproject.toml @@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/chatkit/pyproject.toml b/python/packages/chatkit/pyproject.toml index c1c5ca4661..632bf5aa61 100644 --- a/python/packages/chatkit/pyproject.toml +++ b/python/packages/chatkit/pyproject.toml @@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/claude/LICENSE b/python/packages/claude/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/claude/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/claude/README.md b/python/packages/claude/README.md new file mode 100644 index 0000000000..a89f99920a --- /dev/null +++ b/python/packages/claude/README.md @@ -0,0 +1,11 @@ +# Get Started with Microsoft Agent Framework Claude + +Please install this package via pip: + +```bash +pip install agent-framework-claude --pre +``` + +## Claude Agent + +The Claude agent enables integration with Claude Agent SDK, allowing you to interact with Claude's agentic capabilities through the Agent Framework. diff --git a/python/packages/claude/agent_framework_claude/__init__.py b/python/packages/claude/agent_framework_claude/__init__.py new file mode 100644 index 0000000000..18f30bf25e --- /dev/null +++ b/python/packages/claude/agent_framework_claude/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft. All rights reserved. + +import importlib.metadata + +from ._agent import ClaudeAgent, ClaudeAgentOptions +from ._settings import ClaudeAgentSettings + +try: + __version__ = importlib.metadata.version(__name__) +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" # Fallback for development mode + +__all__ = [ + "ClaudeAgent", + "ClaudeAgentOptions", + "ClaudeAgentSettings", + "__version__", +] diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py new file mode 100644 index 0000000000..8335d2f149 --- /dev/null +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -0,0 +1,645 @@ +# Copyright (c) Microsoft. All rights reserved. + +import contextlib +import sys +from collections.abc import AsyncIterable, Callable, MutableMapping, Sequence +from pathlib import Path +from typing import TYPE_CHECKING, Any, ClassVar, Generic + +from agent_framework import ( + AgentMiddlewareTypes, + AgentResponse, + AgentResponseUpdate, + AgentThread, + BaseAgent, + ChatMessage, + Content, + ContextProvider, + FunctionTool, + Role, + ToolProtocol, + get_logger, + normalize_messages, +) +from agent_framework._types import normalize_tools +from agent_framework.exceptions import ServiceException, ServiceInitializationError +from claude_agent_sdk import ( + ClaudeAgentOptions as SDKOptions, +) +from claude_agent_sdk import ( + ClaudeSDKClient, + ResultMessage, + SdkMcpTool, + create_sdk_mcp_server, +) +from claude_agent_sdk.types import StreamEvent +from pydantic import ValidationError + +from ._settings import ClaudeAgentSettings + +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 # pragma: no cover +else: + from typing_extensions import TypedDict # pragma: no cover + +if TYPE_CHECKING: + from claude_agent_sdk import ( + AgentDefinition, + CanUseTool, + HookMatcher, + McpServerConfig, + PermissionMode, + SandboxSettings, + SdkBeta, + ) + +__all__ = ["ClaudeAgent", "ClaudeAgentOptions"] + +logger = get_logger("agent_framework.claude") + +# Name of the in-process MCP server that hosts Agent Framework tools. +# FunctionTool instances are converted to SDK MCP tools and served +# through this server, as Claude Code CLI only supports tools via MCP. +TOOLS_MCP_SERVER_NAME = "_agent_framework_tools" + + +class ClaudeAgentOptions(TypedDict, total=False): + """Claude Agent-specific options.""" + + system_prompt: str + """System prompt for the agent.""" + + cli_path: str | Path + """Path to Claude CLI executable. Default: auto-detected.""" + + cwd: str | Path + """Working directory for Claude CLI. Default: current working directory.""" + + env: dict[str, str] + """Environment variables to pass to CLI.""" + + settings: str + """Path to Claude settings file.""" + + model: str + """Model to use ("sonnet", "opus", "haiku"). Default: "sonnet".""" + + fallback_model: str + """Fallback model if primary fails.""" + + max_thinking_tokens: int + """Maximum tokens for thinking blocks.""" + + allowed_tools: list[str] + """Allowlist of tools. If set, Claude can ONLY use tools in this list.""" + + disallowed_tools: list[str] + """Blocklist of tools. Claude cannot use these tools.""" + + mcp_servers: dict[str, "McpServerConfig"] + """MCP server configurations for external tools.""" + + permission_mode: "PermissionMode" + """Permission handling mode ("default", "acceptEdits", "plan", "bypassPermissions").""" + + can_use_tool: "CanUseTool" + """Permission callback for tool use.""" + + max_turns: int + """Maximum conversation turns.""" + + max_budget_usd: float + """Budget limit in USD.""" + + hooks: dict[str, list["HookMatcher"]] + """Pre/post tool hooks.""" + + add_dirs: list[str | Path] + """Additional directories to add to context.""" + + sandbox: "SandboxSettings" + """Sandbox configuration for bash isolation.""" + + agents: dict[str, "AgentDefinition"] + """Custom agent definitions.""" + + output_format: dict[str, Any] + """Structured output format (JSON schema).""" + + enable_file_checkpointing: bool + """Enable file checkpointing for rewind.""" + + betas: list["SdkBeta"] + """Beta features to enable.""" + + +TOptions = TypeVar( + "TOptions", + bound=TypedDict, # type: ignore[valid-type] + default="ClaudeAgentOptions", + covariant=True, +) + + +class ClaudeAgent(BaseAgent, Generic[TOptions]): + """Claude Agent using Claude Code CLI. + + Wraps the Claude Agent SDK to provide agentic capabilities including + tool use, session management, and streaming responses. + + This agent communicates with Claude through the Claude Code CLI, + enabling access to Claude's full agentic capabilities like file + editing, code execution, and tool use. + + The agent can be used as an async context manager to ensure proper cleanup: + + Examples: + Basic usage with context manager: + + .. code-block:: python + + from agent_framework_claude import ClaudeAgent + + async with ClaudeAgent( + instructions="You are a helpful assistant.", + ) as agent: + response = await agent.run("Hello!") + print(response.text) + + With streaming: + + .. code-block:: python + + async with ClaudeAgent() as agent: + async for update in agent.run_stream("Write a poem"): + print(update.text, end="", flush=True) + + With session management: + + .. code-block:: python + + async with ClaudeAgent() as agent: + thread = agent.get_new_thread() + await agent.run("Remember my name is Alice", thread=thread) + response = await agent.run("What's my name?", thread=thread) + # Claude will remember "Alice" from the same session + + With Agent Framework tools: + + .. code-block:: python + + from agent_framework import tool + + @tool + def greet(name: str) -> str: + \"\"\"Greet someone by name.\"\"\" + return f"Hello, {name}!" + + async with ClaudeAgent(tools=[greet]) as agent: + response = await agent.run("Greet Alice") + """ + + AGENT_PROVIDER_NAME: ClassVar[str] = "anthropic.claude" + + def __init__( + self, + instructions: str | None = None, + *, + client: ClaudeSDKClient | None = None, + id: str | None = None, + name: str | None = None, + description: str | None = None, + context_provider: ContextProvider | None = None, + middleware: Sequence[AgentMiddlewareTypes] | None = None, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | str + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str] + | None = None, + default_options: TOptions | MutableMapping[str, Any] | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize a ClaudeAgent instance. + + Args: + instructions: System prompt for the agent. + + Keyword Args: + client: Optional pre-configured ClaudeSDKClient instance. If not provided, + a new client will be created using the other parameters. + id: Unique identifier for the agent. + name: Name of the agent. + description: Description of the agent. + context_provider: Context provider for the agent. + middleware: List of middleware. + tools: Tools for the agent. Can be: + - Strings for built-in tools (e.g., "Read", "Write", "Bash", "Glob") + - Functions or ToolProtocol instances for custom tools + default_options: Default ClaudeAgentOptions including system_prompt, model, etc. + env_file_path: Path to .env file. + env_file_encoding: Encoding of .env file. + """ + super().__init__( + id=id, + name=name, + description=description, + context_provider=context_provider, + middleware=middleware, + ) + + self._client = client + self._owns_client = client is None + + # Parse options + opts: dict[str, Any] = dict(default_options) if default_options else {} + + # Handle instructions parameter - set as system_prompt in options + if instructions is not None: + opts["system_prompt"] = instructions + + cli_path = opts.pop("cli_path", None) + model = opts.pop("model", None) + cwd = opts.pop("cwd", None) + permission_mode = opts.pop("permission_mode", None) + max_turns = opts.pop("max_turns", None) + max_budget_usd = opts.pop("max_budget_usd", None) + self._mcp_servers: dict[str, Any] = opts.pop("mcp_servers", None) or {} + + # Load settings from environment and options + try: + self._settings = ClaudeAgentSettings( + cli_path=cli_path, + model=model, + cwd=cwd, + permission_mode=permission_mode, + max_turns=max_turns, + max_budget_usd=max_budget_usd, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Claude Agent settings.", ex) from ex + + # Separate built-in tools (strings) from custom tools (callables/ToolProtocol) + self._builtin_tools: list[str] = [] + self._custom_tools: list[ToolProtocol | MutableMapping[str, Any]] = [] + self._normalize_tools(tools) + + self._default_options = opts + self._started = False + self._current_session_id: str | None = None + + def _normalize_tools( + self, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | str + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str] + | None, + ) -> None: + """Separate built-in tools (strings) from custom tools. + + Args: + tools: Mixed list of tool names and custom tools. + """ + if tools is None: + return + + # Normalize to sequence + if isinstance(tools, str): + tools_list: Sequence[Any] = [tools] + elif isinstance(tools, (ToolProtocol, MutableMapping)) or callable(tools): + tools_list = [tools] + else: + tools_list = list(tools) + + for tool in tools_list: + if isinstance(tool, str): + self._builtin_tools.append(tool) + else: + # Use normalize_tools for custom tools + normalized = normalize_tools(tool) + self._custom_tools.extend(normalized) + + async def __aenter__(self) -> "ClaudeAgent[TOptions]": + """Start the agent when entering async context.""" + await self.start() + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Stop the agent when exiting async context.""" + await self.stop() + + async def start(self) -> None: + """Start the Claude SDK client. + + This method initializes the Claude SDK client and establishes a connection + to the Claude Code CLI. It is called automatically when using the agent + as an async context manager. + + Raises: + ServiceException: If the client fails to start. + """ + await self._ensure_session() + + async def stop(self) -> None: + """Stop the Claude SDK client and clean up resources. + + Stops the client if owned by this agent. Called automatically when + using the agent as an async context manager. + """ + if self._client and self._owns_client: + with contextlib.suppress(Exception): + await self._client.disconnect() + + self._started = False + self._current_session_id = None + + async def _ensure_session(self, session_id: str | None = None) -> None: + """Ensure the client is connected for the specified session. + + If the requested session differs from the current one, recreates the client. + + Args: + session_id: The session ID to use, or None for a new session. + """ + needs_new_client = ( + not self._started or self._client is None or (session_id and session_id != self._current_session_id) + ) + + if needs_new_client: + # Stop existing client if any + if self._client and self._owns_client: + with contextlib.suppress(Exception): + await self._client.disconnect() + self._started = False + + # Create new client with resume option if needed + opts = self._prepare_client_options(resume_session_id=session_id) + self._client = ClaudeSDKClient(options=opts) + self._owns_client = True + + try: + await self._client.connect() + self._started = True + self._current_session_id = session_id + except Exception as ex: + self._client = None + raise ServiceException(f"Failed to start Claude SDK client: {ex}") from ex + + def _prepare_client_options(self, resume_session_id: str | None = None) -> SDKOptions: + """Prepare SDK options for client initialization. + + Args: + resume_session_id: Optional session ID to resume. + + Returns: + SDKOptions instance configured for the client. + """ + opts: dict[str, Any] = {} + + # Set resume option if provided + if resume_session_id: + opts["resume"] = resume_session_id + + # Apply settings from environment + if self._settings.cli_path: + opts["cli_path"] = self._settings.cli_path + if self._settings.model: + opts["model"] = self._settings.model + if self._settings.cwd: + opts["cwd"] = self._settings.cwd + if self._settings.permission_mode: + opts["permission_mode"] = self._settings.permission_mode + if self._settings.max_turns: + opts["max_turns"] = self._settings.max_turns + if self._settings.max_budget_usd: + opts["max_budget_usd"] = self._settings.max_budget_usd + + # Apply default options + for key, value in self._default_options.items(): + if value is not None: + opts[key] = value + + # Add built-in tools (strings like "Read", "Write", "Bash") + if self._builtin_tools: + opts["tools"] = self._builtin_tools + + # Prepare custom tools (FunctionTool instances) + custom_tools_server, custom_tool_names = ( + self._prepare_tools(self._custom_tools) if self._custom_tools else (None, []) + ) + + # MCP servers - merge user-provided servers with custom tools server + mcp_servers = dict(self._mcp_servers) if self._mcp_servers else {} + if custom_tools_server: + mcp_servers[TOOLS_MCP_SERVER_NAME] = custom_tools_server + if mcp_servers: + opts["mcp_servers"] = mcp_servers + + # Add custom tools to allowed_tools so they can be executed + if custom_tool_names: + existing_allowed = opts.get("allowed_tools", []) + opts["allowed_tools"] = list(existing_allowed) + custom_tool_names + + # Always enable partial messages for streaming support + opts["include_partial_messages"] = True + + return SDKOptions(**opts) + + def _prepare_tools( + self, + tools: list[ToolProtocol | MutableMapping[str, Any]], + ) -> tuple[Any, list[str]]: + """Convert Agent Framework tools to SDK MCP server. + + Args: + tools: List of Agent Framework tools. + + Returns: + Tuple of (MCP server config, list of allowed tool names). + """ + sdk_tools: list[SdkMcpTool[Any]] = [] + tool_names: list[str] = [] + + for tool in tools: + if isinstance(tool, FunctionTool): + sdk_tools.append(self._function_tool_to_sdk_mcp_tool(tool)) + # Claude Agent SDK convention: MCP tools use format "mcp__{server}__{tool}" + tool_names.append(f"mcp__{TOOLS_MCP_SERVER_NAME}__{tool.name}") + elif isinstance(tool, ToolProtocol): + logger.debug(f"Unsupported tool type: {type(tool)}") + + if not sdk_tools: + return None, [] + + return create_sdk_mcp_server(name=TOOLS_MCP_SERVER_NAME, tools=sdk_tools), tool_names + + def _function_tool_to_sdk_mcp_tool(self, func_tool: FunctionTool[Any, Any]) -> SdkMcpTool[Any]: + """Convert a FunctionTool to an SDK MCP tool. + + Args: + func_tool: The FunctionTool to convert. + + Returns: + An SdkMcpTool instance. + """ + + async def handler(args: dict[str, Any]) -> dict[str, Any]: + """Handler that invokes the FunctionTool.""" + try: + if func_tool.input_model: + args_instance = func_tool.input_model(**args) + result = await func_tool.invoke(arguments=args_instance) + else: + result = await func_tool.invoke(arguments=args) + return {"content": [{"type": "text", "text": str(result)}]} + except Exception as e: + return {"content": [{"type": "text", "text": f"Error: {e}"}]} + + # Get JSON schema from pydantic model + schema: dict[str, Any] = func_tool.input_model.model_json_schema() if func_tool.input_model else {} + input_schema: dict[str, Any] = { + "type": "object", + "properties": schema.get("properties", {}), + "required": schema.get("required", []), + } + + return SdkMcpTool( + name=func_tool.name, + description=func_tool.description, + input_schema=input_schema, + handler=handler, + ) + + async def _apply_runtime_options(self, options: dict[str, Any] | None) -> None: + """Apply runtime options that can be changed dynamically. + + The Claude SDK supports changing model and permission_mode after connection. + + Args: + options: Runtime options to apply. + """ + if not options or not self._client: + return + + if "model" in options: + await self._client.set_model(options["model"]) + + if "permission_mode" in options: + await self._client.set_permission_mode(options["permission_mode"]) + + def _format_prompt(self, messages: list[ChatMessage] | None) -> str: + """Format messages into a prompt string. + + Args: + messages: List of chat messages. + + Returns: + Formatted prompt string. + """ + if not messages: + return "" + return "\n".join([msg.text or "" for msg in messages]) + + async def run( + self, + messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + options: TOptions | MutableMapping[str, Any] | None = None, + **kwargs: Any, + ) -> AgentResponse[Any]: + """Run the agent with the given messages. + + Args: + messages: The messages to process. + + Keyword Args: + thread: The conversation thread. If thread has service_thread_id set, + the agent will resume that session. + options: Runtime options (model, permission_mode can be changed per-request). + kwargs: Additional keyword arguments. + + Returns: + AgentResponse with the agent's response. + """ + thread = thread or self.get_new_thread() + return await AgentResponse.from_agent_response_generator( + self.run_stream(messages, thread=thread, options=options, **kwargs) + ) + + async def run_stream( + self, + messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + options: TOptions | MutableMapping[str, Any] | None = None, + **kwargs: Any, + ) -> AsyncIterable[AgentResponseUpdate]: + """Stream the agent's response. + + Args: + messages: The messages to process. + + Keyword Args: + thread: The conversation thread. If thread has service_thread_id set, + the agent will resume that session. + options: Runtime options (model, permission_mode can be changed per-request). + kwargs: Additional keyword arguments. + + Yields: + AgentResponseUpdate objects containing chunks of the response. + """ + thread = thread or self.get_new_thread() + + # Ensure we're connected to the right session + await self._ensure_session(thread.service_thread_id) + + if not self._client: + raise ServiceException("Claude SDK client not initialized.") + + prompt = self._format_prompt(normalize_messages(messages)) + + # Apply runtime options (model, permission_mode) + await self._apply_runtime_options(dict(options) if options else None) + + session_id: str | None = None + + await self._client.query(prompt) + async for message in self._client.receive_response(): + if isinstance(message, StreamEvent): + # Handle streaming events - extract text/thinking deltas + event = message.event + if event.get("type") == "content_block_delta": + delta = event.get("delta", {}) + delta_type = delta.get("type") + if delta_type == "text_delta": + text = delta.get("text", "") + if text: + yield AgentResponseUpdate( + role=Role.ASSISTANT, + contents=[Content.from_text(text=text, raw_representation=message)], + raw_representation=message, + ) + elif delta_type == "thinking_delta": + thinking = delta.get("thinking", "") + if thinking: + yield AgentResponseUpdate( + role=Role.ASSISTANT, + contents=[Content.from_text_reasoning(text=thinking, raw_representation=message)], + raw_representation=message, + ) + elif isinstance(message, ResultMessage): + session_id = message.session_id + + # Update thread with session ID + if session_id: + thread.service_thread_id = session_id diff --git a/python/packages/claude/agent_framework_claude/_settings.py b/python/packages/claude/agent_framework_claude/_settings.py new file mode 100644 index 0000000000..b01e189cc8 --- /dev/null +++ b/python/packages/claude/agent_framework_claude/_settings.py @@ -0,0 +1,51 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import ClassVar + +from agent_framework._pydantic import AFBaseSettings + +__all__ = ["ClaudeAgentSettings"] + + +class ClaudeAgentSettings(AFBaseSettings): + """Claude Agent settings. + + The settings are first loaded from environment variables with the prefix 'CLAUDE_AGENT_'. + If the environment variables are not found, the settings can be loaded from a .env file + with the encoding 'utf-8'. If the settings are not found in the .env file, the settings + are ignored; however, validation will fail alerting that the settings are missing. + + Keyword Args: + cli_path: The path to Claude CLI executable. + model: The model to use (sonnet, opus, haiku). + cwd: The working directory for Claude CLI. + permission_mode: Permission mode (default, acceptEdits, plan, bypassPermissions). + max_turns: Maximum number of conversation turns. + max_budget_usd: Maximum budget in USD. + env_file_path: If provided, the .env settings are read from this file path location. + env_file_encoding: The encoding of the .env file, defaults to 'utf-8'. + + Examples: + .. code-block:: python + + from agent_framework.anthropic import ClaudeAgentSettings + + # Using environment variables + # Set CLAUDE_AGENT_MODEL=sonnet + # CLAUDE_AGENT_PERMISSION_MODE=default + + # Or passing parameters directly + settings = ClaudeAgentSettings(model="sonnet") + + # Or loading from a .env file + settings = ClaudeAgentSettings(env_file_path="path/to/.env") + """ + + env_prefix: ClassVar[str] = "CLAUDE_AGENT_" + + cli_path: str | None = None + model: str | None = None + cwd: str | None = None + permission_mode: str | None = None + max_turns: int | None = None + max_budget_usd: float | None = None diff --git a/python/packages/claude/pyproject.toml b/python/packages/claude/pyproject.toml new file mode 100644 index 0000000000..1fd9d04f54 --- /dev/null +++ b/python/packages/claude/pyproject.toml @@ -0,0 +1,89 @@ +[project] +name = "agent-framework-claude" +description = "Claude Agent SDK integration for Microsoft Agent Framework." +authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] +readme = "README.md" +requires-python = ">=3.10" +version = "1.0.0b260130" +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", + "claude-agent-sdk>=0.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 = [ + "ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*" +] +timeout = 120 + +[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_claude"] +exclude_dirs = ["tests"] + +[tool.poe] +executor.type = "uv" +include = "../../shared_tasks.toml" +[tool.poe.tasks] +mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_claude" +test = "pytest --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests" + +[build-system] +requires = ["flit-core >= 3.11,<4.0"] +build-backend = "flit_core.buildapi" diff --git a/python/packages/claude/tests/__init__.py b/python/packages/claude/tests/__init__.py new file mode 100644 index 0000000000..2a50eae894 --- /dev/null +++ b/python/packages/claude/tests/__init__.py @@ -0,0 +1 @@ +# Copyright (c) Microsoft. All rights reserved. diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py new file mode 100644 index 0000000000..4402b611e4 --- /dev/null +++ b/python/packages/claude/tests/test_claude_agent.py @@ -0,0 +1,714 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from agent_framework import AgentResponseUpdate, AgentThread, ChatMessage, Content, Role, tool + +from agent_framework_claude import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings +from agent_framework_claude._agent import TOOLS_MCP_SERVER_NAME + +# region Test ClaudeAgentSettings + + +class TestClaudeAgentSettings: + """Tests for ClaudeAgentSettings.""" + + def test_env_prefix(self) -> None: + """Test that env_prefix is correctly set.""" + assert ClaudeAgentSettings.env_prefix == "CLAUDE_AGENT_" + + def test_default_values(self) -> None: + """Test default values are None.""" + settings = ClaudeAgentSettings() + assert settings.cli_path is None + assert settings.model is None + assert settings.cwd is None + assert settings.permission_mode is None + assert settings.max_turns is None + assert settings.max_budget_usd is None + + def test_explicit_values(self) -> None: + """Test explicit values override defaults.""" + settings = ClaudeAgentSettings( + cli_path="/usr/local/bin/claude", + model="sonnet", + cwd="/home/user/project", + permission_mode="default", + max_turns=10, + max_budget_usd=5.0, + ) + assert settings.cli_path == "/usr/local/bin/claude" + assert settings.model == "sonnet" + assert settings.cwd == "/home/user/project" + assert settings.permission_mode == "default" + assert settings.max_turns == 10 + assert settings.max_budget_usd == 5.0 + + def test_env_variable_loading(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test loading from environment variables.""" + monkeypatch.setenv("CLAUDE_AGENT_MODEL", "opus") + monkeypatch.setenv("CLAUDE_AGENT_MAX_TURNS", "20") + settings = ClaudeAgentSettings() + assert settings.model == "opus" + assert settings.max_turns == 20 + + +# region Test ClaudeAgent Initialization + + +class TestClaudeAgentInit: + """Tests for ClaudeAgent initialization.""" + + def test_default_initialization(self) -> None: + """Test agent initializes with defaults.""" + agent = ClaudeAgent() + assert agent.id is not None + assert agent.name is None + assert agent.description is None + + def test_with_name_and_description(self) -> None: + """Test agent with name and description.""" + agent = ClaudeAgent(name="test-agent", description="A test agent") + assert agent.name == "test-agent" + assert agent.description == "A test agent" + + def test_with_instructions_parameter(self) -> None: + """Test agent with instructions parameter.""" + agent = ClaudeAgent(instructions="You are a helpful assistant.") + assert agent._default_options.get("system_prompt") == "You are a helpful assistant." # type: ignore[reportPrivateUsage] + + def test_with_system_prompt_in_options(self) -> None: + """Test agent with system_prompt in options.""" + options: ClaudeAgentOptions = { + "system_prompt": "You are a helpful assistant.", + } + agent = ClaudeAgent(default_options=options) + assert agent._default_options.get("system_prompt") == "You are a helpful assistant." # type: ignore[reportPrivateUsage] + + def test_with_default_options(self) -> None: + """Test agent with default options.""" + options: ClaudeAgentOptions = { + "model": "sonnet", + "permission_mode": "default", + "max_turns": 10, + } + agent = ClaudeAgent(default_options=options) + assert agent._settings.model == "sonnet" # type: ignore[reportPrivateUsage] + assert agent._settings.permission_mode == "default" # type: ignore[reportPrivateUsage] + assert agent._settings.max_turns == 10 # type: ignore[reportPrivateUsage] + + def test_with_function_tool(self) -> None: + """Test agent with function tool.""" + + @tool + def greet(name: str) -> str: + """Greet someone.""" + return f"Hello, {name}!" + + agent = ClaudeAgent(tools=[greet]) + assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage] + + def test_with_single_tool(self) -> None: + """Test agent with single tool (not in list).""" + + @tool + def greet(name: str) -> str: + """Greet someone.""" + return f"Hello, {name}!" + + agent = ClaudeAgent(tools=greet) + assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage] + + def test_with_builtin_tools(self) -> None: + """Test agent with built-in tool names.""" + agent = ClaudeAgent(tools=["Read", "Write", "Bash"]) + assert agent._builtin_tools == ["Read", "Write", "Bash"] # type: ignore[reportPrivateUsage] + assert agent._custom_tools == [] # type: ignore[reportPrivateUsage] + + def test_with_mixed_tools(self) -> None: + """Test agent with both built-in and custom tools.""" + + @tool + def greet(name: str) -> str: + """Greet someone.""" + return f"Hello, {name}!" + + agent = ClaudeAgent(tools=["Read", greet, "Bash"]) + assert agent._builtin_tools == ["Read", "Bash"] # type: ignore[reportPrivateUsage] + assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage] + + +# region Test ClaudeAgent Lifecycle + + +class TestClaudeAgentLifecycle: + """Tests for ClaudeAgent tool initialization.""" + + def test_custom_tools_stored_from_constructor(self) -> None: + """Test that custom tools from constructor are stored.""" + + @tool + def greet(name: str) -> str: + """Greet someone.""" + return f"Hello, {name}!" + + agent = ClaudeAgent(tools=[greet]) + assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage] + + def test_multiple_custom_tools(self) -> None: + """Test agent with multiple custom tools.""" + + @tool + def greet(name: str) -> str: + """Greet someone.""" + return f"Hello, {name}!" + + @tool + def farewell(name: str) -> str: + """Say goodbye.""" + return f"Goodbye, {name}!" + + agent = ClaudeAgent(tools=[greet, farewell]) + assert len(agent._custom_tools) == 2 # type: ignore[reportPrivateUsage] + + def test_no_tools(self) -> None: + """Test agent without tools.""" + agent = ClaudeAgent() + assert agent._custom_tools == [] # type: ignore[reportPrivateUsage] + assert agent._builtin_tools == [] # type: ignore[reportPrivateUsage] + + +# region Test ClaudeAgent Run + + +class TestClaudeAgentRun: + """Tests for ClaudeAgent run method.""" + + @staticmethod + async def _create_async_generator(items: list[Any]) -> Any: + """Helper to create async generator from list.""" + for item in items: + yield item + + def _create_mock_client(self, messages: list[Any]) -> MagicMock: + """Create a mock ClaudeSDKClient that yields given messages.""" + mock_client = MagicMock() + mock_client.connect = AsyncMock() + mock_client.disconnect = AsyncMock() + mock_client.query = AsyncMock() + mock_client.set_model = AsyncMock() + mock_client.set_permission_mode = AsyncMock() + mock_client.receive_response = MagicMock(return_value=self._create_async_generator(messages)) + return mock_client + + async def test_run_with_string_message(self) -> None: + """Test run with string message.""" + from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock + from claude_agent_sdk.types import StreamEvent + + messages = [ + StreamEvent( + event={ + "type": "content_block_delta", + "delta": {"type": "text_delta", "text": "Hello!"}, + }, + uuid="event-1", + session_id="session-123", + ), + AssistantMessage( + content=[TextBlock(text="Hello!")], + model="claude-sonnet", + ), + ResultMessage( + subtype="success", + duration_ms=100, + duration_api_ms=50, + is_error=False, + num_turns=1, + session_id="session-123", + ), + ] + mock_client = self._create_mock_client(messages) + + with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client): + agent = ClaudeAgent() + response = await agent.run("Hello") + assert response.text == "Hello!" + + async def test_run_captures_session_id(self) -> None: + """Test that session ID is captured from ResultMessage.""" + from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock + from claude_agent_sdk.types import StreamEvent + + messages = [ + StreamEvent( + event={ + "type": "content_block_delta", + "delta": {"type": "text_delta", "text": "Response"}, + }, + uuid="event-1", + session_id="test-session-id", + ), + AssistantMessage( + content=[TextBlock(text="Response")], + model="claude-sonnet", + ), + ResultMessage( + subtype="success", + duration_ms=100, + duration_api_ms=50, + is_error=False, + num_turns=1, + session_id="test-session-id", + ), + ] + mock_client = self._create_mock_client(messages) + + with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client): + agent = ClaudeAgent() + thread = agent.get_new_thread() + await agent.run("Hello", thread=thread) + assert thread.service_thread_id == "test-session-id" + + async def test_run_with_thread(self) -> None: + """Test run with existing thread.""" + from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock + from claude_agent_sdk.types import StreamEvent + + messages = [ + StreamEvent( + event={ + "type": "content_block_delta", + "delta": {"type": "text_delta", "text": "Response"}, + }, + uuid="event-1", + session_id="session-123", + ), + AssistantMessage( + content=[TextBlock(text="Response")], + model="claude-sonnet", + ), + ResultMessage( + subtype="success", + duration_ms=100, + duration_api_ms=50, + is_error=False, + num_turns=1, + session_id="session-123", + ), + ] + mock_client = self._create_mock_client(messages) + + with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client): + agent = ClaudeAgent() + thread = agent.get_new_thread() + thread.service_thread_id = "existing-session" + await agent.run("Hello", thread=thread) + + +# region Test ClaudeAgent Run Stream + + +class TestClaudeAgentRunStream: + """Tests for ClaudeAgent run_stream method.""" + + @staticmethod + async def _create_async_generator(items: list[Any]) -> Any: + """Helper to create async generator from list.""" + for item in items: + yield item + + def _create_mock_client(self, messages: list[Any]) -> MagicMock: + """Create a mock ClaudeSDKClient that yields given messages.""" + mock_client = MagicMock() + mock_client.connect = AsyncMock() + mock_client.disconnect = AsyncMock() + mock_client.query = AsyncMock() + mock_client.set_model = AsyncMock() + mock_client.set_permission_mode = AsyncMock() + mock_client.receive_response = MagicMock(return_value=self._create_async_generator(messages)) + return mock_client + + async def test_run_stream_yields_updates(self) -> None: + """Test run_stream yields AgentResponseUpdate objects.""" + from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock + from claude_agent_sdk.types import StreamEvent + + messages = [ + StreamEvent( + event={ + "type": "content_block_delta", + "delta": {"type": "text_delta", "text": "Streaming "}, + }, + uuid="event-1", + session_id="stream-session", + ), + StreamEvent( + event={ + "type": "content_block_delta", + "delta": {"type": "text_delta", "text": "response"}, + }, + uuid="event-2", + session_id="stream-session", + ), + AssistantMessage( + content=[TextBlock(text="Streaming response")], + model="claude-sonnet", + ), + ResultMessage( + subtype="success", + duration_ms=100, + duration_api_ms=50, + is_error=False, + num_turns=1, + session_id="stream-session", + ), + ] + mock_client = self._create_mock_client(messages) + + with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client): + agent = ClaudeAgent() + updates: list[AgentResponseUpdate] = [] + async for update in agent.run_stream("Hello"): + updates.append(update) + # StreamEvent yields text deltas + assert len(updates) == 2 + assert updates[0].role == Role.ASSISTANT + assert updates[0].text == "Streaming " + assert updates[1].text == "response" + + +# region Test ClaudeAgent Session Management + + +class TestClaudeAgentSessionManagement: + """Tests for ClaudeAgent session management.""" + + def test_get_new_thread(self) -> None: + """Test get_new_thread creates a new thread.""" + agent = ClaudeAgent() + thread = agent.get_new_thread() + assert isinstance(thread, AgentThread) + assert thread.service_thread_id is None + + def test_get_new_thread_with_service_thread_id(self) -> None: + """Test get_new_thread with existing service_thread_id.""" + agent = ClaudeAgent() + thread = agent.get_new_thread(service_thread_id="existing-session-123") + assert isinstance(thread, AgentThread) + assert thread.service_thread_id == "existing-session-123" + + def test_thread_inherits_context_provider(self) -> None: + """Test that thread inherits context provider.""" + mock_provider = MagicMock() + agent = ClaudeAgent(context_provider=mock_provider) + thread = agent.get_new_thread() + assert thread.context_provider == mock_provider + + async def test_ensure_session_creates_client(self) -> None: + """Test _ensure_session creates client when not started.""" + with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class: + mock_client = MagicMock() + mock_client.connect = AsyncMock() + mock_client_class.return_value = mock_client + + agent = ClaudeAgent() + await agent._ensure_session(None) # type: ignore[reportPrivateUsage] + + assert agent._started # type: ignore[reportPrivateUsage] + mock_client.connect.assert_called_once() + + async def test_ensure_session_recreates_for_different_session(self) -> None: + """Test _ensure_session recreates client for different session ID.""" + with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class: + mock_client1 = MagicMock() + mock_client1.connect = AsyncMock() + mock_client1.disconnect = AsyncMock() + + mock_client2 = MagicMock() + mock_client2.connect = AsyncMock() + + mock_client_class.side_effect = [mock_client1, mock_client2] + + agent = ClaudeAgent() + + # First session + await agent._ensure_session(None) # type: ignore[reportPrivateUsage] + assert agent._started # type: ignore[reportPrivateUsage] + + # Different session should recreate client + await agent._ensure_session("new-session-id") # type: ignore[reportPrivateUsage] + assert agent._current_session_id == "new-session-id" # type: ignore[reportPrivateUsage] + mock_client1.disconnect.assert_called_once() + + async def test_ensure_session_reuses_for_same_session(self) -> None: + """Test _ensure_session reuses client for same session ID.""" + with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class: + mock_client = MagicMock() + mock_client.connect = AsyncMock() + mock_client_class.return_value = mock_client + + agent = ClaudeAgent() + + # First call + await agent._ensure_session("session-123") # type: ignore[reportPrivateUsage] + + # Same session should not recreate + await agent._ensure_session("session-123") # type: ignore[reportPrivateUsage] + + # Only called once + assert mock_client_class.call_count == 1 + + +# region Test ClaudeAgent Tool Conversion + + +class TestClaudeAgentToolConversion: + """Tests for ClaudeAgent tool conversion.""" + + def test_prepare_tools_creates_mcp_server(self) -> None: + """Test _prepare_tools creates MCP server for AF tools.""" + + @tool + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + agent = ClaudeAgent(tools=[add]) + server, tool_names = agent._prepare_tools(agent._custom_tools) # type: ignore[reportPrivateUsage] + + assert server is not None + assert len(tool_names) == 1 + assert tool_names[0] == f"mcp__{TOOLS_MCP_SERVER_NAME}__add" + + def test_function_tool_to_sdk_mcp_tool(self) -> None: + """Test converting FunctionTool to SDK MCP tool.""" + + @tool + def greet(name: str) -> str: + """Greet someone.""" + return f"Hello, {name}!" + + agent = ClaudeAgent() + sdk_tool = agent._function_tool_to_sdk_mcp_tool(greet) # type: ignore[reportPrivateUsage] + + assert sdk_tool.name == "greet" + assert sdk_tool.description == "Greet someone." + assert sdk_tool.input_schema is not None + assert "properties" in sdk_tool.input_schema # type: ignore[operator] + + async def test_tool_handler_success(self) -> None: + """Test tool handler executes successfully.""" + + @tool + def greet(name: str) -> str: + """Greet someone.""" + return f"Hello, {name}!" + + agent = ClaudeAgent() + sdk_tool = agent._function_tool_to_sdk_mcp_tool(greet) # type: ignore[reportPrivateUsage] + + result = await sdk_tool.handler({"name": "World"}) + assert result["content"][0]["text"] == "Hello, World!" + + async def test_tool_handler_error(self) -> None: + """Test tool handler handles errors.""" + + @tool + def failing_tool() -> str: + """A tool that fails.""" + raise ValueError("Something went wrong") + + agent = ClaudeAgent() + sdk_tool = agent._function_tool_to_sdk_mcp_tool(failing_tool) # type: ignore[reportPrivateUsage] + + result = await sdk_tool.handler({}) + assert "Error:" in result["content"][0]["text"] + assert "Something went wrong" in result["content"][0]["text"] + + +# region Test ClaudeAgent Permissions + + +class TestClaudeAgentPermissions: + """Tests for ClaudeAgent permission handling.""" + + def test_default_permission_mode(self) -> None: + """Test default permission mode.""" + agent = ClaudeAgent() + assert agent._settings.permission_mode is None # type: ignore[reportPrivateUsage] + + def test_permission_mode_from_settings(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test permission mode from environment settings.""" + monkeypatch.setenv("CLAUDE_AGENT_PERMISSION_MODE", "acceptEdits") + settings = ClaudeAgentSettings() + assert settings.permission_mode == "acceptEdits" + + def test_permission_mode_in_options(self) -> None: + """Test permission mode in options.""" + options: ClaudeAgentOptions = { + "permission_mode": "bypassPermissions", + } + agent = ClaudeAgent(default_options=options) + assert agent._settings.permission_mode == "bypassPermissions" # type: ignore[reportPrivateUsage] + + +# region Test ClaudeAgent Error Handling + + +class TestClaudeAgentErrorHandling: + """Tests for ClaudeAgent error handling.""" + + @staticmethod + async def _empty_gen() -> Any: + """Empty async generator.""" + if False: + yield + + async def test_handles_empty_response(self) -> None: + """Test handling of empty response.""" + mock_client = MagicMock() + mock_client.connect = AsyncMock() + mock_client.disconnect = AsyncMock() + mock_client.query = AsyncMock() + mock_client.set_model = AsyncMock() + mock_client.set_permission_mode = AsyncMock() + mock_client.receive_response = MagicMock(return_value=self._empty_gen()) + + with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client): + agent = ClaudeAgent() + response = await agent.run("Hello") + assert response.messages == [] + + +# region Test Format Prompt + + +class TestFormatPrompt: + """Tests for _format_prompt method.""" + + def test_format_empty_messages(self) -> None: + """Test formatting empty messages.""" + agent = ClaudeAgent() + result = agent._format_prompt([]) # type: ignore[reportPrivateUsage] + assert result == "" + + def test_format_none_messages(self) -> None: + """Test formatting None messages.""" + agent = ClaudeAgent() + result = agent._format_prompt(None) # type: ignore[reportPrivateUsage] + assert result == "" + + def test_format_user_message(self) -> None: + """Test formatting user message.""" + agent = ClaudeAgent() + msg = ChatMessage( + role=Role.USER, + contents=[Content.from_text(text="Hello")], + ) + result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage] + assert "Hello" in result + + def test_format_multiple_messages(self) -> None: + """Test formatting multiple messages.""" + agent = ClaudeAgent() + messages = [ + ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hi")]), + ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello!")]), + ChatMessage(role=Role.USER, contents=[Content.from_text(text="How are you?")]), + ] + result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage] + assert "Hi" in result + assert "Hello!" in result + assert "How are you?" in result + + +# region Test Build Options + + +class TestPrepareClientOptions: + """Tests for _prepare_client_options method.""" + + def test_prepare_client_options_with_settings(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test preparing options with settings.""" + monkeypatch.setenv("CLAUDE_AGENT_MODEL", "opus") + monkeypatch.setenv("CLAUDE_AGENT_MAX_TURNS", "15") + + agent = ClaudeAgent() + + with patch("agent_framework_claude._agent.SDKOptions") as mock_opts: + mock_opts.return_value = MagicMock() + agent._prepare_client_options() # type: ignore[reportPrivateUsage] + call_kwargs = mock_opts.call_args[1] + assert call_kwargs.get("model") == "opus" + assert call_kwargs.get("max_turns") == 15 + + def test_prepare_client_options_with_instructions(self) -> None: + """Test building options with instructions parameter.""" + agent = ClaudeAgent(instructions="Be helpful") + + with patch("agent_framework_claude._agent.SDKOptions") as mock_opts: + mock_opts.return_value = MagicMock() + agent._prepare_client_options() # type: ignore[reportPrivateUsage] + call_kwargs = mock_opts.call_args[1] + assert call_kwargs.get("system_prompt") == "Be helpful" + + def test_prepare_client_options_includes_custom_tools(self) -> None: + """Test that _prepare_client_options includes custom tools MCP server.""" + + @tool + def greet(name: str) -> str: + """Greet someone.""" + return f"Hello, {name}!" + + agent = ClaudeAgent(tools=[greet]) + + with patch("agent_framework_claude._agent.SDKOptions") as mock_opts: + mock_opts.return_value = MagicMock() + agent._prepare_client_options() # type: ignore[reportPrivateUsage] + call_kwargs = mock_opts.call_args[1] + assert "mcp_servers" in call_kwargs + assert TOOLS_MCP_SERVER_NAME in call_kwargs["mcp_servers"] + + +class TestApplyRuntimeOptions: + """Tests for _apply_runtime_options method.""" + + async def test_apply_runtime_model(self) -> None: + """Test applying runtime model option.""" + mock_client = MagicMock() + mock_client.set_model = AsyncMock() + mock_client.set_permission_mode = AsyncMock() + + agent = ClaudeAgent() + agent._client = mock_client # type: ignore[reportPrivateUsage] + + await agent._apply_runtime_options({"model": "opus"}) # type: ignore[reportPrivateUsage] + mock_client.set_model.assert_called_once_with("opus") + + async def test_apply_runtime_permission_mode(self) -> None: + """Test applying runtime permission_mode option.""" + mock_client = MagicMock() + mock_client.set_model = AsyncMock() + mock_client.set_permission_mode = AsyncMock() + + agent = ClaudeAgent() + agent._client = mock_client # type: ignore[reportPrivateUsage] + + await agent._apply_runtime_options({"permission_mode": "acceptEdits"}) # type: ignore[reportPrivateUsage] + mock_client.set_permission_mode.assert_called_once_with("acceptEdits") + + async def test_apply_runtime_options_none(self) -> None: + """Test applying None options does nothing.""" + mock_client = MagicMock() + mock_client.set_model = AsyncMock() + mock_client.set_permission_mode = AsyncMock() + + agent = ClaudeAgent() + agent._client = mock_client # type: ignore[reportPrivateUsage] + + await agent._apply_runtime_options(None) # type: ignore[reportPrivateUsage] + mock_client.set_model.assert_not_called() + mock_client.set_permission_mode.assert_not_called() diff --git a/python/packages/copilotstudio/pyproject.toml b/python/packages/copilotstudio/pyproject.toml index f2dcb2f8ff..fef08eeaa4 100644 --- a/python/packages/copilotstudio/pyproject.toml +++ b/python/packages/copilotstudio/pyproject.toml @@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml index 58f285f0ef..b68e8038dd 100644 --- a/python/packages/core/pyproject.toml +++ b/python/packages/core/pyproject.toml @@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/declarative/pyproject.toml b/python/packages/declarative/pyproject.toml index 5ae6e325a3..ab43d50104 100644 --- a/python/packages/declarative/pyproject.toml +++ b/python/packages/declarative/pyproject.toml @@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/devui/pyproject.toml b/python/packages/devui/pyproject.toml index 7c07ab3f15..6ea79e48e0 100644 --- a/python/packages/devui/pyproject.toml +++ b/python/packages/devui/pyproject.toml @@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://github.com/microsoft/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/durabletask/pyproject.toml b/python/packages/durabletask/pyproject.toml index 3c854f7243..e8b66c59ab 100644 --- a/python/packages/durabletask/pyproject.toml +++ b/python/packages/durabletask/pyproject.toml @@ -4,7 +4,7 @@ description = "Durable Task integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/foundry_local/pyproject.toml b/python/packages/foundry_local/pyproject.toml index 41c638ec20..1a338ede43 100644 --- a/python/packages/foundry_local/pyproject.toml +++ b/python/packages/foundry_local/pyproject.toml @@ -4,7 +4,7 @@ description = "Foundry Local integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/github_copilot/pyproject.toml b/python/packages/github_copilot/pyproject.toml index 9adb4caa92..a80197d2d7 100644 --- a/python/packages/github_copilot/pyproject.toml +++ b/python/packages/github_copilot/pyproject.toml @@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/lab/pyproject.toml b/python/packages/lab/pyproject.toml index be5d8a9868..86cee50527 100644 --- a/python/packages/lab/pyproject.toml +++ b/python/packages/lab/pyproject.toml @@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework" authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/mem0/pyproject.toml b/python/packages/mem0/pyproject.toml index 619b3ccd4d..26e8343aa9 100644 --- a/python/packages/mem0/pyproject.toml +++ b/python/packages/mem0/pyproject.toml @@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/ollama/pyproject.toml b/python/packages/ollama/pyproject.toml index 84bfc15e1b..e050978fca 100644 --- a/python/packages/ollama/pyproject.toml +++ b/python/packages/ollama/pyproject.toml @@ -4,7 +4,7 @@ description = "Ollama integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/purview/pyproject.toml b/python/packages/purview/pyproject.toml index d3e66f0834..df243154fc 100644 --- a/python/packages/purview/pyproject.toml +++ b/python/packages/purview/pyproject.toml @@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://github.com/microsoft/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/redis/pyproject.toml b/python/packages/redis/pyproject.toml index b53385097e..14c75ba37c 100644 --- a/python/packages/redis/pyproject.toml +++ b/python/packages/redis/pyproject.toml @@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/pyproject.toml b/python/pyproject.toml index 8afc0d36bc..a14354cbe4 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260128" +version = "1.0.0b260130" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core[all]==1.0.0b260128", + "agent-framework-core[all]==1.0.0b260130", ] [dependency-groups] @@ -103,6 +103,7 @@ agent-framework-ollama = { workspace = true } agent-framework-purview = { workspace = true } agent-framework-redis = { workspace = true } agent-framework-github-copilot = { workspace = true } +agent-framework-claude = { workspace = true } [tool.ruff] line-length = 120 diff --git a/python/samples/getting_started/agents/anthropic/README.md b/python/samples/getting_started/agents/anthropic/README.md index 2fee2f5c07..84a3b855d7 100644 --- a/python/samples/getting_started/agents/anthropic/README.md +++ b/python/samples/getting_started/agents/anthropic/README.md @@ -2,7 +2,7 @@ This folder contains examples demonstrating how to use Anthropic's Claude models with the Agent Framework. -## Examples +## Anthropic Client Examples | File | Description | |------|-------------| @@ -11,14 +11,36 @@ This folder contains examples demonstrating how to use Anthropic's Claude models | [`anthropic_skills.py`](anthropic_skills.py) | Illustrates how to use Anthropic-managed Skills with an agent, including the Code Interpreter tool and file generation and saving. | | [`anthropic_foundry.py`](anthropic_foundry.py) | Example of using Foundry's Anthropic integration with the Agent Framework. | +## Claude Agent Examples + +| File | Description | +|------|-------------| +| [`anthropic_claude_basic.py`](anthropic_claude_basic.py) | Basic usage of ClaudeAgent with streaming, non-streaming, and custom tools. | +| [`anthropic_claude_with_tools.py`](anthropic_claude_with_tools.py) | Using built-in tools (Read, Glob, Grep, etc.). | +| [`anthropic_claude_with_shell.py`](anthropic_claude_with_shell.py) | Shell command execution with interactive permission handling. | +| [`anthropic_claude_with_multiple_permissions.py`](anthropic_claude_with_multiple_permissions.py) | Combining multiple tools (Bash, Read, Write) with permission prompts. | +| [`anthropic_claude_with_url.py`](anthropic_claude_with_url.py) | Fetching and processing web content with WebFetch. | +| [`anthropic_claude_with_mcp.py`](anthropic_claude_with_mcp.py) | Local (stdio) and remote (HTTP) MCP server configuration. | +| [`anthropic_claude_with_session.py`](anthropic_claude_with_session.py) | Session management, persistence, and resumption. | + ## Environment Variables -Set the following environment variables before running the examples: +### Anthropic Client - `ANTHROPIC_API_KEY`: Your Anthropic API key (get one from [Anthropic Console](https://console.anthropic.com/)) - `ANTHROPIC_CHAT_MODEL_ID`: The Claude model to use (e.g., `claude-haiku-4-5`, `claude-sonnet-4-5-20250929`) -Or, for Foundry: +### Foundry + - `ANTHROPIC_FOUNDRY_API_KEY`: Your Foundry Anthropic API key - `ANTHROPIC_FOUNDRY_ENDPOINT`: The endpoint URL for your Foundry Anthropic resource - `ANTHROPIC_CHAT_MODEL_ID`: The Claude model to use in Foundry (e.g., `claude-haiku-4-5`) + +### Claude Agent + +- `CLAUDE_AGENT_CLI_PATH`: Path to the Claude Code CLI executable +- `CLAUDE_AGENT_MODEL`: Model to use (sonnet, opus, haiku) +- `CLAUDE_AGENT_CWD`: Working directory for Claude CLI +- `CLAUDE_AGENT_PERMISSION_MODE`: Permission mode (default, acceptEdits, plan, bypassPermissions) +- `CLAUDE_AGENT_MAX_TURNS`: Maximum number of conversation turns +- `CLAUDE_AGENT_MAX_BUDGET_USD`: Maximum budget in USD diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_basic.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_basic.py new file mode 100644 index 0000000000..f62cc60664 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_basic.py @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft. All rights reserved. + +""" +Claude Agent Basic Example + +This sample demonstrates using ClaudeAgent for basic interactions +with Claude Agent SDK. + +Prerequisites: +- Claude Code CLI must be installed and configured +- pip install agent-framework-claude + +Environment variables: +- CLAUDE_AGENT_MODEL: Model to use (sonnet, opus, haiku) +- CLAUDE_AGENT_PERMISSION_MODE: Permission mode (default, acceptEdits, bypassPermissions) +""" + +import asyncio +from typing import Annotated + +from agent_framework import tool +from agent_framework_claude import ClaudeAgent + + +@tool +def get_weather(location: Annotated[str, "The city name"]) -> str: + """Get the current weather for a location.""" + return f"The weather in {location} is sunny with a high of 25C." + + +async def non_streaming_example() -> None: + """Example of non-streaming response.""" + print("=== Non-streaming Example ===") + + agent = ClaudeAgent( + name="BasicAgent", + instructions="You are a helpful assistant. Keep responses concise.", + tools=[get_weather], + ) + + async with agent: + query = "What's the weather in Seattle?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result.text}\n") + + +async def streaming_example() -> None: + """Example of streaming response.""" + print("=== Streaming Example ===") + + agent = ClaudeAgent( + name="StreamingAgent", + instructions="You are a helpful assistant.", + tools=[get_weather], + ) + + async with agent: + query = "What's the weather in Paris?" + print(f"User: {query}") + print("Agent: ", end="", flush=True) + async for chunk in agent.run_stream(query): + if chunk.text: + print(chunk.text, end="", flush=True) + print("\n") + + +async def main() -> None: + print("=== Claude Agent Basic Example ===\n") + + await non_streaming_example() + await streaming_example() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_mcp.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_mcp.py new file mode 100644 index 0000000000..f47dbd1648 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_mcp.py @@ -0,0 +1,81 @@ +# Copyright (c) Microsoft. All rights reserved. + +""" +Claude Agent with MCP Servers + +This sample demonstrates how to configure MCP (Model Context Protocol) servers +with ClaudeAgent. It shows both local (stdio) and remote (HTTP) server +configurations, giving the agent access to external tools and data sources. + +Supported MCP server types: +- "stdio": Local process-based server +- "http": Remote HTTP server +- "sse": Remote SSE (Server-Sent Events) server + +SECURITY NOTE: MCP servers can expose powerful capabilities. Only configure +servers you trust. Use permission handlers to control what actions are allowed. +""" + +import asyncio +from typing import Any + +from agent_framework_claude import ClaudeAgent +from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny + + +async def prompt_permission( + tool_name: str, + tool_input: dict[str, Any], + context: object, +) -> PermissionResultAllow | PermissionResultDeny: + """Permission handler that prompts the user for approval.""" + print(f"\n[Permission Request: {tool_name}]") + + response = input("Approve? (y/n): ").strip().lower() + if response in ("y", "yes"): + return PermissionResultAllow() + return PermissionResultDeny(message="Denied by user") + + +async def main() -> None: + print("=== Claude Agent with MCP Servers ===\n") + + # Configure both local and remote MCP servers + mcp_servers: dict[str, Any] = { + # Local stdio server: provides filesystem access tools + "filesystem": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "."], + }, + # Remote HTTP server: Microsoft Learn documentation + "microsoft-learn": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp", + }, + } + + agent = ClaudeAgent( + instructions="You are a helpful assistant with access to the local filesystem and Microsoft Learn.", + default_options={ + "can_use_tool": prompt_permission, + "mcp_servers": mcp_servers, + }, + ) + + async with agent: + # Query that exercises the local filesystem MCP server + query1 = "List the first three files in the current directory" + print(f"User: {query1}") + result1 = await agent.run(query1) + print(f"Agent: {result1.text}\n") + + # Query that exercises the remote Microsoft Learn MCP server + query2 = "Search Microsoft Learn for 'Azure Functions Python' and summarize the top result" + print(f"User: {query2}") + result2 = await agent.run(query2) + print(f"Agent: {result2.text}\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_multiple_permissions.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_multiple_permissions.py new file mode 100644 index 0000000000..e4e2d10605 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_multiple_permissions.py @@ -0,0 +1,69 @@ +# Copyright (c) Microsoft. All rights reserved. + +""" +Claude Agent with Multiple Permissions + +This sample demonstrates how to enable multiple permission types with ClaudeAgent. +By combining different tools and using a permission handler, the agent can perform +complex tasks that require multiple capabilities. + +Available built-in tools: +- "Bash": Execute shell commands +- "Read": Read files from the filesystem +- "Write": Write files to the filesystem +- "Edit": Edit existing files +- "Glob": Search for files by pattern +- "Grep": Search file contents + +SECURITY NOTE: Only enable permissions that are necessary for your use case. +More permissions mean more potential for unintended actions. +""" + +import asyncio +from typing import Any + +from agent_framework_claude import ClaudeAgent +from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny + + +async def prompt_permission( + tool_name: str, + tool_input: dict[str, Any], + context: object, +) -> PermissionResultAllow | PermissionResultDeny: + """Permission handler that prompts the user for approval.""" + print(f"\n[Permission Request: {tool_name}]") + + if "command" in tool_input: + print(f" Command: {tool_input.get('command')}") + if "file_path" in tool_input: + print(f" Path: {tool_input.get('file_path')}") + if "pattern" in tool_input: + print(f" Pattern: {tool_input.get('pattern')}") + + response = input("Approve? (y/n): ").strip().lower() + if response in ("y", "yes"): + return PermissionResultAllow() + return PermissionResultDeny(message="Denied by user") + + +async def main() -> None: + print("=== Claude Agent with Multiple Permissions ===\n") + + agent = ClaudeAgent( + instructions="You are a helpful development assistant that can read, write files and run commands.", + tools=["Bash", "Read", "Write", "Glob"], + default_options={ + "can_use_tool": prompt_permission, + }, + ) + + async with agent: + query = "List the first 3 Python files, then read the first one and create a summary in summary.txt" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result.text}\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_session.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_session.py new file mode 100644 index 0000000000..2549457800 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_session.py @@ -0,0 +1,145 @@ +# Copyright (c) Microsoft. All rights reserved. + +""" +Claude Agent with Session Management + +This sample demonstrates session management with ClaudeAgent, showing +persistent conversation capabilities. Sessions are automatically persisted +by the Claude Code CLI. +""" + +import asyncio +from random import randint +from typing import Annotated + +from agent_framework import tool +from agent_framework_claude import ClaudeAgent +from pydantic import Field + + +@tool +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: + """Each agent instance creates a new session.""" + print("=== Automatic Session Creation Example ===") + + # First agent - first session + agent1 = ClaudeAgent( + instructions="You are a helpful weather agent.", + tools=[get_weather], + ) + + async with agent1: + query1 = "What's the weather like in Seattle?" + print(f"User: {query1}") + result1 = await agent1.run(query1) + print(f"Agent: {result1.text}") + + # Second agent - new session, no memory of previous conversation + agent2 = ClaudeAgent( + instructions="You are a helpful weather agent.", + tools=[get_weather], + ) + + async with agent2: + query2 = "What was the last city I asked about?" + print(f"\nUser: {query2}") + result2 = await agent2.run(query2) + print(f"Agent: {result2.text}") + print("Note: Each agent instance creates a separate session, so the agent doesn't remember previous context.\n") + + +async def example_with_session_persistence() -> None: + """Reuse session via thread object for multi-turn conversations.""" + print("=== Session Persistence Example ===") + + agent = ClaudeAgent( + instructions="You are a helpful weather agent.", + tools=[get_weather], + ) + + async with agent: + # Create a thread to maintain conversation context + thread = agent.get_new_thread() + + # First query + query1 = "What's the weather like in Tokyo?" + print(f"User: {query1}") + result1 = await agent.run(query1, thread=thread) + print(f"Agent: {result1.text}") + + # Second query - using same thread maintains context + query2 = "How about London?" + print(f"\nUser: {query2}") + result2 = await agent.run(query2, thread=thread) + print(f"Agent: {result2.text}") + + # Third query - 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, thread=thread) + 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: + """Resume session in new agent instance using service_thread_id.""" + print("=== Existing Session ID Example ===") + + existing_session_id = None + + # First agent instance - start a conversation + agent1 = ClaudeAgent( + instructions="You are a helpful weather agent.", + tools=[get_weather], + ) + + async with agent1: + thread = agent1.get_new_thread() + + query1 = "What's the weather in Paris?" + print(f"User: {query1}") + result1 = await agent1.run(query1, thread=thread) + print(f"Agent: {result1.text}") + + # Capture the session ID for later use + existing_session_id = thread.service_thread_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 ---") + + # Second agent instance - resume the conversation + agent2 = ClaudeAgent( + instructions="You are a helpful weather agent.", + tools=[get_weather], + ) + + async with agent2: + # Create thread with existing session ID + thread = agent2.get_new_thread(service_thread_id=existing_session_id) + + query2 = "What was the last city I asked about?" + print(f"User: {query2}") + result2 = await agent2.run(query2, thread=thread) + print(f"Agent: {result2.text}") + print("Note: The agent continues the conversation using the session ID.\n") + + +async def main() -> None: + print("=== Claude 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/getting_started/agents/anthropic/anthropic_claude_with_shell.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_shell.py new file mode 100644 index 0000000000..849a96c593 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_shell.py @@ -0,0 +1,57 @@ +# Copyright (c) Microsoft. All rights reserved. + +""" +Claude Agent with Shell Permissions + +This sample demonstrates how to enable shell command execution with ClaudeAgent. +By providing a permission handler via `can_use_tool`, the agent can execute +shell commands to perform tasks like listing files, running scripts, or executing system commands. + +SECURITY NOTE: Only enable shell permissions when you trust the agent's actions. +Shell commands have full access to your system within the permissions of the running process. +""" + +import asyncio +from typing import Any + +from agent_framework_claude import ClaudeAgent +from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny + + +async def prompt_permission( + tool_name: str, + tool_input: dict[str, Any], + context: object, +) -> PermissionResultAllow | PermissionResultDeny: + """Permission handler that prompts the user for approval.""" + print(f"\n[Permission Request: {tool_name}]") + + if "command" in tool_input: + print(f" Command: {tool_input.get('command')}") + + response = input("Approve? (y/n): ").strip().lower() + if response in ("y", "yes"): + return PermissionResultAllow() + return PermissionResultDeny(message="Denied by user") + + +async def main() -> None: + print("=== Claude Agent with Shell Permissions ===\n") + + agent = ClaudeAgent( + instructions="You are a helpful assistant that can execute shell commands.", + tools=["Bash"], + default_options={ + "can_use_tool": prompt_permission, + }, + ) + + async with agent: + query = "List the first 3 Python files in the current directory" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result.text}\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_tools.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_tools.py new file mode 100644 index 0000000000..15b9cbc5dc --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_tools.py @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft. All rights reserved. + +""" +Claude Agent with Built-in Tools + +This sample demonstrates using ClaudeAgent with built-in tools for file operations. +Built-in tools are specified as strings in the tools parameter. + +Available built-in tools: +- "Bash": Execute shell commands +- "Read": Read files from the filesystem +- "Write": Write files to the filesystem +- "Edit": Edit existing files +- "Glob": Search for files by pattern +- "Grep": Search file contents +""" + +import asyncio + +from agent_framework_claude import ClaudeAgent + + +async def main() -> None: + print("=== Claude Agent with Built-in Tools ===\n") + + # Built-in tools can be specified as strings in the tools parameter + agent = ClaudeAgent( + instructions="You are a helpful assistant that can read files.", + tools=["Read", "Glob"], + ) + + async with agent: + query = "List the first 3 Python files in the current directory" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result.text}\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_url.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_url.py new file mode 100644 index 0000000000..102785ca94 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_url.py @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft. All rights reserved. + +""" +Claude Agent with URL Fetching + +This sample demonstrates how to enable URL fetching with ClaudeAgent. +By enabling the WebFetch tool, the agent can fetch and process content from web URLs. + +Available web tools: +- "WebFetch": Fetch content from URLs +- "WebSearch": Search the web + +SECURITY NOTE: Only enable URL permissions when you trust the agent's actions. +URL fetching allows the agent to access any URL accessible from your network. +""" + +import asyncio + +from agent_framework_claude import ClaudeAgent + + +async def main() -> None: + print("=== Claude Agent with URL Fetching ===\n") + + agent = ClaudeAgent( + instructions="You are a helpful assistant that can fetch and summarize web content.", + tools=["WebFetch"], + ) + + async with agent: + query = "Fetch https://learn.microsoft.com/agent-framework/tutorials/quick-start and summarize its contents" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result.text}\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/uv.lock b/python/uv.lock index 0d7436c449..63e2cd11b8 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -35,6 +35,7 @@ members = [ "agent-framework-azurefunctions", "agent-framework-bedrock", "agent-framework-chatkit", + "agent-framework-claude", "agent-framework-copilotstudio", "agent-framework-core", "agent-framework-declarative", @@ -94,7 +95,7 @@ wheels = [ [[package]] name = "agent-framework" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { virtual = "." } dependencies = [ { name = "agent-framework-core", extra = ["all"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -161,7 +162,7 @@ docs = [ [[package]] name = "agent-framework-a2a" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/a2a" } dependencies = [ { name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -176,7 +177,7 @@ requires-dist = [ [[package]] name = "agent-framework-ag-ui" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/ag-ui" } dependencies = [ { name = "ag-ui-protocol", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -206,7 +207,7 @@ provides-extras = ["dev"] [[package]] name = "agent-framework-anthropic" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/anthropic" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -221,7 +222,7 @@ requires-dist = [ [[package]] name = "agent-framework-azure-ai" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/azure-ai" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -240,7 +241,7 @@ requires-dist = [ [[package]] name = "agent-framework-azure-ai-search" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/azure-ai-search" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -255,7 +256,7 @@ requires-dist = [ [[package]] name = "agent-framework-azurefunctions" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/azurefunctions" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -277,7 +278,7 @@ dev = [] [[package]] name = "agent-framework-bedrock" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/bedrock" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -294,7 +295,7 @@ requires-dist = [ [[package]] name = "agent-framework-chatkit" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/chatkit" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -307,9 +308,24 @@ requires-dist = [ { name = "openai-chatkit", specifier = ">=1.4.0,<2.0.0" }, ] +[[package]] +name = "agent-framework-claude" +version = "1.0.0b260130" +source = { editable = "packages/claude" } +dependencies = [ + { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "claude-agent-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 = "claude-agent-sdk", specifier = ">=0.1.25" }, +] + [[package]] name = "agent-framework-copilotstudio" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/copilotstudio" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -324,7 +340,7 @@ requires-dist = [ [[package]] name = "agent-framework-core" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/core" } dependencies = [ { name = "azure-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -394,7 +410,7 @@ provides-extras = ["all"] [[package]] name = "agent-framework-declarative" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/declarative" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -419,7 +435,7 @@ dev = [{ name = "types-pyyaml" }] [[package]] name = "agent-framework-devui" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/devui" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -453,7 +469,7 @@ provides-extras = ["dev", "all"] [[package]] name = "agent-framework-durabletask" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/durabletask" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -480,7 +496,7 @@ dev = [{ name = "types-python-dateutil", specifier = ">=2.9.0" }] [[package]] name = "agent-framework-foundry-local" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/foundry_local" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -495,7 +511,7 @@ requires-dist = [ [[package]] name = "agent-framework-github-copilot" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/github_copilot" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -510,7 +526,7 @@ requires-dist = [ [[package]] name = "agent-framework-lab" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/lab" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -601,7 +617,7 @@ dev = [ [[package]] name = "agent-framework-mem0" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/mem0" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -616,7 +632,7 @@ requires-dist = [ [[package]] name = "agent-framework-ollama" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/ollama" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -631,7 +647,7 @@ requires-dist = [ [[package]] name = "agent-framework-purview" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/purview" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -648,7 +664,7 @@ requires-dist = [ [[package]] name = "agent-framework-redis" -version = "1.0.0b260128" +version = "1.0.0b260130" source = { editable = "packages/redis" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1406,6 +1422,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "claude-agent-sdk" +version = "0.1.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "mcp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { 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/c5/ce/d8dd6eb56e981d1b981bf6766e1849878c54fbd160b6862e7c8e11b282d3/claude_agent_sdk-0.1.25.tar.gz", hash = "sha256:e2284fa2ece778d04b225f0f34118ea2623ae1f9fe315bc3bf921792658b6645", size = 57113, upload-time = "2026-01-29T01:20:17.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/09/e25dad92af3305ded5490d4493f782b1cb8c530145a7107bceea26ec811e/claude_agent_sdk-0.1.25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6adeffacbb75fe5c91529512331587a7af0e5e6dcbce4bd6b3a6ef8a51bdabeb", size = 54672313, upload-time = "2026-01-29T01:20:03.651Z" }, + { url = "https://files.pythonhosted.org/packages/28/0f/7b39ce9dd7d8f995e2c9d2049e1ce79f9010144a6793e8dd6ea9df23f53e/claude_agent_sdk-0.1.25-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:f210a05b2b471568c7f4019875b0ab451c783397f21edc32d7bd9a7144d9aad1", size = 68848229, upload-time = "2026-01-29T01:20:07.311Z" }, + { url = "https://files.pythonhosted.org/packages/40/6f/0b22cd9a68c39c0a8f5bd024072c15ca89bfa2dbfad3a94a35f6a1a90ecd/claude_agent_sdk-0.1.25-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:3399c3c748eb42deac308c6230cb0bb6b975c51b0495b42fe06896fa741d336f", size = 70562885, upload-time = "2026-01-29T01:20:11.033Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b6/2aaf28eeaa994e5491ad9589a9b006d5112b167aab8ced0823a6ffd86e4f/claude_agent_sdk-0.1.25-py3-none-win_amd64.whl", hash = "sha256:c5e8fe666b88049080ae4ac2a02dbd2d5c00ab1c495683d3c2f7dfab8ff1fec9", size = 72746667, upload-time = "2026-01-29T01:20:14.271Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -1423,7 +1456,7 @@ name = "clr-loader" version = "0.2.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { 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')" }, ] 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 = [ @@ -1926,7 +1959,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { 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')" }, + { 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/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -4687,8 +4720,8 @@ name = "powerfx" version = "0.0.34" source = { registry = "https://pypi.org/simple" } dependencies = [ - { 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'" }, + { 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')" }, ] 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 = [ @@ -5355,7 +5388,7 @@ name = "pythonnet" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { 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')" }, ] 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 = [ From f3e0be95556a0dd143c843f143f08f01945fe68d Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:17:52 -0800 Subject: [PATCH 06/20] Python: Disable mem0 telemetry by default (#3506) * disable mem0 telemetry by default * test fix * addressed comments --- python/packages/mem0/README.md | 11 +++++ .../mem0/agent_framework_mem0/__init__.py | 6 +++ .../mem0/agent_framework_mem0/_provider.py | 8 +++- .../mem0/tests/test_mem0_context_provider.py | 43 +++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/python/packages/mem0/README.md b/python/packages/mem0/README.md index cf2b88523d..a824c7db51 100644 --- a/python/packages/mem0/README.md +++ b/python/packages/mem0/README.md @@ -18,3 +18,14 @@ See the [Mem0 basic example](https://github.com/microsoft/agent-framework/tree/m - Teaching the agent user preferences - Retrieving information using remembered context across new threads - Persistent memory + +## Telemetry + +Mem0's telemetry is **disabled by default** when using this package. If you want to enable telemetry, set the environment variable before importing: + +```python +import os +os.environ["MEM0_TELEMETRY"] = "true" + +from agent_framework.mem0 import Mem0Provider +``` diff --git a/python/packages/mem0/agent_framework_mem0/__init__.py b/python/packages/mem0/agent_framework_mem0/__init__.py index 256e15356c..7ff88aaa42 100644 --- a/python/packages/mem0/agent_framework_mem0/__init__.py +++ b/python/packages/mem0/agent_framework_mem0/__init__.py @@ -1,6 +1,12 @@ # Copyright (c) Microsoft. All rights reserved. import importlib.metadata +import os + +# Disable Mem0 telemetry by default to prevent usage data from being sent to telemetry provider. +# Users can opt-in by setting MEM0_TELEMETRY=true before importing this package. +if os.environ.get("MEM0_TELEMETRY") is None: + os.environ["MEM0_TELEMETRY"] = "false" from ._provider import Mem0Provider diff --git a/python/packages/mem0/agent_framework_mem0/_provider.py b/python/packages/mem0/agent_framework_mem0/_provider.py index 3ff04df2a4..8ab9192d1a 100644 --- a/python/packages/mem0/agent_framework_mem0/_provider.py +++ b/python/packages/mem0/agent_framework_mem0/_provider.py @@ -30,7 +30,13 @@ MemorySearchResponse_v2 = list[dict[str, Any]] class Mem0Provider(ContextProvider): - """Mem0 Context Provider.""" + """Mem0 Context Provider. + + Note: + Mem0's telemetry is disabled by default when using this package. + To enable telemetry, set the environment variable ``MEM0_TELEMETRY=true`` before + importing this package. + """ def __init__( self, diff --git a/python/packages/mem0/tests/test_mem0_context_provider.py b/python/packages/mem0/tests/test_mem0_context_provider.py index 0013e7ce6e..85779b6ccf 100644 --- a/python/packages/mem0/tests/test_mem0_context_provider.py +++ b/python/packages/mem0/tests/test_mem0_context_provider.py @@ -1,6 +1,9 @@ # Copyright (c) Microsoft. All rights reserved. # pyright: reportPrivateUsage=false +import importlib +import os +import sys from unittest.mock import AsyncMock, patch import pytest @@ -592,3 +595,43 @@ class TestMem0ProviderBuildFilters: filters = provider._build_filters() assert filters == {} + + +class TestMem0Telemetry: + """Test telemetry configuration for Mem0.""" + + def test_mem0_telemetry_disabled_by_default(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that MEM0_TELEMETRY is set to 'false' by default when importing the package.""" + # Ensure MEM0_TELEMETRY is not set before importing the module under test + monkeypatch.delenv("MEM0_TELEMETRY", raising=False) + + # Remove cached modules to force re-import and trigger module-level initialization + modules_to_remove = [key for key in sys.modules if key.startswith("agent_framework_mem0")] + for mod in modules_to_remove: + del sys.modules[mod] + + # Import (and reload) the module so that it can set MEM0_TELEMETRY when unset + import agent_framework_mem0 + + importlib.reload(agent_framework_mem0) + + # The environment variable should be set to "false" after importing + assert os.environ.get("MEM0_TELEMETRY") == "false" + + def test_mem0_telemetry_respects_user_setting(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that user-set MEM0_TELEMETRY value is not overwritten.""" + # Remove cached modules to force re-import + modules_to_remove = [key for key in sys.modules if key.startswith("agent_framework_mem0")] + for mod in modules_to_remove: + del sys.modules[mod] + + # Set user preference before import + monkeypatch.setenv("MEM0_TELEMETRY", "true") + + # Re-import the module + import agent_framework_mem0 + + importlib.reload(agent_framework_mem0) + + # User setting should be preserved + assert os.environ.get("MEM0_TELEMETRY") == "true" From 493891620ded6eeca0a43c2889eebfc0230111e7 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:22:00 -0800 Subject: [PATCH 07/20] Python: Replaced obsolete create_response method in samples (#3542) * Replaced obsolete create_response method in samples * Addressed PR comments --- .../azure_ai/azure_ai_with_hosted_mcp.py | 4 ++-- .../azure_ai_with_hosted_mcp.py | 2 +- .../azure_ai_with_multiple_tools.py | 2 +- .../azure_responses_client_with_hosted_mcp.py | 6 +++--- ...openai_responses_client_with_hosted_mcp.py | 6 +++--- .../tools/function_tool_with_approval.py | 4 ++-- ...function_tool_with_approval_and_threads.py | 4 ++-- ...ff_with_tool_approval_checkpoint_resume.py | 20 +++++++++---------- .../agents_with_approval_requests.py | 15 +++++++------- .../concurrent_builder_tool_approval.py | 11 +++++----- .../group_chat_builder_tool_approval.py | 8 ++++---- .../sequential_builder_tool_approval.py | 12 +++++------ 12 files changed, 46 insertions(+), 48 deletions(-) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py index 8b120f703d..76394a8aac 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py @@ -28,7 +28,7 @@ async def handle_approvals_without_thread(query: str, agent: "AgentProtocol") -> new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed])) user_approval = input("Approve function call? (y/n): ") new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")]) + ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) result = await agent.run(new_inputs, store=False) @@ -50,7 +50,7 @@ async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", threa new_input.append( ChatMessage( role="user", - contents=[user_input_needed.create_response(user_approval.lower() == "y")], + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) ) result = await agent.run(new_input, thread=thread) diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_hosted_mcp.py index 71ab02b279..a16a6f7a92 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_hosted_mcp.py @@ -31,7 +31,7 @@ async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", threa new_input.append( ChatMessage( role="user", - contents=[user_input_needed.create_response(user_approval.lower() == "y")], + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) ) result = await agent.run(new_input, thread=thread, store=True) diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py index 244c57aa50..4539696bc6 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py @@ -59,7 +59,7 @@ async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", threa new_input.append( ChatMessage( role="user", - contents=[user_input_needed.create_response(user_approval.lower() == "y")], + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) ) result = await agent.run(new_input, thread=thread, store=True) diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py index 9ed1d74e16..0833065742 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py @@ -33,7 +33,7 @@ async def handle_approvals_without_thread(query: str, agent: "AgentProtocol"): new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed])) user_approval = input("Approve function call? (y/n): ") new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")]) + ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) result = await agent.run(new_inputs) @@ -56,7 +56,7 @@ async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", threa new_input.append( ChatMessage( role="user", - contents=[user_input_needed.create_response(user_approval.lower() == "y")], + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) ) result = await agent.run(new_input, thread=thread, store=True) @@ -82,7 +82,7 @@ async def handle_approvals_with_thread_streaming(query: str, agent: "AgentProtoc user_approval = input("Approve function call? (y/n): ") new_input.append( ChatMessage( - role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")] + role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")] ) ) new_input_added = True diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py index e86d113b75..e5fa62f040 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py @@ -32,7 +32,7 @@ async def handle_approvals_without_thread(query: str, agent: "AgentProtocol"): new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed])) user_approval = input("Approve function call? (y/n): ") new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")]) + ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) result = await agent.run(new_inputs) @@ -55,7 +55,7 @@ async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", threa new_input.append( ChatMessage( role="user", - contents=[user_input_needed.create_response(user_approval.lower() == "y")], + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) ) result = await agent.run(new_input, thread=thread, store=True) @@ -81,7 +81,7 @@ async def handle_approvals_with_thread_streaming(query: str, agent: "AgentProtoc user_approval = input("Approve function call? (y/n): ") new_input.append( ChatMessage( - role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")] + role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")] ) ) new_input_added = True diff --git a/python/samples/getting_started/tools/function_tool_with_approval.py b/python/samples/getting_started/tools/function_tool_with_approval.py index 9c026b8bc6..813bbb61ea 100644 --- a/python/samples/getting_started/tools/function_tool_with_approval.py +++ b/python/samples/getting_started/tools/function_tool_with_approval.py @@ -66,7 +66,7 @@ async def handle_approvals(query: str, agent: "AgentProtocol") -> AgentResponse: # Add the user's approval response new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")]) + ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) # Run again with all the context @@ -116,7 +116,7 @@ async def handle_approvals_streaming(query: str, agent: "AgentProtocol") -> None # Add the user's approval response new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")]) + ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) # Update input with all the context for next iteration diff --git a/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py b/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py index 80940efc1f..9b9c06837a 100644 --- a/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py +++ b/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py @@ -54,7 +54,7 @@ async def approval_example() -> None: print(f" Decision: {'Approved' if approved else 'Rejected'}") # Step 2: Send approval response - approval_response = request.create_response(approved=approved) + approval_response = request.to_function_approval_response(approved=approved) result = await agent.run(ChatMessage(role="user", contents=[approval_response]), thread=thread) print(f"Agent: {result}\n") @@ -87,7 +87,7 @@ async def rejection_example() -> None: print(" Decision: Rejected") # Send rejection response - rejection_response = request.create_response(approved=False) + rejection_response = request.to_function_approval_response(approved=False) result = await agent.run(ChatMessage(role="user", contents=[rejection_response]), thread=thread) print(f"Agent: {result}\n") diff --git a/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py b/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py index 0d60f6ca22..694fd868cf 100644 --- a/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py +++ b/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py @@ -9,8 +9,8 @@ from typing import cast from agent_framework import ( ChatAgent, ChatMessage, + Content, FileCheckpointStorage, - FunctionApprovalRequestContent, HandoffBuilder, HandoffUserInputRequest, RequestInfoEvent, @@ -26,7 +26,7 @@ from azure.identity import AzureCliCredential Sample: Handoff Workflow with Tool Approvals + Checkpoint Resume Demonstrates the two-step pattern for resuming a handoff workflow from a checkpoint -while handling both HandoffUserInputRequest prompts and FunctionApprovalRequestContent +while handling both HandoffUserInputRequest prompts and function approval request Content for tool calls (e.g., submit_refund). Scenario: @@ -137,7 +137,7 @@ def _print_handoff_request(request: HandoffUserInputRequest, request_id: str) -> print(f"{'=' * 60}\n") -def _print_function_approval_request(request: FunctionApprovalRequestContent, request_id: str) -> None: +def _print_function_approval_request(request: Content, request_id: str) -> None: """Log pending tool approval details for debugging.""" args = request.function_call.parse_arguments() or {} print(f"\n{'=' * 60}") @@ -161,10 +161,10 @@ def _build_responses_for_requests( if user_response is None: raise ValueError("User response is required for HandoffUserInputRequest") responses[request.request_id] = user_response - elif isinstance(request.data, FunctionApprovalRequestContent): + elif isinstance(request.data, Content) and request.data.type == "function_approval_request": if approve_tools is None: - raise ValueError("Approval decision is required for FunctionApprovalRequestContent") - responses[request.request_id] = request.data.create_response(approved=approve_tools) + raise ValueError("Approval decision is required for function approval request") + responses[request.request_id] = request.data.to_function_approval_response(approved=approve_tools) else: raise ValueError(f"Unsupported request type: {type(request.data)}") return responses @@ -201,7 +201,7 @@ async def run_until_user_input_needed( pending_requests.append(event) if isinstance(event.data, HandoffUserInputRequest): _print_handoff_request(event.data, event.request_id) - elif isinstance(event.data, FunctionApprovalRequestContent): + elif isinstance(event.data, Content) and event.data.type == "function_approval_request": _print_function_approval_request(event.data, event.request_id) elif isinstance(event, WorkflowOutputEvent): @@ -258,7 +258,7 @@ async def resume_with_responses( restored_requests.append(event) if isinstance(event.data, HandoffUserInputRequest): _print_handoff_request(event.data, event.request_id) - elif isinstance(event.data, FunctionApprovalRequestContent): + elif isinstance(event.data, Content) and event.data.type == "function_approval_request": _print_function_approval_request(event.data, event.request_id) if not restored_requests: @@ -291,7 +291,7 @@ async def resume_with_responses( new_pending_requests.append(event) if isinstance(event.data, HandoffUserInputRequest): _print_handoff_request(event.data, event.request_id) - elif isinstance(event.data, FunctionApprovalRequestContent): + elif isinstance(event.data, Content) and event.data.type == "function_approval_request": _print_function_approval_request(event.data, event.request_id) return new_pending_requests, latest_checkpoint.checkpoint_id @@ -362,7 +362,7 @@ async def main() -> None: workflow_step, _, _, _ = create_workflow(checkpoint_storage=storage) needs_user_input = any(isinstance(req.data, HandoffUserInputRequest) for req in pending_requests) - needs_tool_approval = any(isinstance(req.data, FunctionApprovalRequestContent) for req in pending_requests) + needs_tool_approval = any(isinstance(req.data, Content) and req.data.type == "function_approval_request" for req in pending_requests) user_response = None if needs_user_input: diff --git a/python/samples/getting_started/workflows/human-in-the-loop/agents_with_approval_requests.py b/python/samples/getting_started/workflows/human-in-the-loop/agents_with_approval_requests.py index b724f2876c..c49d2c1308 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/agents_with_approval_requests.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/agents_with_approval_requests.py @@ -9,9 +9,8 @@ from agent_framework import ( AgentExecutorResponse, ChatAgent, ChatMessage, + Content, Executor, - FunctionApprovalRequestContent, - FunctionApprovalResponseContent, WorkflowBuilder, WorkflowContext, tool, @@ -251,7 +250,7 @@ async def main() -> None: body="Please provide your team's status update on the project since last week.", ) - responses: dict[str, FunctionApprovalResponseContent] = {} + responses: dict[str, Content] = {} output: list[ChatMessage] | None = None while True: if responses: @@ -262,8 +261,8 @@ async def main() -> None: request_info_events = events.get_request_info_events() for request_info_event in request_info_events: - # We should only expect FunctionApprovalRequestContent in this sample - if not isinstance(request_info_event.data, FunctionApprovalRequestContent): + # We should only expect function_approval_request Content in this sample + if not isinstance(request_info_event.data, Content) or request_info_event.data.type != "function_approval_request": raise ValueError(f"Unexpected request info content type: {type(request_info_event.data)}") # Pretty print the function call details @@ -274,10 +273,10 @@ async def main() -> None: ) # For demo purposes, we automatically approve the request - # The expected response type of the request is `FunctionApprovalResponseContent`, - # which can be created via `create_response` method on the request content + # The expected response type of the request is `function_approval_response Content`, + # which can be created via `to_function_approval_response` method on the request content print("Performing automatic approval for demo purposes...") - responses[request_info_event.request_id] = request_info_event.data.create_response(approved=True) + responses[request_info_event.request_id] = request_info_event.data.to_function_approval_response(approved=True) # Once we get an output event, we can conclude the workflow # Outputs can only be produced by the conclude_workflow_executor in this sample diff --git a/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py b/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py index 83e6175a72..b43e01916f 100644 --- a/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py +++ b/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py @@ -6,8 +6,7 @@ from typing import Annotated from agent_framework import ( ChatMessage, ConcurrentBuilder, - FunctionApprovalRequestContent, - FunctionApprovalResponseContent, + Content, RequestInfoEvent, WorkflowOutputEvent, tool, @@ -139,7 +138,7 @@ async def main() -> None: ): if isinstance(event, RequestInfoEvent): request_info_events.append(event) - if isinstance(event.data, FunctionApprovalRequestContent): + if isinstance(event.data, Content) and event.data.type == "function_approval_request": print(f"\nApproval requested for tool: {event.data.function_call.name}") print(f" Arguments: {event.data.function_call.arguments}") elif isinstance(event, WorkflowOutputEvent): @@ -147,12 +146,12 @@ async def main() -> None: # 6. Handle approval requests (if any) if request_info_events: - responses: dict[str, FunctionApprovalResponseContent] = {} + responses: dict[str, Content] = {} for request_event in request_info_events: - if isinstance(request_event.data, FunctionApprovalRequestContent): + if isinstance(request_event.data, Content) and request_event.data.type == "function_approval_request": print(f"\nSimulating human approval for: {request_event.data.function_call.name}") # Create approval response - responses[request_event.request_id] = request_event.data.create_response(approved=True) + responses[request_event.request_id] = request_event.data.to_function_approval_response(approved=True) if responses: # Phase 2: Send all approvals and continue workflow diff --git a/python/samples/getting_started/workflows/tool-approval/group_chat_builder_tool_approval.py b/python/samples/getting_started/workflows/tool-approval/group_chat_builder_tool_approval.py index 3db5f32d1f..b4bc773eba 100644 --- a/python/samples/getting_started/workflows/tool-approval/group_chat_builder_tool_approval.py +++ b/python/samples/getting_started/workflows/tool-approval/group_chat_builder_tool_approval.py @@ -5,7 +5,7 @@ from typing import Annotated from agent_framework import ( AgentRunUpdateEvent, - FunctionApprovalRequestContent, + Content, GroupChatBuilder, GroupChatRequestSentEvent, GroupChatState, @@ -144,7 +144,7 @@ async def main() -> None: ): if isinstance(event, RequestInfoEvent): request_info_events.append(event) - if isinstance(event.data, FunctionApprovalRequestContent): + if isinstance(event.data, Content) and event.data.type == "function_approval_request": print("\n[APPROVAL REQUIRED] From agent:", event.source_executor_id) print(f" Tool: {event.data.function_call.name}") print(f" Arguments: {event.data.function_call.arguments}") @@ -164,7 +164,7 @@ async def main() -> None: # 6. Handle approval requests if request_info_events: for request_event in request_info_events: - if isinstance(request_event.data, FunctionApprovalRequestContent): + if isinstance(request_event.data, Content) and request_event.data.type == "function_approval_request": print("\n" + "=" * 60) print("Human review required for production deployment!") print("In a real scenario, you would review the deployment details here.") @@ -172,7 +172,7 @@ async def main() -> None: print("=" * 60) # Create approval response - approval_response = request_event.data.create_response(approved=True) + approval_response = request_event.data.to_function_approval_response(approved=True) # Phase 2: Send approval and continue workflow # Keep track of the response to format output nicely in streaming mode diff --git a/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py b/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py index 1397ce31a1..7712873943 100644 --- a/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py +++ b/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py @@ -5,7 +5,7 @@ from typing import Annotated from agent_framework import ( ChatMessage, - FunctionApprovalRequestContent, + Content, RequestInfoEvent, SequentialBuilder, WorkflowOutputEvent, @@ -23,7 +23,7 @@ with approval_mode="always_require" to trigger human-in-the-loop interactions. This sample works as follows: 1. A SequentialBuilder workflow is created with a single agent that has tools requiring approval. 2. The agent receives a user task and determines it needs to call a sensitive tool. -3. The tool call triggers a FunctionApprovalRequestContent, pausing the workflow. +3. The tool call triggers a function_approval_request Content, pausing the workflow. 4. The sample simulates human approval by responding to the RequestInfoEvent. 5. Once approved, the tool executes and the agent completes its response. 6. The workflow outputs the final conversation with all messages. @@ -34,7 +34,7 @@ requiring any additional builder configuration. Demonstrate: - Using @tool(approval_mode="always_require") for sensitive operations. -- Handling RequestInfoEvent with FunctionApprovalRequestContent in sequential workflows. +- Handling RequestInfoEvent with function_approval_request Content in sequential workflows. - Resuming workflow execution after approval via send_responses_streaming. Prerequisites: @@ -92,19 +92,19 @@ async def main() -> None: ): if isinstance(event, RequestInfoEvent): request_info_events.append(event) - if isinstance(event.data, FunctionApprovalRequestContent): + if isinstance(event.data, Content) and event.data.type == "function_approval_request": print(f"\nApproval requested for tool: {event.data.function_call.name}") print(f" Arguments: {event.data.function_call.arguments}") # 5. Handle approval requests if request_info_events: for request_event in request_info_events: - if isinstance(request_event.data, FunctionApprovalRequestContent): + if isinstance(request_event.data, Content) and request_event.data.type == "function_approval_request": # In a real application, you would prompt the user here print("\nSimulating human approval (auto-approving for demo)...") # Create approval response - approval_response = request_event.data.create_response(approved=True) + approval_response = request_event.data.to_function_approval_response(approved=True) # Phase 2: Send approval and continue workflow output: list[ChatMessage] | None = None From 2f7250fe0f5ae014434c64130858330a9b0e5fbc Mon Sep 17 00:00:00 2001 From: Rishabh Chawla Date: Fri, 30 Jan 2026 15:08:39 -0800 Subject: [PATCH 08/20] Python: Add tests to Purview Package (#3513) * Add tests to increase code coverage * Add tests to increase code coverage --- .../agent_framework_purview/_processor.py | 5 +- python/packages/purview/tests/test_cache.py | 19 ++ .../purview/tests/test_chat_middleware.py | 88 +++++++ python/packages/purview/tests/test_client.py | 216 +++++++++++++++++- .../packages/purview/tests/test_middleware.py | 86 +++++++ .../packages/purview/tests/test_processor.py | 77 +++++++ 6 files changed, 487 insertions(+), 4 deletions(-) diff --git a/python/packages/purview/agent_framework_purview/_processor.py b/python/packages/purview/agent_framework_purview/_processor.py index 0e197b78c0..fb115783f5 100644 --- a/python/packages/purview/agent_framework_purview/_processor.py +++ b/python/packages/purview/agent_framework_purview/_processor.py @@ -211,9 +211,8 @@ class ScopedContentProcessor: cache_key = create_protection_scopes_cache_key(ps_req) cached_ps_resp = await self._cache.get(cache_key) - if cached_ps_resp is not None: - if isinstance(cached_ps_resp, ProtectionScopesResponse): - ps_resp = cached_ps_resp + if cached_ps_resp is not None and isinstance(cached_ps_resp, ProtectionScopesResponse): + ps_resp = cached_ps_resp else: try: ps_resp = await self._client.get_protection_scopes(ps_req) diff --git a/python/packages/purview/tests/test_cache.py b/python/packages/purview/tests/test_cache.py index 2089d9e2e7..1892842d42 100644 --- a/python/packages/purview/tests/test_cache.py +++ b/python/packages/purview/tests/test_cache.py @@ -119,6 +119,25 @@ class TestInMemoryCacheProvider: assert result == obj + async def test_estimate_size_conservative_fallback_when_all_size_methods_fail(self, monkeypatch) -> None: + """Test that the cache returns a conservative size estimate when all strategies fail.""" + cache = InMemoryCacheProvider() + + class BadString: + def __str__(self) -> str: + raise RuntimeError("boom") + + def raise_getsizeof(_: object) -> int: + raise RuntimeError("no sizeof") + + monkeypatch.setattr("agent_framework_purview._cache.sys.getsizeof", raise_getsizeof) + + # Arrange/Act + size = cache._estimate_size(BadString()) + + # Assert + assert size == 1024 + async def test_cache_multiple_updates(self) -> None: """Test that updating a key multiple times maintains correct size tracking.""" cache = InMemoryCacheProvider(max_size_bytes=1000) diff --git a/python/packages/purview/tests/test_chat_middleware.py b/python/packages/purview/tests/test_chat_middleware.py index 8d414babb9..3f9595e721 100644 --- a/python/packages/purview/tests/test_chat_middleware.py +++ b/python/packages/purview/tests/test_chat_middleware.py @@ -204,6 +204,39 @@ class TestPurviewChatPolicyMiddleware: with pytest.raises(PurviewPaymentRequiredError): await middleware.process(context, mock_next) + async def test_chat_middleware_handles_payment_required_post_check(self, mock_credential: AsyncMock) -> None: + """Test that 402 in post-check is raised when ignore_payment_required=False.""" + from agent_framework_purview._exceptions import PurviewPaymentRequiredError + + settings = PurviewSettings(app_name="Test App", ignore_payment_required=False) + middleware = PurviewChatPolicyMiddleware(mock_credential, settings) + + chat_client = DummyChatClient() + chat_options = MagicMock() + chat_options.model = "test-model" + context = ChatContext( + chat_client=chat_client, messages=[ChatMessage(role=Role.USER, text="Hello")], options=chat_options + ) + + call_count = 0 + + async def side_effect(*args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count == 1: + return (False, "user-123") + raise PurviewPaymentRequiredError("Payment required") + + with patch.object(middleware._processor, "process_messages", side_effect=side_effect): + + async def mock_next(ctx: ChatContext) -> None: + result = MagicMock() + result.messages = [ChatMessage(role=Role.ASSISTANT, text="OK")] + ctx.result = result + + with pytest.raises(PurviewPaymentRequiredError): + await middleware.process(context, mock_next) + async def test_chat_middleware_ignores_payment_required_when_configured(self, mock_credential: AsyncMock) -> None: """Test that 402 is ignored when ignore_payment_required=True.""" from agent_framework_purview._exceptions import PurviewPaymentRequiredError @@ -274,3 +307,58 @@ class TestPurviewChatPolicyMiddleware: await middleware.process(context, mock_next) # Next should have been called assert context.result is not None + + async def test_chat_middleware_raises_on_pre_check_exception_when_ignore_exceptions_false( + self, mock_credential: AsyncMock + ) -> None: + """Test that exceptions are propagated by default when ignore_exceptions=False.""" + settings = PurviewSettings(app_name="Test App", ignore_exceptions=False) + middleware = PurviewChatPolicyMiddleware(mock_credential, settings) + + chat_client = DummyChatClient() + chat_options = MagicMock() + chat_options.model = "test-model" + context = ChatContext( + chat_client=chat_client, messages=[ChatMessage(role=Role.USER, text="Hello")], options=chat_options + ) + + with patch.object(middleware._processor, "process_messages", side_effect=ValueError("boom")): + + async def mock_next(_: ChatContext) -> None: + raise AssertionError("next should not be called") + + with pytest.raises(ValueError, match="boom"): + await middleware.process(context, mock_next) + + async def test_chat_middleware_raises_on_post_check_exception_when_ignore_exceptions_false( + self, mock_credential: AsyncMock + ) -> None: + """Test that post-check exceptions are propagated by default.""" + settings = PurviewSettings(app_name="Test App", ignore_exceptions=False) + middleware = PurviewChatPolicyMiddleware(mock_credential, settings) + + chat_client = DummyChatClient() + chat_options = MagicMock() + chat_options.model = "test-model" + context = ChatContext( + chat_client=chat_client, messages=[ChatMessage(role=Role.USER, text="Hello")], options=chat_options + ) + + call_count = 0 + + async def side_effect(*args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count == 1: + return (False, "user-123") + raise ValueError("post") + + with patch.object(middleware._processor, "process_messages", side_effect=side_effect): + + async def mock_next(ctx: ChatContext) -> None: + result = MagicMock() + result.messages = [ChatMessage(role=Role.ASSISTANT, text="OK")] + ctx.result = result + + with pytest.raises(ValueError, match="post"): + await middleware.process(context, mock_next) diff --git a/python/packages/purview/tests/test_client.py b/python/packages/purview/tests/test_client.py index 7953c16b77..b740b3f09c 100644 --- a/python/packages/purview/tests/test_client.py +++ b/python/packages/purview/tests/test_client.py @@ -2,6 +2,7 @@ """Tests for Purview client.""" +from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, MagicMock, patch import httpx @@ -18,6 +19,8 @@ from agent_framework_purview._exceptions import ( PurviewServiceError, ) from agent_framework_purview._models import ( + ContentActivitiesRequest, + ContentActivitiesResponse, PolicyLocation, ProcessContentRequest, ProtectionScopesRequest, @@ -47,7 +50,9 @@ class TestPurviewClient: return PurviewSettings(app_name="Test App", tenant_id="test-tenant", default_user_id="test-user") @pytest.fixture - async def client(self, mock_credential: MagicMock, settings: PurviewSettings) -> PurviewClient: + async def client( + self, mock_credential: MagicMock, settings: PurviewSettings + ) -> AsyncGenerator[PurviewClient, None]: """Create a PurviewClient with mock credential.""" client = PurviewClient(mock_credential, settings, timeout=10.0) yield client @@ -185,6 +190,215 @@ class TestPurviewClient: assert response.scope_identifier == "scope-123" assert response.scopes == [] + async def test_get_protection_scopes_uses_etag_header_when_present(self, client: PurviewClient) -> None: + """Test that get_protection_scopes prefers the HTTP ETag header when present.""" + from agent_framework_purview._models import ProtectionScopesResponse + + location = PolicyLocation(**{"@odata.type": "microsoft.graph.policyLocationApplication", "value": "app-id"}) + request = ProtectionScopesRequest( + user_id="user-123", tenant_id="tenant-456", locations=[location], correlation_id="corr-789" + ) + + response_obj = ProtectionScopesResponse(**{"scopeIdentifier": "scope-from-body", "value": []}) + + with patch.object( + client, + "_post", + return_value=(response_obj, {"etag": '"etag-from-header"'}), + ): + response = await client.get_protection_scopes(request) + + assert response.scope_identifier == "etag-from-header" + + async def test_post_402_returns_empty_response_when_ignore_payment_required_enabled( + self, mock_credential: MagicMock + ) -> None: + """Test that 402 is suppressed when ignore_payment_required=True.""" + from agent_framework_purview._models import ProcessContentResponse + + settings = PurviewSettings(app_name="Test App", ignore_payment_required=True) + client = PurviewClient(mock_credential, settings) + + request = ProcessContentRequest(user_id="user-123", tenant_id="tenant-456", content_to_process=[]) + + resp = httpx.Response(402, text="Payment required", request=httpx.Request("POST", "http://test")) + + with patch.object(client._client, "post", return_value=resp): + result = await client._post("http://test", request, ProcessContentResponse, token="fake-token") + + assert isinstance(result, ProcessContentResponse) + await client.close() + + async def test_post_sets_request_and_response_correlation_id(self, client: PurviewClient) -> None: + """Test that correlation_id is injected into request headers and hydrated from response headers.""" + from agent_framework_purview._models import ProcessContentResponse + + # correlation_id is optional and should be auto-generated when empty + request = ProcessContentRequest(user_id="user-123", tenant_id="tenant-456", content_to_process=[]) + request.correlation_id = "" # force auto-generation branch + + captured_headers: dict[str, str] = {} + + async def fake_post(url: str, json=None, headers=None): + nonlocal captured_headers + captured_headers = dict(headers or {}) + return httpx.Response( + 200, + json={"id": "resp-1", "protectionScopeState": "notModified"}, + headers={"client-request-id": "corr-from-response"}, + request=httpx.Request("POST", url), + ) + + with patch.object(client._client, "post", side_effect=fake_post): + result_obj, result_headers = await client._post( + "http://test", + request, + ProcessContentResponse, + token="fake-token", + return_response=True, + ) + + assert "client-request-id" in captured_headers + assert captured_headers["client-request-id"] + assert result_headers["client-request-id"] == "corr-from-response" + assert result_obj.correlation_id == "corr-from-response" + + async def test_process_content_402_returns_empty_when_ignored(self, mock_credential: MagicMock) -> None: + """Test that process_content returns an empty response (non-tuple path) when 402 is ignored.""" + from agent_framework_purview._models import ProcessContentResponse + + settings = PurviewSettings(app_name="Test App", ignore_payment_required=True) + client = PurviewClient(mock_credential, settings) + + req = ProcessContentRequest(user_id="user-123", tenant_id="tenant-456", content_to_process=[]) + + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 402 + mock_response.text = "Payment required" + + with patch.object(client._client, "post", return_value=mock_response): + response = await client.process_content(req) + + assert isinstance(response, ProcessContentResponse) + await client.close() + + async def test_post_sets_correlation_id_attribute_on_recording_span(self, client: PurviewClient) -> None: + """Test that correlation_id is added to the active span when recording is enabled.""" + from agent_framework_purview._models import ProcessContentResponse + + request = ProcessContentRequest(user_id="user-123", tenant_id="tenant-456", content_to_process=[]) + request.correlation_id = "corr-123" + + class RecordingSpan: + def __init__(self) -> None: + self.attributes: dict[str, str] = {} + + def is_recording(self) -> bool: + return True + + def set_attribute(self, key: str, value: str) -> None: + self.attributes[key] = value + + span = RecordingSpan() + + with ( + patch("agent_framework_purview._client.trace.get_current_span", return_value=span), + patch.object( + client._client, + "post", + return_value=httpx.Response( + 200, + json={"id": "resp-1", "protectionScopeState": "notModified"}, + headers={}, + request=httpx.Request("POST", "http://test"), + ), + ), + ): + await client._post("http://test", request, ProcessContentResponse, token="fake-token") + + assert span.attributes["correlation_id"] == "corr-123" + + async def test_post_uses_constructor_when_response_type_has_no_model_validate(self, client: PurviewClient) -> None: + """Test that _post falls back to the response type constructor when model_validate is absent.""" + + class DummyResponse: + def __init__(self, **data): + self.data = data + + request = ProcessContentRequest(user_id="user-123", tenant_id="tenant-456", content_to_process=[]) + request.correlation_id = "corr-123" + + with patch.object( + client._client, + "post", + return_value=httpx.Response( + 200, + json={"hello": "world"}, + headers={}, + request=httpx.Request("POST", "http://test"), + ), + ): + result = await client._post("http://test", request, DummyResponse, token="fake-token") + + assert isinstance(result, DummyResponse) + assert result.data == {"hello": "world"} + + async def test_send_content_activities_success(self, client: PurviewClient, content_to_process_factory) -> None: + """Test send_content_activities success path.""" + request = ContentActivitiesRequest( + user_id="user-123", + tenant_id="tenant-456", + content_to_process=content_to_process_factory("hello"), + correlation_id="corr-1", + ) + + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.return_value = {"error": None} + + with patch.object(client._client, "post", return_value=mock_response): + resp = await client.send_content_activities(request) + + assert isinstance(resp, ContentActivitiesResponse) + + async def test_post_handles_invalid_json_response_body(self, client: PurviewClient) -> None: + """Test that invalid JSON bodies fall back to an empty dict.""" + request = ProcessContentRequest(user_id="user-123", tenant_id="tenant-456", content_to_process=[]) + request.correlation_id = "corr-123" + + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.side_effect = ValueError("not json") + + with patch.object(client._client, "post", return_value=mock_response): + result = await client._post("http://test", request, ContentActivitiesResponse, token="fake-token") + + assert isinstance(result, ContentActivitiesResponse) + + async def test_post_deserialization_failure_raises_purview_service_error(self, client: PurviewClient) -> None: + """Test that response deserialization errors are wrapped as PurviewServiceError.""" + + class BadResponseType: + @classmethod + def model_validate(cls, value): + raise RuntimeError("boom") + + request = ProcessContentRequest(user_id="user-123", tenant_id="tenant-456", content_to_process=[]) + request.correlation_id = "corr-123" + + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.return_value = {"any": "data"} + + with ( + patch.object(client._client, "post", return_value=mock_response), + pytest.raises(PurviewServiceError, match="Failed to deserialize Purview response"), + ): + await client._post("http://test", request, BadResponseType, token="fake-token") + async def test_client_close(self, mock_credential: AsyncMock, settings: PurviewSettings) -> None: """Test client properly closes HTTP client.""" client = PurviewClient(mock_credential, settings) diff --git a/python/packages/purview/tests/test_middleware.py b/python/packages/purview/tests/test_middleware.py index 9426bc66af..b973e8ea34 100644 --- a/python/packages/purview/tests/test_middleware.py +++ b/python/packages/purview/tests/test_middleware.py @@ -153,6 +153,92 @@ class TestPurviewPolicyMiddleware: for call in mock_process.call_args_list: assert call[0][1] == Activity.UPLOAD_TEXT + async def test_middleware_streaming_skips_post_check( + self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock + ) -> None: + """Test that streaming results skip post-check evaluation.""" + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello")]) + context.is_streaming = True + + with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: + + async def mock_next(ctx: AgentRunContext) -> None: + ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="streaming")]) + + await middleware.process(context, mock_next) + + assert mock_proc.call_count == 1 + + async def test_middleware_payment_required_in_pre_check_raises_by_default( + self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock + ) -> None: + """Test that 402 in pre-check is raised when ignore_payment_required=False.""" + from agent_framework_purview._exceptions import PurviewPaymentRequiredError + + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello")]) + + with patch.object( + middleware._processor, + "process_messages", + side_effect=PurviewPaymentRequiredError("Payment required"), + ): + + async def mock_next(_: AgentRunContext) -> None: + raise AssertionError("next should not be called") + + with pytest.raises(PurviewPaymentRequiredError): + await middleware.process(context, mock_next) + + async def test_middleware_payment_required_in_post_check_raises_by_default( + self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock + ) -> None: + """Test that 402 in post-check is raised when ignore_payment_required=False.""" + from agent_framework_purview._exceptions import PurviewPaymentRequiredError + + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello")]) + + call_count = 0 + + async def side_effect(*args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count == 1: + return (False, "user-123") + raise PurviewPaymentRequiredError("Payment required") + + with patch.object(middleware._processor, "process_messages", side_effect=side_effect): + + async def mock_next(ctx: AgentRunContext) -> None: + ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="OK")]) + + with pytest.raises(PurviewPaymentRequiredError): + await middleware.process(context, mock_next) + + async def test_middleware_post_check_exception_raises_when_ignore_exceptions_false( + self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock + ) -> None: + """Test that post-check exceptions are propagated when ignore_exceptions=False.""" + middleware._settings.ignore_exceptions = False + + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello")]) + + call_count = 0 + + async def side_effect(*args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count == 1: + return (False, "user-123") + raise ValueError("Post-check blew up") + + with patch.object(middleware._processor, "process_messages", side_effect=side_effect): + + async def mock_next(ctx: AgentRunContext) -> None: + ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="OK")]) + + with pytest.raises(ValueError, match="Post-check blew up"): + await middleware.process(context, mock_next) + async def test_middleware_handles_pre_check_exception( self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock ) -> None: diff --git a/python/packages/purview/tests/test_processor.py b/python/packages/purview/tests/test_processor.py index c517da2459..11f48ed199 100644 --- a/python/packages/purview/tests/test_processor.py +++ b/python/packages/purview/tests/test_processor.py @@ -242,6 +242,83 @@ class TestScopedContentProcessor: # The response should have id=204 (No Content) when no scopes apply assert response.id == "204" + async def test_process_with_scopes_ignores_unexpected_cached_value_type( + self, processor: ScopedContentProcessor, mock_client: AsyncMock, process_content_request_factory + ) -> None: + """Test that a corrupted cache entry does not crash processing.""" + from agent_framework_purview._models import ( + ExecutionMode, + PolicyLocation, + PolicyScope, + ProcessContentResponse, + ProtectionScopeActivities, + ProtectionScopesResponse, + ) + + request = process_content_request_factory() + + # Return a valid, inline scope so we stay on the normal (non-background) path. + scope_location = PolicyLocation(**{ + "@odata.type": "microsoft.graph.policyLocationApplication", + "value": "app-id", + }) + scope = PolicyScope(**{ + "activities": ProtectionScopeActivities.UPLOAD_TEXT, + "locations": [scope_location], + "execution_mode": ExecutionMode.EVALUATE_INLINE, + }) + mock_client.get_protection_scopes = AsyncMock(return_value=ProtectionScopesResponse(**{"value": [scope]})) + mock_client.process_content = AsyncMock( + return_value=ProcessContentResponse(**{"id": "ok", "protectionScopeState": "notModified"}) + ) + + # First cache read is the tenant payment key (None). Second is the scopes cache (corrupt value). + processor._cache.get = AsyncMock(side_effect=[None, "corrupt-value"]) # type: ignore[method-assign] + processor._cache.set = AsyncMock() # type: ignore[method-assign] + + response = await processor._process_with_scopes(request) + + assert response.id == "ok" + mock_client.get_protection_scopes.assert_called_once() + mock_client.process_content.assert_called_once() + + async def test_process_with_scopes_uses_tenant_payment_exception_cache( + self, processor: ScopedContentProcessor, mock_client: AsyncMock, process_content_request_factory + ) -> None: + """Test that a cached 402 exception short-circuits all subsequent requests for the tenant.""" + from agent_framework_purview._exceptions import PurviewPaymentRequiredError + + request = process_content_request_factory() + + processor._cache.get = AsyncMock(return_value=PurviewPaymentRequiredError("Payment required")) # type: ignore[method-assign] + + with pytest.raises(PurviewPaymentRequiredError): + await processor._process_with_scopes(request) + + mock_client.get_protection_scopes.assert_not_called() + + async def test_process_content_background_retries_on_modified_state( + self, processor: ScopedContentProcessor, mock_client: AsyncMock, process_content_request_factory + ) -> None: + """Test offline background processing invalidates cache and retries when scope state changes.""" + from agent_framework_purview._models import ProcessContentResponse + + request = process_content_request_factory() + request.scope_identifier = "etag-1" + + mock_client.process_content = AsyncMock( + side_effect=[ + ProcessContentResponse(**{"id": "r1", "protectionScopeState": "modified"}), + ProcessContentResponse(**{"id": "r2", "protectionScopeState": "notModified"}), + ] + ) + processor._cache.remove = AsyncMock() # type: ignore[method-assign] + + await processor._process_content_background(request, cache_key="purview:protection_scopes:abc") + + processor._cache.remove.assert_called_once_with("purview:protection_scopes:abc") + assert mock_client.process_content.call_count == 2 + async def test_map_messages_with_user_id_in_additional_properties(self, mock_client: AsyncMock) -> None: """Test user_id extraction from message additional_properties.""" settings = PurviewSettings( From 184ee9d5189fa203b89946a2d26e2d0bb507305b Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Sun, 1 Feb 2026 01:05:35 -0800 Subject: [PATCH 09/20] Fix AzureAIAgentClient dropping agent instructions in sequential workflows (#3563) In _prepare_options(), the 'instructions' key was excluded from run_options but never re-added. This caused instructions passed via as_agent(instructions=...) to be silently dropped, making agents in sequential workflows ignore their configured instructions. Fixes #3507 --- .../agent_framework_azure_ai/_chat_client.py | 4 ++ .../tests/test_azure_ai_agent_client.py | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+) 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 c54334aaef..540aacbca2 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 @@ -948,6 +948,10 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA if additional_messages: run_options["additional_messages"] = additional_messages + # Add instructions from options (agent's instructions set via as_agent()) + if options_instructions := options.get("instructions"): + instructions.append(options_instructions) + # Add instruction from existing agent at the beginning if ( agent_definition is not None 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 9e5e409db3..4366ea8141 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 @@ -467,6 +467,56 @@ async def test_azure_ai_chat_client_prepare_options_with_messages(mock_agents_cl assert len(run_options["additional_messages"]) == 1 # Only user message +async def test_azure_ai_chat_client_prepare_options_with_instructions_from_options( + mock_agents_client: MagicMock, +) -> None: + """Test _prepare_options includes instructions passed via options. + + This verifies that agent instructions set via as_agent(instructions=...) + are properly included in the API call. + """ + chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + mock_agents_client.get_agent = AsyncMock(return_value=None) + + messages = [ChatMessage(role=Role.USER, text="Hello")] + chat_options: ChatOptions = { + "instructions": "You are a thoughtful reviewer. Give brief feedback.", + } + + run_options, _ = await chat_client._prepare_options(messages, chat_options) # type: ignore + + assert "instructions" in run_options + assert "reviewer" in run_options["instructions"].lower() + + +async def test_azure_ai_chat_client_prepare_options_merges_instructions_from_messages_and_options( + mock_agents_client: MagicMock, +) -> None: + """Test _prepare_options merges instructions from both system messages and options. + + When instructions come from both system/developer messages AND from options, + both should be included in the final instructions. + """ + chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + mock_agents_client.get_agent = AsyncMock(return_value=None) + + messages = [ + ChatMessage(role=Role.SYSTEM, text="Context: You are reviewing marketing copy."), + ChatMessage(role=Role.USER, text="Review this tagline"), + ] + chat_options: ChatOptions = { + "instructions": "Be concise and constructive in your feedback.", + } + + run_options, _ = await chat_client._prepare_options(messages, chat_options) # type: ignore + + assert "instructions" in run_options + instructions_text = run_options["instructions"] + # Both instruction sources should be present + assert "marketing" in instructions_text.lower() + assert "concise" in instructions_text.lower() + + async def test_azure_ai_chat_client_inner_get_response(mock_agents_client: MagicMock) -> None: """Test _inner_get_response method.""" chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") From 98cd72839e4057d661a58092a3b013993264d834 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 2 Feb 2026 08:24:31 -0800 Subject: [PATCH 10/20] Update readme (#3576) * Updated README.md * More updates --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 64b0dbd821..e86cf94e60 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ async def main(): # api_version=os.environ["AZURE_OPENAI_API_VERSION"], # api_key=os.environ["AZURE_OPENAI_API_KEY"], # Optional if using AzureCliCredential credential=AzureCliCredential(), # Optional, if using api_key - ).create_agent( + ).as_agent( name="HaikuBot", instructions="You are an upbeat assistant that writes beautifully.", ) @@ -131,7 +131,7 @@ using OpenAI; // Replace the with your OpenAI API key. var agent = new OpenAIClient("") .GetOpenAIResponseClient("gpt-4o-mini") - .CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully."); + .AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully."); Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework.")); ``` @@ -150,7 +150,7 @@ var agent = new OpenAIClient( new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions() { Endpoint = new Uri("https://.openai.azure.com/openai/v1") }) .GetOpenAIResponseClient("gpt-4o-mini") - .CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully."); + .AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully."); Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework.")); ``` From 73033c300fcec6d90a9506a4a04daa655145c690 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 2 Feb 2026 21:40:35 -0800 Subject: [PATCH 11/20] Python: Updated instructions/system_message logic in GitHub Copilot agent (#3625) * Updated instructions handling * Small improvement * Included runtime options in session creation logic --- .../agent_framework_github_copilot/_agent.py | 83 +++++++++++++++---- .../tests/test_github_copilot_agent.py | 79 ++++++++++++++++-- .../github_copilot/github_copilot_basic.py | 45 ++++++++-- .../github_copilot_with_file_operations.py | 10 +-- .../github_copilot/github_copilot_with_mcp.py | 6 +- ...ithub_copilot_with_multiple_permissions.py | 10 +-- .../github_copilot_with_session.py | 18 ++-- .../github_copilot_with_shell.py | 10 +-- .../github_copilot/github_copilot_with_url.py | 10 +-- 9 files changed, 206 insertions(+), 65 deletions(-) diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 8bcfa9a5ba..90655ae055 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -31,6 +31,7 @@ from copilot.types import ( PermissionRequestResult, ResumeSessionConfig, SessionConfig, + SystemMessageConfig, ToolInvocation, ToolResult, ) @@ -57,8 +58,9 @@ logger = logging.getLogger("agent_framework.github_copilot") class GitHubCopilotOptions(TypedDict, total=False): """GitHub Copilot-specific options.""" - instructions: str - """System message to append to the session.""" + system_message: SystemMessageConfig + """System message configuration for the session. Use mode 'append' to add to the default + system prompt, or 'replace' to completely override it.""" cli_path: str """Path to the Copilot CLI executable. Defaults to GITHUB_COPILOT_CLI_PATH environment variable @@ -139,6 +141,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): def __init__( self, + instructions: str | None = None, *, client: CopilotClient | None = None, id: str | None = None, @@ -157,6 +160,9 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): ) -> None: """Initialize the GitHub Copilot Agent. + Args: + instructions: System message for the agent. + Keyword Args: client: Optional pre-configured CopilotClient instance. If not provided, a new client will be created using the other parameters. @@ -188,7 +194,10 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): # Parse options opts: dict[str, Any] = dict(default_options) if default_options else {} - instructions = opts.pop("instructions", None) + + # Handle instructions - direct parameter takes precedence over default_options.system_message + self._prepare_system_message(instructions, opts) + cli_path = opts.pop("cli_path", None) model = opts.pop("model", None) timeout = opts.pop("timeout", None) @@ -208,7 +217,6 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): except ValidationError as ex: raise ServiceInitializationError("Failed to create GitHub Copilot settings.", ex) from ex - self._instructions = instructions self._tools = normalize_tools(tools) self._permission_handler = on_permission_request self._mcp_servers = mcp_servers @@ -302,7 +310,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): opts: dict[str, Any] = dict(options) if options else {} timeout = opts.pop("timeout", None) or self._settings.timeout or DEFAULT_TIMEOUT_SECONDS - session = await self._get_or_create_session(thread, streaming=False) + session = await self._get_or_create_session(thread, streaming=False, runtime_options=opts) input_messages = normalize_messages(messages) prompt = "\n".join([message.text for message in input_messages]) @@ -365,7 +373,9 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): if not thread: thread = self.get_new_thread() - session = await self._get_or_create_session(thread, streaming=True) + opts: dict[str, Any] = dict(options) if options else {} + + session = await self._get_or_create_session(thread, streaming=True, runtime_options=opts) input_messages = normalize_messages(messages) prompt = "\n".join([message.text for message in input_messages]) @@ -400,6 +410,29 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): finally: unsubscribe() + @staticmethod + def _prepare_system_message( + instructions: str | None, + opts: dict[str, Any], + ) -> None: + """Prepare system message configuration in opts. + + If instructions is provided, it takes precedence for content. + If system_message is also provided, its mode is preserved. + Modifies opts in place. + + Args: + instructions: Direct instructions parameter for content. + opts: Options dictionary to modify. + """ + opts_system_message = opts.pop("system_message", None) + if instructions is not None: + # Use instructions for content, but preserve mode from system_message if provided + mode = opts_system_message.get("mode", "append") if opts_system_message else "append" + opts["system_message"] = {"mode": mode, "content": instructions} + elif opts_system_message is not None: + opts["system_message"] = opts_system_message + def _prepare_tools( self, tools: list[ToolProtocol | MutableMapping[str, Any]], @@ -459,12 +492,14 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): self, thread: AgentThread, streaming: bool = False, + runtime_options: dict[str, Any] | None = None, ) -> CopilotSession: """Get an existing session or create a new one for the thread. Args: thread: The conversation thread. streaming: Whether to enable streaming for the session. + runtime_options: Runtime options from run/run_stream that take precedence. Returns: A CopilotSession instance. @@ -479,33 +514,47 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): if thread.service_thread_id: return await self._resume_session(thread.service_thread_id, streaming) - session = await self._create_session(streaming) + session = await self._create_session(streaming, runtime_options) thread.service_thread_id = session.session_id return session except Exception as ex: raise ServiceException(f"Failed to create GitHub Copilot session: {ex}") from ex - async def _create_session(self, streaming: bool) -> CopilotSession: - """Create a new Copilot session.""" + async def _create_session( + self, + streaming: bool, + runtime_options: dict[str, Any] | None = None, + ) -> CopilotSession: + """Create a new Copilot session. + + Args: + streaming: Whether to enable streaming for the session. + runtime_options: Runtime options that take precedence over default_options. + """ if not self._client: raise ServiceException("GitHub Copilot client not initialized. Call start() first.") + opts = runtime_options or {} config: SessionConfig = {"streaming": streaming} - if self._settings.model: - config["model"] = self._settings.model # type: ignore[typeddict-item] + model = opts.get("model") or self._settings.model + if model: + config["model"] = model # type: ignore[typeddict-item] - if self._instructions: - config["system_message"] = {"mode": "append", "content": self._instructions} + system_message = opts.get("system_message") or self._default_options.get("system_message") + if system_message: + config["system_message"] = system_message if self._tools: config["tools"] = self._prepare_tools(self._tools) - if self._permission_handler: - config["on_permission_request"] = self._permission_handler + permission_handler = opts.get("on_permission_request") or self._permission_handler + if permission_handler: + config["on_permission_request"] = permission_handler - if self._mcp_servers: - config["mcp_servers"] = self._mcp_servers + mcp_servers = opts.get("mcp_servers") or self._mcp_servers + if mcp_servers: + config["mcp_servers"] = mcp_servers return await self._client.create_session(config) diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index f53aa0fe59..e68b58c243 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -135,12 +135,52 @@ class TestGitHubCopilotAgentInit: agent = GitHubCopilotAgent(tools=[my_tool]) assert len(agent._tools) == 1 # type: ignore - def test_init_with_instructions(self) -> None: - """Test initialization with custom instructions.""" + def test_init_with_instructions_parameter(self) -> None: + """Test initialization with instructions parameter.""" + agent = GitHubCopilotAgent(instructions="You are a helpful assistant.") + assert agent._default_options.get("system_message") == { # type: ignore + "mode": "append", + "content": "You are a helpful assistant.", + } + + def test_init_with_system_message_in_default_options(self) -> None: + """Test initialization with system_message object in default_options.""" agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={"instructions": "You are a helpful assistant."} + default_options={"system_message": {"mode": "append", "content": "You are a helpful assistant."}} ) - assert agent._instructions == "You are a helpful assistant." # type: ignore + assert agent._default_options.get("system_message") == { # type: ignore + "mode": "append", + "content": "You are a helpful assistant.", + } + + def test_init_with_system_message_replace_mode(self) -> None: + """Test initialization with system_message in replace mode.""" + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + default_options={"system_message": {"mode": "replace", "content": "Custom system prompt."}} + ) + assert agent._default_options.get("system_message") == { # type: ignore + "mode": "replace", + "content": "Custom system prompt.", + } + + def test_instructions_parameter_takes_precedence_for_content(self) -> None: + """Test that direct instructions parameter takes precedence for content but preserves mode.""" + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + instructions="Direct instructions", + default_options={"system_message": {"mode": "replace", "content": "Options system_message"}}, + ) + assert agent._default_options.get("system_message") == { # type: ignore + "mode": "replace", + "content": "Direct instructions", + } + + def test_instructions_parameter_defaults_to_append_mode(self) -> None: + """Test that instructions parameter defaults to append mode when no system_message provided.""" + agent = GitHubCopilotAgent(instructions="Direct instructions") + assert agent._default_options.get("system_message") == { # type: ignore + "mode": "append", + "content": "Direct instructions", + } class TestGitHubCopilotAgentLifecycle: @@ -462,10 +502,10 @@ class TestGitHubCopilotAgentSessionManagement: mock_client: MagicMock, mock_session: MagicMock, ) -> None: - """Test that session config includes instructions.""" - agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + """Test that session config includes instructions from direct parameter.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant.", client=mock_client, - default_options={"instructions": "You are a helpful assistant."}, ) await agent.start() @@ -476,6 +516,31 @@ class TestGitHubCopilotAgentSessionManagement: assert config["system_message"]["mode"] == "append" assert config["system_message"]["content"] == "You are a helpful assistant." + async def test_runtime_options_take_precedence_over_default( + self, + mock_client: MagicMock, + mock_session: MagicMock, + ) -> None: + """Test that runtime options from run() take precedence over default_options.""" + agent = GitHubCopilotAgent( + instructions="Default instructions", + client=mock_client, + ) + await agent.start() + + runtime_options: GitHubCopilotOptions = { + "system_message": {"mode": "replace", "content": "Runtime instructions"} + } + await agent._get_or_create_session( # type: ignore + AgentThread(), + runtime_options=runtime_options, + ) + + call_args = mock_client.create_session.call_args + config = call_args[0][0] + assert config["system_message"]["mode"] == "replace" + assert config["system_message"]["content"] == "Runtime instructions" + async def test_session_config_includes_streaming_flag( self, mock_client: MagicMock, diff --git a/python/samples/getting_started/agents/github_copilot/github_copilot_basic.py b/python/samples/getting_started/agents/github_copilot/github_copilot_basic.py index 826113aa2a..d23591eb02 100644 --- a/python/samples/getting_started/agents/github_copilot/github_copilot_basic.py +++ b/python/samples/getting_started/agents/github_copilot/github_copilot_basic.py @@ -18,7 +18,7 @@ from random import randint from typing import Annotated from agent_framework import tool -from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions +from agent_framework.github import GitHubCopilotAgent from pydantic import Field @@ -36,8 +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: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={"instructions": "You are a helpful weather agent."}, + agent = GitHubCopilotAgent( + instructions="You are a helpful weather agent.", tools=[get_weather], ) @@ -52,8 +52,8 @@ async def streaming_example() -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") - agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={"instructions": "You are a helpful weather agent."}, + agent = GitHubCopilotAgent( + instructions="You are a helpful weather agent.", tools=[get_weather], ) @@ -67,11 +67,46 @@ async def streaming_example() -> None: print("\n") +async def runtime_options_example() -> None: + """Example of overriding system message at runtime.""" + print("=== Runtime Options Example ===") + + agent = GitHubCopilotAgent( + instructions="Always respond in exactly 3 words.", + tools=[get_weather], + ) + + async with agent: + query = "What's the weather like in Paris?" + + # First call uses default instructions (3 words response) + print("Using default instructions (3 words):") + print(f"User: {query}") + result1 = await agent.run(query) + print(f"Agent: {result1}\n") + + # Second call overrides with runtime system_message in replace mode + print("Using runtime system_message with replace mode (detailed response):") + print(f"User: {query}") + result2 = await agent.run( + query, + options={ + "system_message": { + "mode": "replace", + "content": "You are a weather expert. Provide detailed weather information " + "with temperature, and recommendations.", + } + }, + ) + print(f"Agent: {result2}\n") + + async def main() -> None: print("=== Basic GitHub Copilot Agent Example ===") await non_streaming_example() await streaming_example() + await runtime_options_example() if __name__ == "__main__": diff --git a/python/samples/getting_started/agents/github_copilot/github_copilot_with_file_operations.py b/python/samples/getting_started/agents/github_copilot/github_copilot_with_file_operations.py index 70386f8cbd..b5a17262ec 100644 --- a/python/samples/getting_started/agents/github_copilot/github_copilot_with_file_operations.py +++ b/python/samples/getting_started/agents/github_copilot/github_copilot_with_file_operations.py @@ -14,7 +14,7 @@ SECURITY NOTE: Only enable file permissions when you trust the agent's actions. import asyncio -from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions +from agent_framework.github import GitHubCopilotAgent from copilot.types import PermissionRequest, PermissionRequestResult @@ -35,11 +35,9 @@ def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> Pe async def main() -> None: print("=== GitHub Copilot Agent with File Operation Permissions ===\n") - agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={ - "instructions": "You are a helpful assistant that can read and write files.", - "on_permission_request": prompt_permission, - }, + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant that can read and write files.", + default_options={"on_permission_request": prompt_permission}, ) async with agent: diff --git a/python/samples/getting_started/agents/github_copilot/github_copilot_with_mcp.py b/python/samples/getting_started/agents/github_copilot/github_copilot_with_mcp.py index 6f940b4f91..61e9959793 100644 --- a/python/samples/getting_started/agents/github_copilot/github_copilot_with_mcp.py +++ b/python/samples/getting_started/agents/github_copilot/github_copilot_with_mcp.py @@ -14,7 +14,7 @@ of MCP-related actions. import asyncio -from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions +from agent_framework.github import GitHubCopilotAgent from copilot.types import MCPServerConfig, PermissionRequest, PermissionRequestResult @@ -49,9 +49,9 @@ async def main() -> None: }, } - agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant with access to the local filesystem and Microsoft Learn.", default_options={ - "instructions": "You are a helpful assistant with access to the local filesystem and Microsoft Learn.", "on_permission_request": prompt_permission, "mcp_servers": mcp_servers, }, diff --git a/python/samples/getting_started/agents/github_copilot/github_copilot_with_multiple_permissions.py b/python/samples/getting_started/agents/github_copilot/github_copilot_with_multiple_permissions.py index 4b6e8948d4..8ecc26ab01 100644 --- a/python/samples/getting_started/agents/github_copilot/github_copilot_with_multiple_permissions.py +++ b/python/samples/getting_started/agents/github_copilot/github_copilot_with_multiple_permissions.py @@ -20,7 +20,7 @@ More permissions mean more potential for unintended actions. import asyncio -from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions +from agent_framework.github import GitHubCopilotAgent from copilot.types import PermissionRequest, PermissionRequestResult @@ -43,11 +43,9 @@ def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> Pe async def main() -> None: print("=== GitHub Copilot Agent with Multiple Permissions ===\n") - agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={ - "instructions": "You are a helpful development assistant that can read, write files and run commands.", - "on_permission_request": prompt_permission, - }, + agent = GitHubCopilotAgent( + instructions="You are a helpful development assistant that can read, write files and run commands.", + default_options={"on_permission_request": prompt_permission}, ) async with agent: diff --git a/python/samples/getting_started/agents/github_copilot/github_copilot_with_session.py b/python/samples/getting_started/agents/github_copilot/github_copilot_with_session.py index 1150091022..fa1c2e4640 100644 --- a/python/samples/getting_started/agents/github_copilot/github_copilot_with_session.py +++ b/python/samples/getting_started/agents/github_copilot/github_copilot_with_session.py @@ -13,7 +13,7 @@ from random import randint from typing import Annotated from agent_framework import tool -from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions +from agent_framework.github import GitHubCopilotAgent from pydantic import Field @@ -31,8 +31,8 @@ async def example_with_automatic_session_creation() -> None: """Each run() without thread creates a new session.""" print("=== Automatic Session Creation Example ===") - agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={"instructions": "You are a helpful weather agent."}, + agent = GitHubCopilotAgent( + instructions="You are a helpful weather agent.", tools=[get_weather], ) @@ -55,8 +55,8 @@ async def example_with_session_persistence() -> None: """Reuse session via thread object for multi-turn conversations.""" print("=== Session Persistence Example ===") - agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={"instructions": "You are a helpful weather agent."}, + agent = GitHubCopilotAgent( + instructions="You are a helpful weather agent.", tools=[get_weather], ) @@ -91,8 +91,8 @@ async def example_with_existing_session_id() -> None: existing_session_id = None # First agent instance - start a conversation - agent1: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={"instructions": "You are a helpful weather agent."}, + agent1 = GitHubCopilotAgent( + instructions="You are a helpful weather agent.", tools=[get_weather], ) @@ -112,8 +112,8 @@ async def example_with_existing_session_id() -> None: print("\n--- Continuing with the same session ID in a new agent instance ---") # Second agent instance - resume the conversation - agent2: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={"instructions": "You are a helpful weather agent."}, + agent2 = GitHubCopilotAgent( + instructions="You are a helpful weather agent.", tools=[get_weather], ) diff --git a/python/samples/getting_started/agents/github_copilot/github_copilot_with_shell.py b/python/samples/getting_started/agents/github_copilot/github_copilot_with_shell.py index dae7302c6e..f5e00aedca 100644 --- a/python/samples/getting_started/agents/github_copilot/github_copilot_with_shell.py +++ b/python/samples/getting_started/agents/github_copilot/github_copilot_with_shell.py @@ -13,7 +13,7 @@ Shell commands have full access to your system within the permissions of the run import asyncio -from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions +from agent_framework.github import GitHubCopilotAgent from copilot.types import PermissionRequest, PermissionRequestResult @@ -34,11 +34,9 @@ def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> Pe async def main() -> None: print("=== GitHub Copilot Agent with Shell Permissions ===\n") - agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={ - "instructions": "You are a helpful assistant that can execute shell commands.", - "on_permission_request": prompt_permission, - }, + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant that can execute shell commands.", + default_options={"on_permission_request": prompt_permission}, ) async with agent: diff --git a/python/samples/getting_started/agents/github_copilot/github_copilot_with_url.py b/python/samples/getting_started/agents/github_copilot/github_copilot_with_url.py index 89d5ffbf58..4c46017468 100644 --- a/python/samples/getting_started/agents/github_copilot/github_copilot_with_url.py +++ b/python/samples/getting_started/agents/github_copilot/github_copilot_with_url.py @@ -13,7 +13,7 @@ URL fetching allows the agent to access any URL accessible from your network. import asyncio -from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions +from agent_framework.github import GitHubCopilotAgent from copilot.types import PermissionRequest, PermissionRequestResult @@ -34,11 +34,9 @@ def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> Pe async def main() -> None: print("=== GitHub Copilot Agent with URL Fetching ===\n") - agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={ - "instructions": "You are a helpful assistant that can fetch and summarize web content.", - "on_permission_request": prompt_permission, - }, + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant that can fetch and summarize web content.", + default_options={"on_permission_request": prompt_permission}, ) async with agent: From 405bd6fb4b95ed9e3d5d603d5aac6322eea9ba7c Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:15:50 -0800 Subject: [PATCH 12/20] Python: Filter response_format from MCP tool call kwargs (#3494) * Filter response_format from MCP tool call kwargs * added tests --- python/packages/core/agent_framework/_mcp.py | 4 +- python/packages/core/tests/core/test_mcp.py | 194 +++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index 51116b71ae..9410a6698b 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -756,10 +756,12 @@ class MCPTool: # that should not be forwarded to external MCP servers. # conversation_id is an internal tracking ID used by services like Azure AI. # options contains metadata/store used by AG-UI for Azure AI client requirements. + # response_format is a Pydantic model class used for structured output (not serializable). filtered_kwargs = { k: v for k, v in kwargs.items() - if k not in {"chat_options", "tools", "tool_choice", "thread", "conversation_id", "options"} + if k + not in {"chat_options", "tools", "tool_choice", "thread", "conversation_id", "options", "response_format"} } # Try the operation, reconnecting once if the connection is closed diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 67bf94acaf..f6d2b535d8 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. # type: ignore[reportPrivateUsage] +import logging import os from contextlib import _AsyncGeneratorContextManager # type: ignore from typing import Any @@ -30,6 +31,7 @@ from agent_framework._mcp import ( _parse_message_from_mcp, _prepare_content_for_mcp, _prepare_message_for_mcp, + logger, ) from agent_framework.exceptions import ToolException, ToolExecutionException @@ -2514,3 +2516,195 @@ async def test_mcp_tool_safe_close_handles_cancelled_error(): # Verify aclose was called mock_exit_stack.aclose.assert_called_once() + + +async def test_connect_sets_logging_level_when_logger_level_is_set(): + """Test that connect() sets the MCP server logging level when the logger level is not NOTSET.""" + + tool = MCPStdioTool( + name="test_server", + command="test_command", + args=["arg1"], + load_tools=False, + load_prompts=False, + ) + + # Mock the transport and session + mock_transport = (Mock(), Mock()) + mock_context = AsyncMock() + mock_context.__aenter__ = AsyncMock(return_value=mock_transport) + mock_context.__aexit__ = AsyncMock() + + mock_session = Mock() + mock_session._request_id = 1 + mock_session.initialize = AsyncMock() + mock_session.set_logging_level = AsyncMock() + + mock_session_context = AsyncMock() + mock_session_context.__aenter__ = AsyncMock(return_value=mock_session) + mock_session_context.__aexit__ = AsyncMock() + + with ( + patch.object(tool, "get_mcp_client", return_value=mock_context), + patch("agent_framework._mcp.ClientSession", return_value=mock_session_context), + patch.object(logger, "level", logging.DEBUG), # Set logger level to DEBUG + ): + await tool.connect() + + # Verify set_logging_level was called with "debug" + mock_session.set_logging_level.assert_called_once_with("debug") + + +async def test_connect_does_not_set_logging_level_when_logger_level_is_notset(): + """Test that connect() does not set logging level when logger level is NOTSET.""" + + tool = MCPStdioTool( + name="test_server", + command="test_command", + args=["arg1"], + load_tools=False, + load_prompts=False, + ) + + # Mock the transport and session + mock_transport = (Mock(), Mock()) + mock_context = AsyncMock() + mock_context.__aenter__ = AsyncMock(return_value=mock_transport) + mock_context.__aexit__ = AsyncMock() + + mock_session = Mock() + mock_session._request_id = 1 + mock_session.initialize = AsyncMock() + mock_session.set_logging_level = AsyncMock() + + mock_session_context = AsyncMock() + mock_session_context.__aenter__ = AsyncMock(return_value=mock_session) + mock_session_context.__aexit__ = AsyncMock() + + with ( + patch.object(tool, "get_mcp_client", return_value=mock_context), + patch("agent_framework._mcp.ClientSession", return_value=mock_session_context), + patch.object(logger, "level", logging.NOTSET), # Set logger level to NOTSET + ): + await tool.connect() + + # Verify set_logging_level was NOT called + mock_session.set_logging_level.assert_not_called() + + +async def test_connect_handles_set_logging_level_exception(): + """Test that connect() handles exceptions from set_logging_level gracefully.""" + + tool = MCPStdioTool( + name="test_server", + command="test_command", + args=["arg1"], + load_tools=False, + load_prompts=False, + ) + + # Mock the transport and session + mock_transport = (Mock(), Mock()) + mock_context = AsyncMock() + mock_context.__aenter__ = AsyncMock(return_value=mock_transport) + mock_context.__aexit__ = AsyncMock() + + mock_session = Mock() + mock_session._request_id = 1 + mock_session.initialize = AsyncMock() + # Make set_logging_level raise an exception + mock_session.set_logging_level = AsyncMock(side_effect=RuntimeError("Server doesn't support logging level")) + + mock_session_context = AsyncMock() + mock_session_context.__aenter__ = AsyncMock(return_value=mock_session) + mock_session_context.__aexit__ = AsyncMock() + + with ( + patch.object(tool, "get_mcp_client", return_value=mock_context), + patch("agent_framework._mcp.ClientSession", return_value=mock_session_context), + patch.object(logger, "level", logging.INFO), # Set logger level to INFO + patch.object(logger, "warning") as mock_warning, + ): + # Should NOT raise - the exception should be caught and logged + await tool.connect() + + # Verify set_logging_level was called + mock_session.set_logging_level.assert_called_once_with("info") + + # Verify warning was logged + mock_warning.assert_called_once() + call_args = mock_warning.call_args + assert "Failed to set log level" in call_args[0][0] + + +async def test_mcp_tool_filters_framework_kwargs(): + """Test that call_tool filters out framework-specific kwargs before calling MCP session. + + This verifies that non-serializable kwargs like response_format (Pydantic model class), + chat_options, tools, tool_choice, thread, conversation_id, and options are filtered out + before being passed to the external MCP server. + """ + + class TestServer(MCPTool): + async def connect(self): + self.session = Mock(spec=ClientSession) + self.session.list_tools = AsyncMock( + return_value=types.ListToolsResult( + tools=[ + types.Tool( + name="test_tool", + description="Test tool", + inputSchema={ + "type": "object", + "properties": {"param": {"type": "string"}}, + "required": ["param"], + }, + ) + ] + ) + ) + # Mock call_tool to capture the arguments it receives + self.session.call_tool = AsyncMock( + return_value=types.CallToolResult(content=[types.TextContent(type="text", text="Success")]) + ) + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + return None + + # Create a mock Pydantic model class to use as response_format + class MockResponseFormat(BaseModel): + result: str + + server = TestServer(name="test_server") + async with server: + await server.load_tools() + func = server.functions[0] + + # Invoke the tool with framework kwargs that should be filtered out + await func.invoke( + param="test_value", + response_format=MockResponseFormat, # Should be filtered + chat_options={"some": "option"}, # Should be filtered + tools=[Mock()], # Should be filtered + tool_choice="auto", # Should be filtered + thread=Mock(), # Should be filtered + conversation_id="conv-123", # Should be filtered + options={"metadata": "value"}, # Should be filtered + ) + + # Verify call_tool was called with only the valid argument + server.session.call_tool.assert_called_once() + call_args = server.session.call_tool.call_args + + # Check that the arguments dict only contains 'param' and none of the framework kwargs + arguments = call_args.kwargs.get("arguments", call_args[1] if len(call_args) > 1 else {}) + assert arguments == {"param": "test_value"}, f"Expected only 'param' but got: {arguments}" + + # Explicitly verify that framework kwargs were NOT passed + assert "response_format" not in arguments + assert "chat_options" not in arguments + assert "tools" not in arguments + assert "tool_choice" not in arguments + assert "thread" not in arguments + assert "conversation_id" not in arguments + assert "options" not in arguments From 6fdf6111e6269c44fb7b1ca5d4410d876b464ac4 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:33:50 +0000 Subject: [PATCH 13/20] .NET: [BREAKING] Rename GetNewSession to CreateSession (#3501) * Rename GetNewSession to CreateSession * Address copilot feedback * Suppress warning * Suppress warning * Fix further warnings. --- .../A2AClientServer/A2AClient/Program.cs | 2 +- .../AGUIClientServer/AGUIClient/Program.cs | 2 +- .../AzureFunctions/01_SingleAgent/Program.cs | 2 ++ .../FunctionTriggers.cs | 2 +- .../02_AgentOrchestration_Chaining/Program.cs | 2 ++ .../Program.cs | 2 ++ .../FunctionTriggers.cs | 4 +-- .../Program.cs | 2 ++ .../FunctionTriggers.cs | 2 +- .../05_AgentOrchestration_HITL/Program.cs | 2 ++ .../06_LongRunningTools/FunctionTriggers.cs | 2 +- .../06_LongRunningTools/Program.cs | 2 ++ .../07_AgentAsMcpTool/Program.cs | 2 ++ .../08_ReliableStreaming/FunctionTriggers.cs | 2 +- .../08_ReliableStreaming/Program.cs | 2 ++ .../ConsoleApps/01_SingleAgent/Program.cs | 2 +- .../02_AgentOrchestration_Chaining/Program.cs | 2 +- .../Program.cs | 4 +-- .../05_AgentOrchestration_HITL/Program.cs | 2 +- .../06_LongRunningTools/Program.cs | 4 +-- .../07_ReliableStreaming/Program.cs | 2 +- .../Program.cs | 2 +- .../Step01_GettingStarted/Client/Program.cs | 2 +- .../Step02_BackendTools/Client/Program.cs | 2 +- .../Step03_FrontendTools/Client/Program.cs | 2 +- .../Step05_StateManagement/Client/Program.cs | 2 +- .../AgentOpenTelemetry/Program.cs | 2 +- .../Program.cs | 2 +- .../Agent_With_AzureAIProject/Program.cs | 2 +- .../Program.cs | 6 ++-- .../Agent_With_OpenAIAssistants/Program.cs | 2 +- .../Program.cs | 4 +-- .../Program.cs | 4 +-- .../Program.cs | 4 +-- .../Program.cs | 4 +-- .../Program.cs | 2 +- .../README.md | 4 +-- .../Program.cs | 2 +- .../Program.cs | 2 +- .../Program.cs | 2 +- .../Program.cs | 2 +- .../Program.cs | 4 +-- .../Program.cs | 2 +- .../Program.cs | 2 +- .../Program.cs | 2 +- .../Program.cs | 2 +- .../Agent_Step11_UsingImages/Program.cs | 2 +- .../Program.cs | 2 +- .../Agents/Agent_Step14_Middleware/Program.cs | 2 +- .../Agent_Step16_ChatReduction/Program.cs | 2 +- .../Program.cs | 4 +-- .../Agent_Step18_DeepResearch/Program.cs | 2 +- .../Program.cs | 2 +- .../Program.cs | 4 +-- .../README.md | 2 +- .../Program.cs | 4 +-- .../Program.cs | 2 +- .../Program.cs | 2 +- .../Program.cs | 4 +-- .../Program.cs | 2 +- .../Program.cs | 2 +- .../Program.cs | 2 +- .../Program.cs | 2 +- .../FoundryAgents_Step13_Plugins/Program.cs | 2 +- .../Program.cs | 2 +- .../FoundryAgent_Hosted_MCP/Program.cs | 4 +-- .../ResponseAgent_Hosted_MCP/Program.cs | 4 +-- .../Agents/CustomAgentExecutors/Program.cs | 4 +-- .../Agents/WorkflowAsAnAgent/Program.cs | 2 +- .../Declarative/HostedWorkflow/Program.cs | 2 +- .../WorkflowAsAnAgent/Program.cs | 2 +- .../samples/M365Agent/AFAgentApplication.cs | 2 +- .../src/Microsoft.Agents.AI.A2A/A2AAgent.cs | 6 ++-- .../AIAgent.cs | 2 +- .../AgentSession.cs | 4 +-- .../DelegatingAIAgent.cs | 2 +- .../CopilotStudioAgent.cs | 8 +++--- .../AgentEntity.cs | 2 +- .../DurableAIAgent.cs | 6 ++-- .../DurableAIAgentProxy.cs | 6 ++-- .../GitHubCopilotAgent.cs | 6 ++-- .../README.md | 4 +-- .../Local/InMemoryAgentSessionStore.cs | 2 +- .../NoopAgentSessionStore.cs | 2 +- .../PurviewAgent.cs | 4 +-- .../Specialized/AIAgentHostExecutor.cs | 2 +- .../WorkflowHostAgent.cs | 4 +-- .../ChatClient/ChatClientAgent.cs | 10 +++---- .../ChatClientAgentRunStreamingTests.cs | 4 +-- .../ChatClientAgentRunTests.cs | 4 +-- .../RunStreamingTests.cs | 10 +++---- .../RunTests.cs | 10 +++---- .../A2AAgentTests.cs | 28 +++++++++---------- .../AGUIChatClientTests.cs | 6 ++-- .../AIAgentTests.cs | 2 +- .../DelegatingAIAgentTests.cs | 10 +++---- .../AnthropicBetaServiceExtensionsTests.cs | 2 +- .../AnthropicClientExtensionsTests.cs | 2 +- .../AzureAIProjectChatClientTests.cs | 8 +++--- .../AggregatorPromptAgentFactoryTests.cs | 2 +- .../AgentEntityTests.cs | 6 ++-- .../ExternalClientTests.cs | 6 ++-- .../TimeToLiveTests.cs | 4 +-- .../GitHubCopilotAgentTests.cs | 6 ++-- .../GitHubCopilotAgentTests.cs | 8 +++--- .../AIAgentExtensionsTests.cs | 4 +-- .../BasicStreamingTests.cs | 16 +++++------ .../ForwardedPropertiesTests.cs | 2 +- .../SharedStateTests.cs | 16 +++++------ .../ToolCallingTests.cs | 18 ++++++------ ...AGUIEndpointRouteBuilderExtensionsTests.cs | 4 +-- .../TestAgent.cs | 2 +- .../AgentExtensionsTests.cs | 2 +- .../ChatClient/ChatClientAgentTests.cs | 12 ++++---- ...tClientAgent_ChatHistoryManagementTests.cs | 10 +++---- ... => ChatClientAgent_CreateSessionTests.cs} | 20 ++++++------- ...atClientAgent_RunWithCustomOptionsTests.cs | 24 ++++++++-------- .../TestAIAgent.cs | 8 +++--- .../AgentWorkflowBuilderTests.cs | 2 +- .../InProcessExecutionTests.cs | 2 +- .../RepresentationTests.cs | 2 +- .../RoleCheckAgent.cs | 2 +- .../Sample/06_GroupChat_Workflow.cs | 2 +- .../07_GroupChat_Workflow_HostAsAgent.cs | 2 +- .../Sample/10_Sequential_HostAsAgent.cs | 2 +- .../Sample/11_Concurrent_HostAsAgent.cs | 2 +- .../Sample/12_HandOff_HostAsAgent.cs | 2 +- .../Sample/13_Subworkflow_Checkpointing.cs | 2 +- .../TestEchoAgent.cs | 4 +-- .../TestReplayAgent.cs | 2 +- .../TestRequestAgent.cs | 4 +-- .../WorkflowHostSmokeTests.cs | 2 +- 132 files changed, 277 insertions(+), 261 deletions(-) rename dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/{ChatClientAgent_GetNewSessionTests.cs => ChatClientAgent_CreateSessionTests.cs} (83%) diff --git a/dotnet/samples/A2AClientServer/A2AClient/Program.cs b/dotnet/samples/A2AClientServer/A2AClient/Program.cs index bdf942a0cc..0b9696e3a1 100644 --- a/dotnet/samples/A2AClientServer/A2AClient/Program.cs +++ b/dotnet/samples/A2AClientServer/A2AClient/Program.cs @@ -42,7 +42,7 @@ public static class Program // Create the Host agent var hostAgent = new HostClientAgent(loggerFactory); await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";")); - AgentSession session = await hostAgent.Agent!.GetNewSessionAsync(cancellationToken); + AgentSession session = await hostAgent.Agent!.CreateSessionAsync(cancellationToken); try { while (true) diff --git a/dotnet/samples/AGUIClientServer/AGUIClient/Program.cs b/dotnet/samples/AGUIClientServer/AGUIClient/Program.cs index e6be9472da..1e5b6d6fee 100644 --- a/dotnet/samples/AGUIClientServer/AGUIClient/Program.cs +++ b/dotnet/samples/AGUIClientServer/AGUIClient/Program.cs @@ -88,7 +88,7 @@ public static class Program description: "AG-UI Client Agent", tools: [changeBackground, readClientClimateSensors]); - AgentSession session = await agent.GetNewSessionAsync(cancellationToken); + AgentSession session = await agent.CreateSessionAsync(cancellationToken); List messages = [new(ChatRole.System, "You are a helpful assistant.")]; try { diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/01_SingleAgent/Program.cs b/dotnet/samples/Durable/Agents/AzureFunctions/01_SingleAgent/Program.cs index 609ba162c9..e629f3ee2c 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/01_SingleAgent/Program.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/01_SingleAgent/Program.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0002 // Simplify Member Access + using Azure; using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/02_AgentOrchestration_Chaining/FunctionTriggers.cs b/dotnet/samples/Durable/Agents/AzureFunctions/02_AgentOrchestration_Chaining/FunctionTriggers.cs index 02f8cceced..7f67b8a6df 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/02_AgentOrchestration_Chaining/FunctionTriggers.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/02_AgentOrchestration_Chaining/FunctionTriggers.cs @@ -19,7 +19,7 @@ public static class FunctionTriggers public static async Task RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context) { DurableAIAgent writer = context.GetAgent("WriterAgent"); - AgentSession writerSession = await writer.GetNewSessionAsync(); + AgentSession writerSession = await writer.CreateSessionAsync(); AgentResponse initial = await writer.RunAsync( message: "Write a concise inspirational sentence about learning.", diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/02_AgentOrchestration_Chaining/Program.cs b/dotnet/samples/Durable/Agents/AzureFunctions/02_AgentOrchestration_Chaining/Program.cs index d906714c5c..7ab6a23477 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/02_AgentOrchestration_Chaining/Program.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/02_AgentOrchestration_Chaining/Program.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0002 // Simplify Member Access + using Azure; using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/03_AgentOrchestration_Concurrency/Program.cs b/dotnet/samples/Durable/Agents/AzureFunctions/03_AgentOrchestration_Concurrency/Program.cs index b180c8139f..621e093c67 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/03_AgentOrchestration_Concurrency/Program.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/03_AgentOrchestration_Concurrency/Program.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0002 // Simplify Member Access + using Azure; using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/04_AgentOrchestration_Conditionals/FunctionTriggers.cs b/dotnet/samples/Durable/Agents/AzureFunctions/04_AgentOrchestration_Conditionals/FunctionTriggers.cs index fcb0952394..868c2be487 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/04_AgentOrchestration_Conditionals/FunctionTriggers.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/04_AgentOrchestration_Conditionals/FunctionTriggers.cs @@ -21,7 +21,7 @@ public static class FunctionTriggers // Get the spam detection agent DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent"); - AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync(); + AgentSession spamSession = await spamDetectionAgent.CreateSessionAsync(); // Step 1: Check if the email is spam AgentResponse spamDetectionResponse = await spamDetectionAgent.RunAsync( @@ -43,7 +43,7 @@ public static class FunctionTriggers // Generate and send response for legitimate email DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent"); - AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync(); + AgentSession emailSession = await emailAssistantAgent.CreateSessionAsync(); AgentResponse emailAssistantResponse = await emailAssistantAgent.RunAsync( message: diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/04_AgentOrchestration_Conditionals/Program.cs b/dotnet/samples/Durable/Agents/AzureFunctions/04_AgentOrchestration_Conditionals/Program.cs index 07dcd302cc..b6638edf04 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/04_AgentOrchestration_Conditionals/Program.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/04_AgentOrchestration_Conditionals/Program.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0002 // Simplify Member Access + using Azure; using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/05_AgentOrchestration_HITL/FunctionTriggers.cs b/dotnet/samples/Durable/Agents/AzureFunctions/05_AgentOrchestration_HITL/FunctionTriggers.cs index b5cd6c43db..46282eec59 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/05_AgentOrchestration_HITL/FunctionTriggers.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/05_AgentOrchestration_HITL/FunctionTriggers.cs @@ -24,7 +24,7 @@ public static class FunctionTriggers // Get the writer agent DurableAIAgent writerAgent = context.GetAgent("WriterAgent"); - AgentSession writerSession = await writerAgent.GetNewSessionAsync(); + AgentSession writerSession = await writerAgent.CreateSessionAsync(); // Set initial status context.SetCustomStatus($"Starting content generation for topic: {input.Topic}"); diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/05_AgentOrchestration_HITL/Program.cs b/dotnet/samples/Durable/Agents/AzureFunctions/05_AgentOrchestration_HITL/Program.cs index 77e2dfa2d4..284a6af3ba 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/05_AgentOrchestration_HITL/Program.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/05_AgentOrchestration_HITL/Program.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0002 // Simplify Member Access + using Azure; using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/06_LongRunningTools/FunctionTriggers.cs b/dotnet/samples/Durable/Agents/AzureFunctions/06_LongRunningTools/FunctionTriggers.cs index 3f85749880..ed66be8bdd 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/06_LongRunningTools/FunctionTriggers.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/06_LongRunningTools/FunctionTriggers.cs @@ -20,7 +20,7 @@ public static class FunctionTriggers // Get the writer agent DurableAIAgent writerAgent = context.GetAgent("Writer"); - AgentSession writerSession = await writerAgent.GetNewSessionAsync(); + AgentSession writerSession = await writerAgent.CreateSessionAsync(); // Set initial status context.SetCustomStatus($"Starting content generation for topic: {input.Topic}"); diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/06_LongRunningTools/Program.cs b/dotnet/samples/Durable/Agents/AzureFunctions/06_LongRunningTools/Program.cs index e4d88d3ae7..149e020614 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/06_LongRunningTools/Program.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/06_LongRunningTools/Program.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0002 // Simplify Member Access + using Azure; using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/07_AgentAsMcpTool/Program.cs b/dotnet/samples/Durable/Agents/AzureFunctions/07_AgentAsMcpTool/Program.cs index 2503037a8c..3625eaa9eb 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/07_AgentAsMcpTool/Program.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/07_AgentAsMcpTool/Program.cs @@ -5,6 +5,8 @@ // generate a remote MCP endpoint for the app at /runtime/webhooks/mcp with a agent-specific // query tool name. +#pragma warning disable IDE0002 // Simplify Member Access + using Azure; using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/08_ReliableStreaming/FunctionTriggers.cs b/dotnet/samples/Durable/Agents/AzureFunctions/08_ReliableStreaming/FunctionTriggers.cs index f4fb251726..8ae1ee348e 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/08_ReliableStreaming/FunctionTriggers.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/08_ReliableStreaming/FunctionTriggers.cs @@ -95,7 +95,7 @@ public sealed class FunctionTriggers AIAgent agentProxy = durableClient.AsDurableAgentProxy(context, "TravelPlanner"); // Create a new agent session - AgentSession session = await agentProxy.GetNewSessionAsync(cancellationToken); + AgentSession session = await agentProxy.CreateSessionAsync(cancellationToken); string agentSessionId = session.GetService().ToString(); this._logger.LogInformation("Creating new agent session: {AgentSessionId}", agentSessionId); diff --git a/dotnet/samples/Durable/Agents/AzureFunctions/08_ReliableStreaming/Program.cs b/dotnet/samples/Durable/Agents/AzureFunctions/08_ReliableStreaming/Program.cs index c279b968a3..dd90af2287 100644 --- a/dotnet/samples/Durable/Agents/AzureFunctions/08_ReliableStreaming/Program.cs +++ b/dotnet/samples/Durable/Agents/AzureFunctions/08_ReliableStreaming/Program.cs @@ -8,6 +8,8 @@ // This pattern is inspired by OpenAI's background mode for the Responses API, which allows clients // to disconnect and reconnect to ongoing agent responses without losing messages. +#pragma warning disable IDE0002 // Simplify Member Access + using Azure; using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/Durable/Agents/ConsoleApps/01_SingleAgent/Program.cs b/dotnet/samples/Durable/Agents/ConsoleApps/01_SingleAgent/Program.cs index 8fa004470c..188d29ea46 100644 --- a/dotnet/samples/Durable/Agents/ConsoleApps/01_SingleAgent/Program.cs +++ b/dotnet/samples/Durable/Agents/ConsoleApps/01_SingleAgent/Program.cs @@ -61,7 +61,7 @@ Console.WriteLine("Enter a message for the Joker agent (or 'exit' to quit):"); Console.WriteLine(); // Create a session for the conversation -AgentSession session = await agentProxy.GetNewSessionAsync(); +AgentSession session = await agentProxy.CreateSessionAsync(); while (true) { diff --git a/dotnet/samples/Durable/Agents/ConsoleApps/02_AgentOrchestration_Chaining/Program.cs b/dotnet/samples/Durable/Agents/ConsoleApps/02_AgentOrchestration_Chaining/Program.cs index e7b736a068..91b9d2da67 100644 --- a/dotnet/samples/Durable/Agents/ConsoleApps/02_AgentOrchestration_Chaining/Program.cs +++ b/dotnet/samples/Durable/Agents/ConsoleApps/02_AgentOrchestration_Chaining/Program.cs @@ -47,7 +47,7 @@ AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterInstr static async Task RunOrchestratorAsync(TaskOrchestrationContext context) { DurableAIAgent writer = context.GetAgent("WriterAgent"); - AgentSession writerSession = await writer.GetNewSessionAsync(); + AgentSession writerSession = await writer.CreateSessionAsync(); AgentResponse initial = await writer.RunAsync( message: "Write a concise inspirational sentence about learning.", diff --git a/dotnet/samples/Durable/Agents/ConsoleApps/04_AgentOrchestration_Conditionals/Program.cs b/dotnet/samples/Durable/Agents/ConsoleApps/04_AgentOrchestration_Conditionals/Program.cs index e9e3fca3a1..e4062779f6 100644 --- a/dotnet/samples/Durable/Agents/ConsoleApps/04_AgentOrchestration_Conditionals/Program.cs +++ b/dotnet/samples/Durable/Agents/ConsoleApps/04_AgentOrchestration_Conditionals/Program.cs @@ -56,7 +56,7 @@ static async Task RunOrchestratorAsync(TaskOrchestrationContext context, { // Get the spam detection agent DurableAIAgent spamDetectionAgent = context.GetAgent(SpamDetectionAgentName); - AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync(); + AgentSession spamSession = await spamDetectionAgent.CreateSessionAsync(); // Step 1: Check if the email is spam AgentResponse spamDetectionResponse = await spamDetectionAgent.RunAsync( @@ -78,7 +78,7 @@ static async Task RunOrchestratorAsync(TaskOrchestrationContext context, // Generate and send response for legitimate email DurableAIAgent emailAssistantAgent = context.GetAgent(EmailAssistantAgentName); - AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync(); + AgentSession emailSession = await emailAssistantAgent.CreateSessionAsync(); AgentResponse emailAssistantResponse = await emailAssistantAgent.RunAsync( message: diff --git a/dotnet/samples/Durable/Agents/ConsoleApps/05_AgentOrchestration_HITL/Program.cs b/dotnet/samples/Durable/Agents/ConsoleApps/05_AgentOrchestration_HITL/Program.cs index 8dc2186571..c114ee6b48 100644 --- a/dotnet/samples/Durable/Agents/ConsoleApps/05_AgentOrchestration_HITL/Program.cs +++ b/dotnet/samples/Durable/Agents/ConsoleApps/05_AgentOrchestration_HITL/Program.cs @@ -48,7 +48,7 @@ static async Task RunOrchestratorAsync(TaskOrchestrationContext context, { // Get the writer agent DurableAIAgent writerAgent = context.GetAgent("WriterAgent"); - AgentSession writerSession = await writerAgent.GetNewSessionAsync(); + AgentSession writerSession = await writerAgent.CreateSessionAsync(); // Set initial status context.SetCustomStatus($"Starting content generation for topic: {input.Topic}"); diff --git a/dotnet/samples/Durable/Agents/ConsoleApps/06_LongRunningTools/Program.cs b/dotnet/samples/Durable/Agents/ConsoleApps/06_LongRunningTools/Program.cs index 0e493a0ffc..8a593020c3 100644 --- a/dotnet/samples/Durable/Agents/ConsoleApps/06_LongRunningTools/Program.cs +++ b/dotnet/samples/Durable/Agents/ConsoleApps/06_LongRunningTools/Program.cs @@ -59,7 +59,7 @@ static async Task RunOrchestratorAsync(TaskOrchestrationContext context, { // Get the writer agent DurableAIAgent writerAgent = context.GetAgent(WriterAgentName); - AgentSession writerSession = await writerAgent.GetNewSessionAsync(); + AgentSession writerSession = await writerAgent.CreateSessionAsync(); // Set initial status context.SetCustomStatus($"Starting content generation for topic: {input.Topic}"); @@ -299,7 +299,7 @@ Console.WriteLine("Enter a topic for the Publisher agent to write about (or 'exi Console.WriteLine(); // Create a session for the conversation -AgentSession session = await agentProxy.GetNewSessionAsync(); +AgentSession session = await agentProxy.CreateSessionAsync(); using CancellationTokenSource cts = new(); Console.CancelKeyPress += (sender, e) => diff --git a/dotnet/samples/Durable/Agents/ConsoleApps/07_ReliableStreaming/Program.cs b/dotnet/samples/Durable/Agents/ConsoleApps/07_ReliableStreaming/Program.cs index 720e2d7030..516ee889d8 100644 --- a/dotnet/samples/Durable/Agents/ConsoleApps/07_ReliableStreaming/Program.cs +++ b/dotnet/samples/Durable/Agents/ConsoleApps/07_ReliableStreaming/Program.cs @@ -305,7 +305,7 @@ if (string.IsNullOrWhiteSpace(prompt) || prompt.Equals("exit", StringComparison. } // Create a new agent session -AgentSession session = await agentProxy.GetNewSessionAsync(); +AgentSession session = await agentProxy.CreateSessionAsync(); AgentSessionId sessionId = session.GetService(); string conversationId = sessionId.ToString(); diff --git a/dotnet/samples/GettingStarted/A2A/A2AAgent_PollingForTaskCompletion/Program.cs b/dotnet/samples/GettingStarted/A2A/A2AAgent_PollingForTaskCompletion/Program.cs index ddc330f321..e1731604a9 100644 --- a/dotnet/samples/GettingStarted/A2A/A2AAgent_PollingForTaskCompletion/Program.cs +++ b/dotnet/samples/GettingStarted/A2A/A2AAgent_PollingForTaskCompletion/Program.cs @@ -16,7 +16,7 @@ AgentCard agentCard = await agentCardResolver.GetAgentCardAsync(); // Create an instance of the AIAgent for an existing A2A agent specified by the agent card. AIAgent agent = agentCard.AsAIAgent(); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); // Start the initial run with a long-running task. AgentResponse response = await agent.RunAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", session); diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs index 56d289ce7b..cff6cbbfde 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs @@ -20,7 +20,7 @@ AIAgent agent = chatClient.AsAIAgent( name: "agui-client", description: "AG-UI Client Agent"); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); List messages = [ new(ChatRole.System, "You are a helpful assistant.") diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs index 8ab9d11b76..203a2a0802 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs @@ -20,7 +20,7 @@ AIAgent agent = chatClient.AsAIAgent( name: "agui-client", description: "AG-UI Client Agent"); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); List messages = [ new(ChatRole.System, "You are a helpful assistant.") diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs index 06ce9e8b55..7f3806a721 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs @@ -33,7 +33,7 @@ AIAgent agent = chatClient.AsAIAgent( description: "AG-UI Client Agent", tools: frontendTools); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); List messages = [ new(ChatRole.System, "You are a helpful assistant.") diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs index 8db46493d2..a358956ce8 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs @@ -30,7 +30,7 @@ JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web) }; StatefulAgent agent = new(baseAgent, jsonOptions, new AgentState()); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); List messages = [ new(ChatRole.System, "You are a helpful recipe assistant.") diff --git a/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs b/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs index 1b09b43588..b818de2e44 100644 --- a/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs +++ b/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs @@ -128,7 +128,7 @@ var agent = new ChatClientAgent(instrumentedChatClient, .UseOpenTelemetry(SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level .Build(); -var session = await agent.GetNewSessionAsync(); +var session = await agent.CreateSessionAsync(); appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.Id); diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgentsPersistent/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgentsPersistent/Program.cs index 38836b90d3..07b160e880 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgentsPersistent/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgentsPersistent/Program.cs @@ -31,7 +31,7 @@ AIAgent agent2 = await persistentAgentsClient.CreateAIAgentAsync( instructions: JokerInstructions); // You can then invoke the agent like any other AIAgent. -AgentSession session = await agent1.GetNewSessionAsync(); +AgentSession session = await agent1.CreateSessionAsync(); Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", session)); // Cleanup for sample purposes. diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIProject/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIProject/Program.cs index d4dd7e7d3a..4ac6f40022 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIProject/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIProject/Program.cs @@ -40,7 +40,7 @@ var latestAgentVersion = jokerAgentLatest.GetService()!; Console.WriteLine($"Latest agent version id: {latestAgentVersion.Id}"); // Once you have the AIAgent, you can invoke it like any other AIAgent. -AgentSession session = await jokerAgentLatest.GetNewSessionAsync(); +AgentSession session = await jokerAgentLatest.CreateSessionAsync(); Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate.", session)); // This will use the same session to continue the conversation. diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs index 980a4eda40..1e6190ca6b 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs @@ -28,7 +28,7 @@ namespace SampleApp { public override string? Name => "UpperCaseParrotAgent"; - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new CustomAgentSession()); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) @@ -37,7 +37,7 @@ namespace SampleApp protected override async Task RunCoreAsync(IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { // Create a session if the user didn't supply one. - session ??= await this.GetNewSessionAsync(cancellationToken); + session ??= await this.CreateSessionAsync(cancellationToken); if (session is not CustomAgentSession typedSession) { @@ -69,7 +69,7 @@ namespace SampleApp protected override async IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Create a session if the user didn't supply one. - session ??= await this.GetNewSessionAsync(cancellationToken); + session ??= await this.CreateSessionAsync(cancellationToken); if (session is not CustomAgentSession typedSession) { diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIAssistants/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIAssistants/Program.cs index 69bd31d971..fa7edfb52c 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIAssistants/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIAssistants/Program.cs @@ -33,7 +33,7 @@ AIAgent agent2 = await assistantClient.CreateAIAgentAsync( instructions: JokerInstructions); // You can invoke the agent like any other AIAgent. -AgentSession session = await agent1.GetNewSessionAsync(); +AgentSession session = await agent1.CreateSessionAsync(); Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", session)); // Cleanup for sample purposes. diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs index 6c32741c94..c58c7ebcc8 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs @@ -26,11 +26,11 @@ AIAgent agent = new AnthropicClient { ApiKey = apiKey } .AsAIAgent(model: model, instructions: AssistantInstructions, name: AssistantName, tools: [tool]); // Non-streaming agent interaction with function tools. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", session)); // Streaming agent interaction with function tools. -session = await agent.GetNewSessionAsync(); +session = await agent.CreateSessionAsync(); await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", session)) { Console.WriteLine(update); diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/Program.cs index 6299f6381a..ec55abf3a4 100644 --- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/Program.cs @@ -47,14 +47,14 @@ AIAgent agent = new AzureOpenAIClient( }); // Start a new session for the agent conversation. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); // Run the agent with the session that stores conversation history in the vector store. Console.WriteLine(await agent.RunAsync("I like jokes about Pirates. Tell me a joke about a pirate.", session)); // Start a second session. Since we configured the search scope to be across all sessions for the user, // the agent should remember that the user likes pirate jokes. -AgentSession? session2 = await agent.GetNewSessionAsync(); +AgentSession? session2 = await agent.CreateSessionAsync(); // Run the agent with the second session. Console.WriteLine(await agent.RunAsync("Tell me a joke that I might like.", session2)); diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/Program.cs index ab0f1735c0..30cccde55a 100644 --- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/Program.cs @@ -40,7 +40,7 @@ AIAgent agent = new AzureOpenAIClient( : new Mem0Provider(mem0HttpClient, ctx.SerializedState, ctx.JsonSerializerOptions)) }); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); // Clear any existing memories for this scope to demonstrate fresh behavior. Mem0Provider mem0Provider = session.GetService()!; @@ -60,5 +60,5 @@ AgentSession restoredSession = await agent.DeserializeSessionAsync(serializedSes Console.WriteLine(await agent.RunAsync("Can you recap the personal details you remember?", restoredSession)); Console.WriteLine("\n>> Start a new session that shares the same Mem0 scope\n"); -AgentSession newSession = await agent.GetNewSessionAsync(); +AgentSession newSession = await agent.CreateSessionAsync(); Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newSession)); diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/Program.cs index cf8e0dd943..42a5e15b64 100644 --- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/Program.cs @@ -37,7 +37,7 @@ AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions() }); // Create a new session for the conversation. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(">> Use session with blank memory\n"); @@ -68,7 +68,7 @@ Console.WriteLine("\n>> Use new session with previously created memories\n"); // It is also possible to set the memories in a memory component on an individual session. // This is useful if we want to start a new session, but have it share the same memories as a previous session. -var newSession = await agent.GetNewSessionAsync(); +var newSession = await agent.CreateSessionAsync(); if (userInfo is not null && newSession.GetService() is UserInfoMemory newSessionMemory) { newSessionMemory.UserInfo = userInfo; diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Program.cs b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Program.cs index 5311f14778..9659d3dc40 100644 --- a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Program.cs @@ -30,7 +30,7 @@ using JsonDocument createConversationResultAsJson = JsonDocument.Parse(createCon string conversationId = createConversationResultAsJson.RootElement.GetProperty("id"u8)!.GetString()!; // Create a session for the conversation - this enables conversation state management for subsequent turns -AgentSession session = await agent.GetNewSessionAsync(conversationId); +AgentSession session = await agent.CreateSessionAsync(conversationId); Console.WriteLine("=== Multi-turn Conversation Demo ===\n"); diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/README.md b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/README.md index 2b4c282ead..9706ca05f1 100644 --- a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/README.md +++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/README.md @@ -33,7 +33,7 @@ The `AgentSession` works with `ChatClientAgentRunOptions` to link the agent to a ChatClientAgentRunOptions agentRunOptions = new() { ChatOptions = new ChatOptions() { ConversationId = conversationId } }; // Create a session for the conversation -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); // First call links the session to the conversation ChatCompletion firstResponse = await agent.RunAsync([firstMessage], session, agentRunOptions); @@ -59,7 +59,7 @@ foreach (ClientResult result in getConversationItemsResults.GetRawPages()) 1. **Create an OpenAI Client**: Initialize an `OpenAIClient` with your API key 2. **Create a Conversation**: Use `ConversationClient` to create a server-side conversation 3. **Create an Agent**: Initialize an `OpenAIResponseClientAgent` with the desired model and instructions -4. **Create a Session**: Call `agent.GetNewSessionAsync()` to create a new conversation session +4. **Create a Session**: Call `agent.CreateSessionAsync()` to create a new conversation session 5. **Link Session to Conversation**: Pass `ChatClientAgentRunOptions` with the `ConversationId` on the first call 6. **Send Messages**: Subsequent calls to `agent.RunAsync()` only need the session - context is maintained 7. **Cleanup**: Delete the conversation when done using `conversationClient.DeleteConversation()` diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs index 0beb0063a4..ca798aa333 100644 --- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs @@ -70,7 +70,7 @@ AIAgent agent = azureOpenAIClient .WithAIContextProviderMessageRemoval()), }); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(">> Asking about returns\n"); Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", session)); diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs index 486fdbe44d..3648ccc898 100644 --- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs @@ -74,7 +74,7 @@ AIAgent agent = azureOpenAIClient AIContextProviderFactory = (ctx, ct) => new ValueTask(new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)) }); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(">> Asking about SK sessions\n"); Console.WriteLine(await agent.RunAsync("Hi! How do I create a thread/session in Semantic Kernel?", session)); diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/Program.cs index dd258faf81..bcc823de46 100644 --- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/Program.cs @@ -32,7 +32,7 @@ AIAgent agent = new AzureOpenAIClient( AIContextProviderFactory = (ctx, ct) => new ValueTask(new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)) }); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(">> Asking about returns\n"); Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", session)); diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs index c7faa71ed5..cfb40f4029 100644 --- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/Program.cs @@ -43,7 +43,7 @@ AIAgent agent = await aiProjectClient instructions: "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.", tools: [fileSearchTool]); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(">> Asking about returns\n"); Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", session)); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Program.cs index 73a9a76786..6a60de132d 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Program.cs @@ -17,12 +17,12 @@ AIAgent agent = new AzureOpenAIClient( .AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker"); // Invoke the agent with a multi-turn conversation, where the context is preserved in the session object. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session)); Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session)); // Invoke the agent with a multi-turn conversation and streaming, where the context is preserved in the session object. -session = await agent.GetNewSessionAsync(); +session = await agent.CreateSessionAsync(); await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate.", session)) { Console.WriteLine(update); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Program.cs index ad4303583c..f13aa8ff26 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Program.cs @@ -30,7 +30,7 @@ AIAgent agent = new AzureOpenAIClient( .AsAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]); // Call the agent and check if there are any user input requests to handle. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); var response = await agent.RunAsync("What is the weather like in Amsterdam?", session); var userInputRequests = response.UserInputRequests.ToList(); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Program.cs index c76ec1ab6e..84ba3a918d 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Program.cs @@ -19,7 +19,7 @@ AIAgent agent = new AzureOpenAIClient( .AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker"); // Start a new session for the agent conversation. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); // Run the agent with a new session. Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session)); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs index b4dc0e8e0e..a1898ea426 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs @@ -39,7 +39,7 @@ AIAgent agent = new AzureOpenAIClient( }); // Start a new session for the agent conversation. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); // Run the agent with the session that stores chat history in the vector store. Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session)); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Program.cs index 0f25862ffd..1eb0c16742 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Program.cs @@ -49,7 +49,7 @@ internal sealed class SampleService(AIAgent agent, IHostApplicationLifetime appL public async Task StartAsync(CancellationToken cancellationToken) { // Create a session that will be used for the entirety of the service lifetime so that the user can ask follow up questions. - this._session = await agent.GetNewSessionAsync(cancellationToken); + this._session = await agent.CreateSessionAsync(cancellationToken); _ = this.RunAsync(appLifetime.ApplicationStopping); } diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Program.cs index 4cbff0efe0..9e5985b8c0 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Program.cs @@ -22,7 +22,7 @@ ChatMessage message = new(ChatRole.User, [ new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg") ]); -var session = await agent.GetNewSessionAsync(); +var session = await agent.CreateSessionAsync(); await foreach (var update in agent.RunStreamingAsync(message, session)) { diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step13_BackgroundResponsesWithToolsAndPersistence/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step13_BackgroundResponsesWithToolsAndPersistence/Program.cs index 9332e0f05e..5dfae17df0 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step13_BackgroundResponsesWithToolsAndPersistence/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step13_BackgroundResponsesWithToolsAndPersistence/Program.cs @@ -32,7 +32,7 @@ AIAgent agent = new AzureOpenAIClient( // Enable background responses (only supported by {Azure}OpenAI Responses at this time). AgentRunOptions options = new() { AllowBackgroundResponses = true }; -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); // Start the initial run. AgentResponse response = await agent.RunAsync("Write a very long novel about a team of astronauts exploring an uncharted galaxy.", session, options); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/Program.cs index 049160046e..f3bc09bdec 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/Program.cs @@ -45,7 +45,7 @@ var middlewareEnabledAgent = originalAgent .Use(GuardrailMiddleware, null) .Build(); -var session = await middlewareEnabledAgent.GetNewSessionAsync(); +var session = await middlewareEnabledAgent.CreateSessionAsync(); Console.WriteLine("\n\n=== Example 1: Wording Guardrail ==="); var guardRailedResponse = await middlewareEnabledAgent.RunAsync("Tell me something harmful."); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs index 525a65e609..f9a5a1fc01 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs @@ -27,7 +27,7 @@ AIAgent agent = new AzureOpenAIClient( ChatHistoryProviderFactory = (ctx, ct) => new ValueTask(new InMemoryChatHistoryProvider(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)) }); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); // Invoke the agent and output the text result. Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session)); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs index 55dbfa6199..1fa436b156 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs @@ -19,7 +19,7 @@ AIAgent agent = new AzureOpenAIClient( // Enable background responses (only supported by OpenAI Responses at this time). AgentRunOptions options = new() { AllowBackgroundResponses = true }; -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); // Start the initial run. AgentResponse response = await agent.RunAsync("Write a very long novel about otters in space.", session, options); @@ -41,7 +41,7 @@ Console.WriteLine(response.Text); // Reset options and session for streaming. options = new() { AllowBackgroundResponses = true }; -session = await agent.GetNewSessionAsync(); +session = await agent.CreateSessionAsync(); AgentResponseUpdate? lastReceivedUpdate = null; // Start streaming. diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Program.cs index 2c55c5294f..d7ebc9fca4 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Program.cs @@ -39,7 +39,7 @@ Console.WriteLine(); try { - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); await foreach (var response in agent.RunStreamingAsync(Task, session)) { diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step20_AdditionalAIContext/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step20_AdditionalAIContext/Program.cs index c3b4d6f979..52e5c37cb8 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step20_AdditionalAIContext/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step20_AdditionalAIContext/Program.cs @@ -58,7 +58,7 @@ AIAgent agent = new AzureOpenAIClient( }); // Invoke the agent and output the text result. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(await agent.RunAsync("I need to pick up milk from the supermarket.", session) + "\n"); Console.WriteLine(await agent.RunAsync("I need to take Sally for soccer practice.", session) + "\n"); Console.WriteLine(await agent.RunAsync("I need to make a dentist appointment for Jimmy.", session) + "\n"); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/Program.cs index f55e57cc65..1e1e7d72a8 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/Program.cs @@ -29,13 +29,13 @@ ProjectConversation conversation = await conversationsClient.CreateProjectConver // Providing the conversation Id is not strictly necessary, but by not providing it no information will show up in the Foundry Project UI as conversations. // Sessions that don't have a conversation Id will work based on the `PreviousResponseId`. -AgentSession session = await jokerAgent.GetNewSessionAsync(conversation.Id); +AgentSession session = await jokerAgent.CreateSessionAsync(conversation.Id); Console.WriteLine(await jokerAgent.RunAsync("Tell me a joke about a pirate.", session)); Console.WriteLine(await jokerAgent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session)); // Invoke the agent with a multi-turn conversation and streaming, where the context is preserved in the session object. -session = await jokerAgent.GetNewSessionAsync(conversation.Id); +session = await jokerAgent.CreateSessionAsync(conversation.Id); await foreach (AgentResponseUpdate update in jokerAgent.RunStreamingAsync("Tell me a joke about a pirate.", session)) { Console.WriteLine(update); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/README.md b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/README.md index 4cbe5a5922..6611287bd9 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/README.md +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/README.md @@ -54,6 +54,6 @@ The sample will: When working with multi-turn conversations, there are two approaches: -- **With Conversation ID**: By passing a `conversation.Id` to `GetNewSessionAsync()`, the conversation will be visible in the Azure Foundry Project UI. This is useful for tracking and debugging conversations. +- **With Conversation ID**: By passing a `conversation.Id` to `CreateSessionAsync()`, the conversation will be visible in the Azure Foundry Project UI. This is useful for tracking and debugging conversations. - **Without Conversation ID**: Sessions created without a conversation ID still work correctly, maintaining context via `PreviousResponseId`. However, these conversations may not appear in the Foundry UI. diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/Program.cs index 4ff34d86f6..9393b71b9a 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/Program.cs @@ -37,11 +37,11 @@ var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, mod var existingAgent = await aiProjectClient.GetAIAgentAsync(name: AssistantName, tools: [tool]); // Non-streaming agent interaction with function tools. -AgentSession session = await existingAgent.GetNewSessionAsync(); +AgentSession session = await existingAgent.CreateSessionAsync(); Console.WriteLine(await existingAgent.RunAsync("What is the weather like in Amsterdam?", session)); // Streaming agent interaction with function tools. -session = await existingAgent.GetNewSessionAsync(); +session = await existingAgent.CreateSessionAsync(); await foreach (AgentResponseUpdate update in existingAgent.RunStreamingAsync("What is the weather like in Amsterdam?", session)) { Console.WriteLine(update); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/Program.cs index b030ca8c64..1e09f95161 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/Program.cs @@ -32,7 +32,7 @@ AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, mo // Call the agent with approval-required function tools. // The agent will request approval before invoking the function. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); AgentResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", session); // Check if there are any user input requests (approvals needed). diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step06_PersistedConversations/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step06_PersistedConversations/Program.cs index 9d55230e50..20e3471df0 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step06_PersistedConversations/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step06_PersistedConversations/Program.cs @@ -19,7 +19,7 @@ AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential( AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: JokerName, model: deploymentName, instructions: JokerInstructions); // Start a new session for the agent conversation. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); // Run the agent with a new session. Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session)); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step07_Observability/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step07_Observability/Program.cs index 1ac24011b4..ac0dc23012 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step07_Observability/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step07_Observability/Program.cs @@ -38,11 +38,11 @@ AIAgent agent = (await aiProjectClient.CreateAIAgentAsync(name: JokerName, model .Build(); // Invoke the agent and output the text result. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session)); // Invoke the agent with streaming support. -session = await agent.GetNewSessionAsync(); +session = await agent.CreateSessionAsync(); await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Tell me a joke about a pirate.", session)) { Console.WriteLine(update); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step08_DependencyInjection/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step08_DependencyInjection/Program.cs index dcded5c0ab..4c53d4ab9c 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step08_DependencyInjection/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step08_DependencyInjection/Program.cs @@ -54,7 +54,7 @@ internal sealed class SampleService(AIProjectClient client, AIAgent agent, IHost public async Task StartAsync(CancellationToken cancellationToken) { // Create a session that will be used for the entirety of the service lifetime so that the user can ask follow up questions. - this._session = await agent.GetNewSessionAsync(cancellationToken); + this._session = await agent.CreateSessionAsync(cancellationToken); _ = this.RunAsync(appLifetime.ApplicationStopping); } diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Program.cs index 547998fb27..8f38378626 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Program.cs @@ -24,7 +24,7 @@ ChatMessage message = new(ChatRole.User, [ new DataContent(File.ReadAllBytes("assets/walkway.jpg"), "image/jpeg") ]); -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(message, session)) { diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step11_AsFunctionTool/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step11_AsFunctionTool/Program.cs index 29a1eda312..ccf0a0f012 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step11_AsFunctionTool/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step11_AsFunctionTool/Program.cs @@ -39,7 +39,7 @@ AIAgent agent = await aiProjectClient.CreateAIAgentAsync( tools: [weatherAgent.AsAIFunction()]); // Invoke the agent and output the text result. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", session)); // Cleanup by agent name removes the agent versions created. diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step12_Middleware/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step12_Middleware/Program.cs index d8c9fb3d15..11bfe1ae45 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step12_Middleware/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step12_Middleware/Program.cs @@ -49,7 +49,7 @@ AIAgent middlewareEnabledAgent = originalAgent .Use(GuardrailMiddleware, null) .Build(); -AgentSession session = await middlewareEnabledAgent.GetNewSessionAsync(); +AgentSession session = await middlewareEnabledAgent.CreateSessionAsync(); Console.WriteLine("\n\n=== Example 1: Wording Guardrail ==="); AgentResponse guardRailedResponse = await middlewareEnabledAgent.RunAsync("Tell me something harmful."); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step13_Plugins/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step13_Plugins/Program.cs index 15269ca9b8..b761c120db 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step13_Plugins/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step13_Plugins/Program.cs @@ -42,7 +42,7 @@ AIAgent agent = await aiProjectClient.CreateAIAgentAsync( services: serviceProvider); // Invoke the agent and output the text result. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(await agent.RunAsync("Tell me current time and weather in Seattle.", session)); // Cleanup by agent name removes the agent version created. diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/Program.cs index d7951635eb..e3294be059 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/Program.cs @@ -83,7 +83,7 @@ internal sealed class Program AllowBackgroundResponses = true, }; - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage message = new(ChatRole.User, [ new TextContent("I need you to help me search for 'OpenAI news'. Please type 'OpenAI news' and submit the search. Once you see search results, the task is complete."), diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs b/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs index 4251f399d0..f6e762d2b4 100644 --- a/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs +++ b/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs @@ -42,7 +42,7 @@ AIAgent agent = await persistentAgentsClient.CreateAIAgentAsync( }); // You can then invoke the agent like any other AIAgent. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(await agent.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", session)); // Cleanup for sample purposes. @@ -75,7 +75,7 @@ AIAgent agentWithRequiredApproval = await persistentAgentsClient.CreateAIAgentAs }); // You can then invoke the agent like any other AIAgent. -var sessionWithRequiredApproval = await agentWithRequiredApproval.GetNewSessionAsync(); +var sessionWithRequiredApproval = await agentWithRequiredApproval.CreateSessionAsync(); var response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", sessionWithRequiredApproval); var userInputRequests = response.UserInputRequests.ToList(); diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs b/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs index c8edf00d03..0c4684bc36 100644 --- a/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs +++ b/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs @@ -37,7 +37,7 @@ AIAgent agent = new AzureOpenAIClient( tools: [mcpTool]); // You can then invoke the agent like any other AIAgent. -AgentSession session = await agent.GetNewSessionAsync(); +AgentSession session = await agent.CreateSessionAsync(); Console.WriteLine(await agent.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", session)); // **** MCP Tool with Approval Required **** @@ -64,7 +64,7 @@ AIAgent agentWithRequiredApproval = new AzureOpenAIClient( tools: [mcpToolWithApproval]); // You can then invoke the agent like any other AIAgent. -var sessionWithRequiredApproval = await agentWithRequiredApproval.GetNewSessionAsync(); +var sessionWithRequiredApproval = await agentWithRequiredApproval.CreateSessionAsync(); var response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", sessionWithRequiredApproval); var userInputRequests = response.UserInputRequests.ToList(); diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs index 9689b1c306..02fbdf3ecc 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs @@ -136,7 +136,7 @@ internal sealed class SloganWriterExecutor : Executor public async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) { - this._session ??= await this._agent.GetNewSessionAsync(cancellationToken); + this._session ??= await this._agent.CreateSessionAsync(cancellationToken); var result = await this._agent.RunAsync(message, this._session, cancellationToken: cancellationToken); @@ -209,7 +209,7 @@ internal sealed class FeedbackExecutor : Executor public override async ValueTask HandleAsync(SloganResult message, IWorkflowContext context, CancellationToken cancellationToken = default) { - this._session ??= await this._agent.GetNewSessionAsync(cancellationToken); + this._session ??= await this._agent.CreateSessionAsync(cancellationToken); var sloganMessage = $""" Here is a slogan for the task '{message.Task}': diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs index 908ed3b914..48436b7b8f 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs @@ -37,7 +37,7 @@ public static class Program // Create the workflow and turn it into an agent var workflow = WorkflowFactory.BuildWorkflow(chatClient); var agent = workflow.AsAgent("workflow-agent", "Workflow Agent"); - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); // Start an interactive loop to interact with the workflow as if it were an agent while (true) diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/HostedWorkflow/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/HostedWorkflow/Program.cs index 96d728b487..b6011a1a71 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/HostedWorkflow/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/HostedWorkflow/Program.cs @@ -47,7 +47,7 @@ internal sealed class Program AIAgent agent = aiProjectClient.AsAIAgent(agentVersion); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ProjectConversation conversation = await aiProjectClient diff --git a/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/Program.cs b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/Program.cs index 806fad421b..7d7d4d69fd 100644 --- a/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/Program.cs @@ -90,7 +90,7 @@ public static class Program { EnableSensitiveData = true // enable sensitive data at the agent level such as prompts and responses }; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); // Start an interactive loop to interact with the workflow as if it were an agent while (true) diff --git a/dotnet/samples/M365Agent/AFAgentApplication.cs b/dotnet/samples/M365Agent/AFAgentApplication.cs index 9b7d25abe4..4dea6c6e28 100644 --- a/dotnet/samples/M365Agent/AFAgentApplication.cs +++ b/dotnet/samples/M365Agent/AFAgentApplication.cs @@ -44,7 +44,7 @@ internal sealed class AFAgentApplication : AgentApplication // Deserialize the conversation history into an AgentSession, or create a new one if none exists. AgentSession agentSession = sessionElementStart.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null ? await this._agent.DeserializeSessionAsync(sessionElementStart, JsonUtilities.DefaultOptions, cancellationToken) - : await this._agent.GetNewSessionAsync(cancellationToken); + : await this._agent.CreateSessionAsync(cancellationToken); ChatMessage chatMessage = HandleUserInput(turnContext); diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs index 1eb0f8449e..3ccc9f481e 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs @@ -54,7 +54,7 @@ public sealed class A2AAgent : AIAgent } /// - public sealed override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public sealed override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new A2AAgentSession()); /// @@ -62,7 +62,7 @@ public sealed class A2AAgent : AIAgent /// /// The context id to continue. /// A value task representing the asynchronous operation. The task result contains a new instance. - public ValueTask GetNewSessionAsync(string contextId) + public ValueTask CreateSessionAsync(string contextId) => new(new A2AAgentSession() { ContextId = contextId }); /// @@ -230,7 +230,7 @@ public sealed class A2AAgent : AIAgent throw new InvalidOperationException("A session must be provided when AllowBackgroundResponses is enabled."); } - session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + session ??= await this.CreateSessionAsync(cancellationToken).ConfigureAwait(false); if (session is not A2AAgentSession typedSession) { diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs index 9ec8d6b7e9..31f4993723 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs @@ -123,7 +123,7 @@ public abstract class AIAgent /// may be deferred until first use to optimize performance. /// /// - public abstract ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default); + public abstract ValueTask CreateSessionAsync(CancellationToken cancellationToken = default); /// /// Deserializes an agent session from its JSON serialized representation. diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSession.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSession.cs index b722582668..5bcea2239d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSession.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSession.cs @@ -26,7 +26,7 @@ namespace Microsoft.Agents.AI; /// Chat history reduction, e.g. where messages needs to be summarized or truncated to reduce the size. /// /// An is always constructed by an so that the -/// can attach any necessary behaviors to the . See the +/// can attach any necessary behaviors to the . See the /// and methods for more information. /// /// @@ -42,7 +42,7 @@ namespace Microsoft.Agents.AI; /// /// /// -/// +/// /// public abstract class AgentSession { diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs index 9e7bda3f28..99979871b1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs @@ -74,7 +74,7 @@ public abstract class DelegatingAIAgent : AIAgent } /// - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => this.InnerAgent.GetNewSessionAsync(cancellationToken); + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => this.InnerAgent.CreateSessionAsync(cancellationToken); /// public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) diff --git a/dotnet/src/Microsoft.Agents.AI.CopilotStudio/CopilotStudioAgent.cs b/dotnet/src/Microsoft.Agents.AI.CopilotStudio/CopilotStudioAgent.cs index bf786a1712..378c36298b 100644 --- a/dotnet/src/Microsoft.Agents.AI.CopilotStudio/CopilotStudioAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.CopilotStudio/CopilotStudioAgent.cs @@ -42,7 +42,7 @@ public class CopilotStudioAgent : AIAgent } /// - public sealed override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public sealed override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new CopilotStudioAgentSession()); /// @@ -50,7 +50,7 @@ public class CopilotStudioAgent : AIAgent /// /// The conversation id to continue. /// A new instance. - public ValueTask GetNewSessionAsync(string conversationId) + public ValueTask CreateSessionAsync(string conversationId) => new(new CopilotStudioAgentSession() { ConversationId = conversationId }); /// @@ -68,7 +68,7 @@ public class CopilotStudioAgent : AIAgent // Ensure that we have a valid session to work with. // If the session ID is null, we need to start a new conversation and set the session ID accordingly. - session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + session ??= await this.CreateSessionAsync(cancellationToken).ConfigureAwait(false); if (session is not CopilotStudioAgentSession typedSession) { throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be used."); @@ -107,7 +107,7 @@ public class CopilotStudioAgent : AIAgent // Ensure that we have a valid session to work with. // If the session ID is null, we need to start a new conversation and set the session ID accordingly. - session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + session ??= await this.CreateSessionAsync(cancellationToken).ConfigureAwait(false); if (session is not CopilotStudioAgentSession typedSession) { throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be used."); diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs index 09c6df31ea..e87f17be68 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs @@ -67,7 +67,7 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella // Start the agent response stream IAsyncEnumerable responseStream = agentWrapper.RunStreamingAsync( this.State.Data.ConversationHistory.SelectMany(e => e.Messages).Select(m => m.ToChatMessage()), - await agentWrapper.GetNewSessionAsync(cancellationToken).ConfigureAwait(false), + await agentWrapper.CreateSessionAsync(cancellationToken).ConfigureAwait(false), options: null, this._cancellationToken); diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs index f944131b4e..a97652bd93 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs @@ -34,7 +34,7 @@ public sealed class DurableAIAgent : AIAgent /// /// The cancellation token. /// A value task that represents the asynchronous operation. The task result contains a new agent session. - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) { AgentSessionId sessionId = this._context.NewAgentSessionId(this._agentName); return ValueTask.FromResult(new DurableAgentSession(sessionId)); @@ -76,12 +76,12 @@ public sealed class DurableAIAgent : AIAgent throw new NotSupportedException("Cancellation is not supported for durable agents."); } - session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + session ??= await this.CreateSessionAsync(cancellationToken).ConfigureAwait(false); if (session is not DurableAgentSession durableSession) { throw new ArgumentException( "The provided session is not valid for a durable agent. " + - "Create a new session using GetNewSessionAsync or provide a session previously created by this agent.", + "Create a new session using CreateSessionAsync or provide a session previously created by this agent.", paramName: nameof(session)); } diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs index d61bd6df77..b6d3fa4900 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs @@ -18,7 +18,7 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient) return ValueTask.FromResult(DurableAgentSession.Deserialize(serializedSession, jsonSerializerOptions)); } - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) { return ValueTask.FromResult(new DurableAgentSession(AgentSessionId.WithRandomKey(this.Name!))); } @@ -29,12 +29,12 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient) AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + session ??= await this.CreateSessionAsync(cancellationToken).ConfigureAwait(false); if (session is not DurableAgentSession durableSession) { throw new ArgumentException( "The provided session is not valid for a durable agent. " + - "Create a new session using GetNewSession or provide a session previously created by this agent.", + "Create a new session using CreateSessionAsync or provide a session previously created by this agent.", paramName: nameof(session)); } diff --git a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs index 41236fcc55..e2771e21c4 100644 --- a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs @@ -86,7 +86,7 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable } /// - public sealed override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public sealed override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new GitHubCopilotAgentSession()); /// @@ -94,7 +94,7 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable /// /// The session id to continue. /// A new instance. - public ValueTask GetNewSessionAsync(string sessionId) + public ValueTask CreateSessionAsync(string sessionId) => new(new GitHubCopilotAgentSession() { SessionId = sessionId }); /// @@ -122,7 +122,7 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable _ = Throw.IfNull(messages); // Ensure we have a valid session - session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + session ??= await this.CreateSessionAsync(cancellationToken).ConfigureAwait(false); if (session is not GitHubCopilotAgentSession typedSession) { throw new InvalidOperationException( diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/README.md b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/README.md index 0506a0ea23..6cacc5adff 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/README.md +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/README.md @@ -74,7 +74,7 @@ public static async Task SpamDetectionOrchestration( // Get the spam detection agent DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent"); - AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync(); + AgentSession spamSession = await spamDetectionAgent.CreateSessionAsync(); // Step 1: Check if the email is spam AgentResponse spamDetectionResponse = await spamDetectionAgent.RunAsync( @@ -97,7 +97,7 @@ public static async Task SpamDetectionOrchestration( { // Generate and send response for legitimate email DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent"); - AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync(); + AgentSession emailSession = await emailAssistantAgent.CreateSessionAsync(); AgentResponse emailAssistantResponse = await emailAssistantAgent.RunAsync( message: diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs index ec1381f405..d66192c709 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs @@ -45,7 +45,7 @@ public sealed class InMemoryAgentSessionStore : AgentSessionStore return sessionContent switch { - null => await agent.GetNewSessionAsync(cancellationToken).ConfigureAwait(false), + null => await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false), _ => await agent.DeserializeSessionAsync(sessionContent.Value, cancellationToken: cancellationToken).ConfigureAwait(false), }; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/NoopAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/NoopAgentSessionStore.cs index c0736afda3..163c1ea867 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/NoopAgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/NoopAgentSessionStore.cs @@ -20,6 +20,6 @@ public sealed class NoopAgentSessionStore : AgentSessionStore /// public override ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) { - return agent.GetNewSessionAsync(cancellationToken); + return agent.CreateSessionAsync(cancellationToken); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/PurviewAgent.cs b/dotnet/src/Microsoft.Agents.AI.Purview/PurviewAgent.cs index 336be0e108..a08d978fa8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Purview/PurviewAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Purview/PurviewAgent.cs @@ -36,9 +36,9 @@ internal class PurviewAgent : AIAgent, IDisposable } /// - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) { - return this._innerAgent.GetNewSessionAsync(cancellationToken); + return this._innerAgent.CreateSessionAsync(cancellationToken); } /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs index f53a6f076b..3562747b7c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs @@ -93,7 +93,7 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor => emitEvents ?? this._options.EmitAgentUpdateEvents ?? false; private async ValueTask EnsureSessionAsync(IWorkflowContext context, CancellationToken cancellationToken) => - this._session ??= await this._agent.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + this._session ??= await this._agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false); private const string UserInputRequestStateKey = nameof(_userInputHandler); private const string FunctionCallRequestStateKey = nameof(_functionCallHandler); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs index 3bd1059ce8..86c725bca1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs @@ -65,7 +65,7 @@ internal sealed class WorkflowHostAgent : AIAgent protocol.ThrowIfNotChatProtocol(allowCatchAll: true); } - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new WorkflowSession(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails, this._includeWorkflowOutputsInResponse)); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) @@ -73,7 +73,7 @@ internal sealed class WorkflowHostAgent : AIAgent private async ValueTask UpdateSessionAsync(IEnumerable messages, AgentSession? session = null, CancellationToken cancellationToken = default) { - session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + session ??= await this.CreateSessionAsync(cancellationToken).ConfigureAwait(false); if (session is not WorkflowSession workflowSession) { diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index 937a6d0f0b..504928d2d6 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -302,7 +302,7 @@ public sealed partial class ChatClientAgent : AIAgent : this.ChatClient.GetService(serviceType, serviceKey)); /// - public override async ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override async ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) { ChatHistoryProvider? chatHistoryProvider = this._agentOptions?.ChatHistoryProviderFactory is not null ? await this._agentOptions.ChatHistoryProviderFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false) @@ -337,7 +337,7 @@ public sealed partial class ChatClientAgent : AIAgent /// instances that support server-side conversation storage through their underlying . /// /// - public async ValueTask GetNewSessionAsync(string conversationId, CancellationToken cancellationToken = default) + public async ValueTask CreateSessionAsync(string conversationId, CancellationToken cancellationToken = default) { AIContextProvider? contextProvider = this._agentOptions?.AIContextProviderFactory is not null ? await this._agentOptions.AIContextProviderFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false) @@ -365,14 +365,14 @@ public sealed partial class ChatClientAgent : AIAgent /// with a may not be compatible with these services. /// /// - /// Where a service requires server-side conversation storage, use . + /// Where a service requires server-side conversation storage, use . /// /// /// If the agent detects, during the first run, that the underlying AI service requires server-side conversation storage, /// the session will throw an exception to indicate that it cannot continue using the provided . /// /// - public async ValueTask GetNewSessionAsync(ChatHistoryProvider chatHistoryProvider, CancellationToken cancellationToken = default) + public async ValueTask CreateSessionAsync(ChatHistoryProvider chatHistoryProvider, CancellationToken cancellationToken = default) { AIContextProvider? contextProvider = this._agentOptions?.AIContextProviderFactory is not null ? await this._agentOptions.AIContextProviderFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false) @@ -689,7 +689,7 @@ public sealed partial class ChatClientAgent : AIAgent throw new InvalidOperationException("A session must be provided when continuing a background response with a continuation token."); } - session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + session ??= await this.CreateSessionAsync(cancellationToken).ConfigureAwait(false); if (session is not ChatClientAgentSession typedSession) { throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be used."); diff --git a/dotnet/tests/AgentConformance.IntegrationTests/ChatClientAgentRunStreamingTests.cs b/dotnet/tests/AgentConformance.IntegrationTests/ChatClientAgentRunStreamingTests.cs index a32fa3e0ea..aa7d09a86d 100644 --- a/dotnet/tests/AgentConformance.IntegrationTests/ChatClientAgentRunStreamingTests.cs +++ b/dotnet/tests/AgentConformance.IntegrationTests/ChatClientAgentRunStreamingTests.cs @@ -22,7 +22,7 @@ public abstract class ChatClientAgentRunStreamingTests(Func(Func(Func { // Arrange var agent = await this.Fixture.CreateChatClientAgentAsync(instructions: "ALWAYS RESPOND WITH 'Computer says no', even if there was no user input."); - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var agentCleanup = new AgentCleanup(agent, this.Fixture); await using var sessionCleanup = new SessionCleanup(session, this.Fixture); @@ -53,7 +53,7 @@ public abstract class ChatClientAgentRunTests(Func AIFunctionFactory.Create(MenuPlugin.GetSpecials), AIFunctionFactory.Create(MenuPlugin.GetItemPrice) ]); - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); foreach (var questionAndAnswer in questionsAndAnswers) { diff --git a/dotnet/tests/AgentConformance.IntegrationTests/RunStreamingTests.cs b/dotnet/tests/AgentConformance.IntegrationTests/RunStreamingTests.cs index 2290e20cb4..f9cc732175 100644 --- a/dotnet/tests/AgentConformance.IntegrationTests/RunStreamingTests.cs +++ b/dotnet/tests/AgentConformance.IntegrationTests/RunStreamingTests.cs @@ -24,7 +24,7 @@ public abstract class RunStreamingTests(Func creat { // Arrange var agent = this.Fixture.Agent; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var cleanup = new SessionCleanup(session, this.Fixture); // Act @@ -36,7 +36,7 @@ public abstract class RunStreamingTests(Func creat { // Arrange var agent = this.Fixture.Agent; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var cleanup = new SessionCleanup(session, this.Fixture); // Act @@ -52,7 +52,7 @@ public abstract class RunStreamingTests(Func creat { // Arrange var agent = this.Fixture.Agent; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var cleanup = new SessionCleanup(session, this.Fixture); // Act @@ -68,7 +68,7 @@ public abstract class RunStreamingTests(Func creat { // Arrange var agent = this.Fixture.Agent; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var cleanup = new SessionCleanup(session, this.Fixture); // Act @@ -92,7 +92,7 @@ public abstract class RunStreamingTests(Func creat const string Q1 = "What is the capital of France."; const string Q2 = "And Austria?"; var agent = this.Fixture.Agent; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var cleanup = new SessionCleanup(session, this.Fixture); // Act diff --git a/dotnet/tests/AgentConformance.IntegrationTests/RunTests.cs b/dotnet/tests/AgentConformance.IntegrationTests/RunTests.cs index be1fa0f2f3..302784a1a8 100644 --- a/dotnet/tests/AgentConformance.IntegrationTests/RunTests.cs +++ b/dotnet/tests/AgentConformance.IntegrationTests/RunTests.cs @@ -24,7 +24,7 @@ public abstract class RunTests(Func createAgentFix { // Arrange var agent = this.Fixture.Agent; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var cleanup = new SessionCleanup(session, this.Fixture); // Act @@ -39,7 +39,7 @@ public abstract class RunTests(Func createAgentFix { // Arrange var agent = this.Fixture.Agent; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var cleanup = new SessionCleanup(session, this.Fixture); // Act @@ -57,7 +57,7 @@ public abstract class RunTests(Func createAgentFix { // Arrange var agent = this.Fixture.Agent; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var cleanup = new SessionCleanup(session, this.Fixture); // Act @@ -74,7 +74,7 @@ public abstract class RunTests(Func createAgentFix { // Arrange var agent = this.Fixture.Agent; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var cleanup = new SessionCleanup(session, this.Fixture); // Act @@ -99,7 +99,7 @@ public abstract class RunTests(Func createAgentFix const string Q1 = "What is the capital of France."; const string Q2 = "And Austria?"; var agent = this.Fixture.Agent; - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await using var cleanup = new SessionCleanup(session, this.Fixture); // Act diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs index a903973806..d6e736a528 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs @@ -148,7 +148,7 @@ public sealed class A2AAgentTests : IDisposable new(ChatRole.User, "Test message") }; - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); // Act await this._agent.RunAsync(inputMessages, session); @@ -168,7 +168,7 @@ public sealed class A2AAgentTests : IDisposable new(ChatRole.User, "Test message") }; - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); var a2aSession = (A2AAgentSession)session; a2aSession.ContextId = "existing-context-id"; @@ -201,7 +201,7 @@ public sealed class A2AAgentTests : IDisposable ContextId = "different-context" }; - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); var a2aSession = (A2AAgentSession)session; a2aSession.ContextId = "existing-context-id"; @@ -272,7 +272,7 @@ public sealed class A2AAgentTests : IDisposable ContextId = "new-stream-context" }; - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); // Act await foreach (var _ in this._agent.RunStreamingAsync(inputMessages, session)) @@ -296,7 +296,7 @@ public sealed class A2AAgentTests : IDisposable this._handler.StreamingResponseToReturn = new AgentMessage(); - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); var a2aSession = (A2AAgentSession)session; a2aSession.ContextId = "existing-context-id"; @@ -316,7 +316,7 @@ public sealed class A2AAgentTests : IDisposable public async Task RunStreamingAsync_WithSessionHavingDifferentContextId_ThrowsInvalidOperationExceptionAsync() { // Arrange - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); var a2aSession = (A2AAgentSession)session; a2aSession.ContextId = "existing-context-id"; @@ -440,7 +440,7 @@ public sealed class A2AAgentTests : IDisposable Parts = [new TextPart { Text = "Response to task" }] }; - var session = (A2AAgentSession)await this._agent.GetNewSessionAsync(); + var session = (A2AAgentSession)await this._agent.CreateSessionAsync(); session.TaskId = "task-123"; var inputMessage = new ChatMessage(ChatRole.User, "Please make the background transparent"); @@ -466,7 +466,7 @@ public sealed class A2AAgentTests : IDisposable Status = new() { State = TaskState.Submitted } }; - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); // Act await this._agent.RunAsync("Start a task", session); @@ -492,7 +492,7 @@ public sealed class A2AAgentTests : IDisposable } }; - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); // Act var result = await this._agent.RunAsync("Start a long-running task", session); @@ -586,7 +586,7 @@ public sealed class A2AAgentTests : IDisposable Parts = [new TextPart { Text = "Response to task" }] }; - var session = (A2AAgentSession)await this._agent.GetNewSessionAsync(); + var session = (A2AAgentSession)await this._agent.CreateSessionAsync(); session.TaskId = "task-123"; // Act @@ -613,7 +613,7 @@ public sealed class A2AAgentTests : IDisposable Status = new() { State = TaskState.Submitted } }; - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); // Act await foreach (var _ in this._agent.RunStreamingAsync("Start a task", session)) @@ -686,7 +686,7 @@ public sealed class A2AAgentTests : IDisposable ] }; - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); // Act var updates = new List(); @@ -725,7 +725,7 @@ public sealed class A2AAgentTests : IDisposable Status = new() { State = TaskState.Working } }; - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); // Act var updates = new List(); @@ -768,7 +768,7 @@ public sealed class A2AAgentTests : IDisposable } }; - var session = await this._agent.GetNewSessionAsync(); + var session = await this._agent.CreateSessionAsync(); // Act var updates = new List(); diff --git a/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs index 260e6483dd..decfb93a31 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs @@ -228,7 +228,7 @@ public sealed class AGUIAgentTests var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "agent1", description: "Test agent", tools: []); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); List messages = [new ChatMessage(ChatRole.User, "Hello")]; // Act @@ -250,7 +250,7 @@ public sealed class AGUIAgentTests using var httpClient = new HttpClient(); var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "agent1", description: "Test agent", tools: []); - AgentSession originalSession = await agent.GetNewSessionAsync(); + AgentSession originalSession = await agent.CreateSessionAsync(); JsonElement serialized = originalSession.Serialize(); // Act @@ -487,7 +487,7 @@ public sealed class AGUIAgentTests var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "agent1", description: "Test agent", tools: [testTool]); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); List messages = [new ChatMessage(ChatRole.User, "Test")]; // Act diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs index 4b872f0da1..17445eaffa 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs @@ -378,7 +378,7 @@ public class AIAgentTests protected override string? IdCore { get; } - public override async ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override async ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override async ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs index 5be491213b..6320d9c900 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs @@ -35,7 +35,7 @@ public class DelegatingAIAgentTests this._innerAgentMock.Protected().SetupGet("IdCore").Returns("test-agent-id"); this._innerAgentMock.Setup(x => x.Name).Returns("Test Agent"); this._innerAgentMock.Setup(x => x.Description).Returns("Test Description"); - this._innerAgentMock.Setup(x => x.GetNewSessionAsync()).ReturnsAsync(this._testSession); + this._innerAgentMock.Setup(x => x.CreateSessionAsync()).ReturnsAsync(this._testSession); this._innerAgentMock .Protected() @@ -132,17 +132,17 @@ public class DelegatingAIAgentTests #region Method Delegation Tests /// - /// Verify that GetNewSessionAsync delegates to inner agent. + /// Verify that CreateSessionAsync delegates to inner agent. /// [Fact] - public async Task GetNewSessionAsync_DelegatesToInnerAgentAsync() + public async Task CreateSessionAsync_DelegatesToInnerAgentAsync() { // Act - var session = await this._delegatingAgent.GetNewSessionAsync(); + var session = await this._delegatingAgent.CreateSessionAsync(); // Assert Assert.Same(this._testSession, session); - this._innerAgentMock.Verify(x => x.GetNewSessionAsync(), Times.Once); + this._innerAgentMock.Verify(x => x.CreateSessionAsync(), Times.Once); } /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs index 738475da03..6485eaa85b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs @@ -252,7 +252,7 @@ public sealed class AnthropicBetaServiceExtensionsTests defaultMaxTokens: 8192); // Invoke the agent to trigger the request - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); try { await agent.RunAsync("Test message", session); diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs index 33cd212928..79844ed60a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs @@ -320,7 +320,7 @@ public sealed class AnthropicClientExtensionsTests defaultMaxTokens: 8192); // Invoke the agent to trigger the request - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); try { await agent.RunAsync("Test message", session); diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs index 483b81fa1d..9cc340ef5e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs @@ -53,7 +53,7 @@ public class AzureAIProjectChatClientTests }); // Act - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await agent.RunAsync("Hello", session); Assert.True(requestTriggered); @@ -102,7 +102,7 @@ public class AzureAIProjectChatClientTests }); // Act - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await agent.RunAsync("Hello", session, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "conv_12345" } }); Assert.True(requestTriggered); @@ -151,7 +151,7 @@ public class AzureAIProjectChatClientTests }); // Act - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await agent.RunAsync("Hello", session, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "conv_12345" } }); Assert.True(requestTriggered); @@ -200,7 +200,7 @@ public class AzureAIProjectChatClientTests }); // Act - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); await agent.RunAsync("Hello", session, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "resp_0888a" } }); Assert.True(requestTriggered); diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs index 2059eceeb6..930557ab79 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs @@ -71,7 +71,7 @@ public sealed class AggregatorPromptAgentFactoryTests throw new NotImplementedException(); } - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) { throw new NotImplementedException(); } diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/AgentEntityTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/AgentEntityTests.cs index 87774d9223..fe20b2e843 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/AgentEntityTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/AgentEntityTests.cs @@ -51,7 +51,7 @@ public sealed class AgentEntityTests(ITestOutputHelper outputHelper) : IDisposab // A proxy agent is needed to call the hosted test agent AIAgent simpleAgentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services); - AgentSession session = await simpleAgentProxy.GetNewSessionAsync(this.TestTimeoutToken); + AgentSession session = await simpleAgentProxy.CreateSessionAsync(this.TestTimeoutToken); DurableTaskClient client = testHelper.GetClient(); @@ -98,7 +98,7 @@ public sealed class AgentEntityTests(ITestOutputHelper outputHelper) : IDisposab // A proxy agent is needed to call the hosted test agent AIAgent simpleAgentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services); - AgentSession session = await simpleAgentProxy.GetNewSessionAsync(this.TestTimeoutToken); + AgentSession session = await simpleAgentProxy.CreateSessionAsync(this.TestTimeoutToken); DurableTaskClient client = testHelper.GetClient(); @@ -184,7 +184,7 @@ public sealed class AgentEntityTests(ITestOutputHelper outputHelper) : IDisposab public override async Task RunAsync(TaskOrchestrationContext context, string input) { DurableAIAgent writer = context.GetAgent("TestAgent"); - AgentSession writerSession = await writer.GetNewSessionAsync(); + AgentSession writerSession = await writer.CreateSessionAsync(); await writer.RunAsync( message: context.GetInput()!, diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs index 2683047acb..d48e8c0c28 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs @@ -51,7 +51,7 @@ public sealed class ExternalClientTests(ITestOutputHelper outputHelper) : IDispo AIAgent simpleAgentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services); // Act: send a prompt to the agent and wait for a response - AgentSession session = await simpleAgentProxy.GetNewSessionAsync(this.TestTimeoutToken); + AgentSession session = await simpleAgentProxy.CreateSessionAsync(this.TestTimeoutToken); await simpleAgentProxy.RunAsync( message: "Hello!", session, @@ -156,7 +156,7 @@ public sealed class ExternalClientTests(ITestOutputHelper outputHelper) : IDispo { // 1. Get agent and create a session DurableAIAgent agent = context.GetAgent("SimpleAgent"); - AgentSession session = await agent.GetNewSessionAsync(this.TestTimeoutToken); + AgentSession session = await agent.CreateSessionAsync(this.TestTimeoutToken); // 2. Call an agent and tell it my name await agent.RunAsync($"My name is {name}.", session); @@ -194,7 +194,7 @@ public sealed class ExternalClientTests(ITestOutputHelper outputHelper) : IDispo AIAgent workflowManagerAgentProxy = testHelper.Services.GetDurableAgentProxy("WorkflowAgent"); // Act: send a prompt to the agent - AgentSession session = await workflowManagerAgentProxy.GetNewSessionAsync(this.TestTimeoutToken); + AgentSession session = await workflowManagerAgentProxy.CreateSessionAsync(this.TestTimeoutToken); await workflowManagerAgentProxy.RunAsync( message: "Start a greeting workflow for \"John Doe\".", session, diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs index 306daa5682..f9f008c1c2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs @@ -55,7 +55,7 @@ public sealed class TimeToLiveTests(ITestOutputHelper outputHelper) : IDisposabl }); AIAgent agentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services); - AgentSession session = await agentProxy.GetNewSessionAsync(this.TestTimeoutToken); + AgentSession session = await agentProxy.CreateSessionAsync(this.TestTimeoutToken); DurableTaskClient client = testHelper.GetClient(); AgentSessionId sessionId = session.GetService(); @@ -120,7 +120,7 @@ public sealed class TimeToLiveTests(ITestOutputHelper outputHelper) : IDisposabl }); AIAgent agentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services); - AgentSession session = await agentProxy.GetNewSessionAsync(this.TestTimeoutToken); + AgentSession session = await agentProxy.CreateSessionAsync(this.TestTimeoutToken); DurableTaskClient client = testHelper.GetClient(); AgentSessionId sessionId = session.GetService(); diff --git a/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/GitHubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/GitHubCopilotAgentTests.cs index 985439bc46..855e9b4037 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/GitHubCopilotAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/GitHubCopilotAgentTests.cs @@ -96,7 +96,7 @@ public class GitHubCopilotAgentTests client, instructions: "You are a helpful assistant. Keep your answers short."); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); // Act - First turn AgentResponse response1 = await agent.RunAsync("My name is Alice.", session); @@ -123,7 +123,7 @@ public class GitHubCopilotAgentTests client1, instructions: "You are a helpful assistant. Keep your answers short."); - AgentSession session1 = await agent1.GetNewSessionAsync(); + AgentSession session1 = await agent1.CreateSessionAsync(); await agent1.RunAsync("Remember this number: 42.", session1); sessionId = ((GitHubCopilotAgentSession)session1).SessionId; @@ -137,7 +137,7 @@ public class GitHubCopilotAgentTests client2, instructions: "You are a helpful assistant. Keep your answers short."); - AgentSession session2 = await agent2.GetNewSessionAsync(sessionId); + AgentSession session2 = await agent2.CreateSessionAsync(sessionId); AgentResponse response = await agent2.RunAsync("What number did I ask you to remember?", session2); // Assert diff --git a/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs index 797325e93f..e80990de1b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs @@ -55,14 +55,14 @@ public sealed class GitHubCopilotAgentTests } [Fact] - public async Task GetNewSessionAsync_ReturnsGitHubCopilotAgentSessionAsync() + public async Task CreateSessionAsync_ReturnsGitHubCopilotAgentSessionAsync() { // Arrange CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); var agent = new GitHubCopilotAgent(copilotClient, ownsClient: false, tools: null); // Act - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); // Assert Assert.NotNull(session); @@ -70,7 +70,7 @@ public sealed class GitHubCopilotAgentTests } [Fact] - public async Task GetNewSessionAsync_WithSessionId_ReturnsSessionWithSessionIdAsync() + public async Task CreateSessionAsync_WithSessionId_ReturnsSessionWithSessionIdAsync() { // Arrange CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); @@ -78,7 +78,7 @@ public sealed class GitHubCopilotAgentTests const string TestSessionId = "test-session-id"; // Act - var session = await agent.GetNewSessionAsync(TestSessionId); + var session = await agent.CreateSessionAsync(TestSessionId); // Assert Assert.NotNull(session); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs index 4b9cbce2c4..f98dc84c81 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs @@ -175,7 +175,7 @@ public sealed class AIAgentExtensionsTests { Mock agentMock = new() { CallBase = true }; agentMock.SetupGet(x => x.Name).Returns("TestAgent"); - agentMock.Setup(x => x.GetNewSessionAsync()).ReturnsAsync(new TestAgentSession()); + agentMock.Setup(x => x.CreateSessionAsync()).ReturnsAsync(new TestAgentSession()); agentMock .Protected() .Setup>("RunCoreAsync", @@ -194,7 +194,7 @@ public sealed class AIAgentExtensionsTests { Mock agentMock = new() { CallBase = true }; agentMock.SetupGet(x => x.Name).Returns("TestAgent"); - agentMock.Setup(x => x.GetNewSessionAsync()).ReturnsAsync(new TestAgentSession()); + agentMock.Setup(x => x.CreateSessionAsync()).ReturnsAsync(new TestAgentSession()); agentMock .Protected() .Setup>("RunCoreAsync", diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs index da139ea1a7..aee568204e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs @@ -31,7 +31,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable await this.SetupTestServerAsync(); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession? session = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession? session = (ChatClientAgentSession)await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "hello"); List updates = []; @@ -62,7 +62,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable await this.SetupTestServerAsync(); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession? session = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession? session = (ChatClientAgentSession)await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "test"); List updates = []; @@ -106,7 +106,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable await this.SetupTestServerAsync(); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession? session = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession? session = (ChatClientAgentSession)await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "hello"); // Act @@ -125,7 +125,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable await this.SetupTestServerAsync(); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession chatClientSession = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession chatClientSession = (ChatClientAgentSession)await agent.CreateSessionAsync(); ChatMessage firstUserMessage = new(ChatRole.User, "First question"); // Act - First turn @@ -169,7 +169,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable await this.SetupTestServerAsync(useMultiMessageAgent: true); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession chatClientSession = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession chatClientSession = (ChatClientAgentSession)await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "Tell me a story"); List updates = []; @@ -201,7 +201,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable await this.SetupTestServerAsync(); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession chatClientSession = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession chatClientSession = (ChatClientAgentSession)await agent.CreateSessionAsync(); // Multiple user messages sent in one turn ChatMessage[] userMessages = @@ -280,7 +280,7 @@ internal sealed class FakeChatClientAgent : AIAgent public override string? Description => "A fake agent for testing"; - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new FakeInMemoryAgentSession()); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => @@ -344,7 +344,7 @@ internal sealed class FakeMultiMessageAgent : AIAgent public override string? Description => "A fake agent that sends multiple messages for testing"; - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new FakeInMemoryAgentSession()); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs index d66a63b854..92c83e06f5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs @@ -334,7 +334,7 @@ internal sealed class FakeForwardedPropsAgent : AIAgent await Task.CompletedTask; } - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new FakeInMemoryAgentSession()); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SharedStateTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SharedStateTests.cs index b80931db23..2d8742e930 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SharedStateTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SharedStateTests.cs @@ -34,7 +34,7 @@ public sealed class SharedStateTests : IAsyncDisposable await this.SetupTestServerAsync(fakeAgent); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession? session = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession? session = (ChatClientAgentSession)await agent.CreateSessionAsync(); string stateJson = JsonSerializer.Serialize(initialState); byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson); @@ -77,7 +77,7 @@ public sealed class SharedStateTests : IAsyncDisposable await this.SetupTestServerAsync(fakeAgent); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession? session = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession? session = (ChatClientAgentSession)await agent.CreateSessionAsync(); string stateJson = JsonSerializer.Serialize(initialState); byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson); @@ -119,7 +119,7 @@ public sealed class SharedStateTests : IAsyncDisposable await this.SetupTestServerAsync(fakeAgent); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession? session = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession? session = (ChatClientAgentSession)await agent.CreateSessionAsync(); string stateJson = JsonSerializer.Serialize(complexState); byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson); @@ -159,7 +159,7 @@ public sealed class SharedStateTests : IAsyncDisposable await this.SetupTestServerAsync(fakeAgent); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession? session = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession? session = (ChatClientAgentSession)await agent.CreateSessionAsync(); string stateJson = JsonSerializer.Serialize(initialState); byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson); @@ -210,7 +210,7 @@ public sealed class SharedStateTests : IAsyncDisposable await this.SetupTestServerAsync(fakeAgent); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession? session = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession? session = (ChatClientAgentSession)await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "hello"); @@ -243,7 +243,7 @@ public sealed class SharedStateTests : IAsyncDisposable await this.SetupTestServerAsync(fakeAgent); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession? session = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession? session = (ChatClientAgentSession)await agent.CreateSessionAsync(); string stateJson = JsonSerializer.Serialize(emptyState); byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson); @@ -280,7 +280,7 @@ public sealed class SharedStateTests : IAsyncDisposable await this.SetupTestServerAsync(fakeAgent); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []); - ChatClientAgentSession? session = (ChatClientAgentSession)await agent.GetNewSessionAsync(); + ChatClientAgentSession? session = (ChatClientAgentSession)await agent.CreateSessionAsync(); string stateJson = JsonSerializer.Serialize(initialState); byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson); @@ -417,7 +417,7 @@ internal sealed class FakeStateAgent : AIAgent await Task.CompletedTask; } - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new FakeInMemoryAgentSession()); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ToolCallingTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ToolCallingTests.cs index 2257d0920b..d512af28cd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ToolCallingTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ToolCallingTests.cs @@ -45,7 +45,7 @@ public sealed class ToolCallingTests : IAsyncDisposable await this.SetupTestServerAsync(serverTools: [serverTool]); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "Call the server function"); List updates = []; @@ -93,7 +93,7 @@ public sealed class ToolCallingTests : IAsyncDisposable await this.SetupTestServerAsync(serverTools: [getWeatherTool, getTimeTool]); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "What's the weather and time?"); List updates = []; @@ -134,7 +134,7 @@ public sealed class ToolCallingTests : IAsyncDisposable await this.SetupTestServerAsync(); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [clientTool]); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "Call the client function"); List updates = []; @@ -182,7 +182,7 @@ public sealed class ToolCallingTests : IAsyncDisposable await this.SetupTestServerAsync(); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [calculateTool, formatTool]); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "Calculate 5 + 3 and format 'hello'"); List updates = []; @@ -233,7 +233,7 @@ public sealed class ToolCallingTests : IAsyncDisposable await this.SetupTestServerAsync(serverTools: [serverTool]); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [clientTool]); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "Get both server and client data"); List updates = []; @@ -298,7 +298,7 @@ public sealed class ToolCallingTests : IAsyncDisposable await this.SetupTestServerAsync(serverTools: [testTool]); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "Call the test function"); List updates = []; @@ -342,7 +342,7 @@ public sealed class ToolCallingTests : IAsyncDisposable await this.SetupTestServerAsync(serverTools: [func1, func2], triggerParallelCalls: true); var chatClient = new AGUIChatClient(this._client!, "", null); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "Call both functions in parallel"); List updates = []; @@ -428,7 +428,7 @@ public sealed class ToolCallingTests : IAsyncDisposable await this.SetupTestServerAsync(serverTools: [serverTool], jsonSerializerOptions: ServerJsonContext.Default.Options); var chatClient = new AGUIChatClient(this._client!, "", null, ServerJsonContext.Default.Options); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "Get server forecast for Seattle for 5 days"); List updates = []; @@ -474,7 +474,7 @@ public sealed class ToolCallingTests : IAsyncDisposable await this.SetupTestServerAsync(); var chatClient = new AGUIChatClient(this._client!, "", null, ClientJsonContext.Default.Options); AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [clientTool]); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage userMessage = new(ChatRole.User, "Get client forecast for Portland with hourly data"); List updates = []; diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs index 50972d3aa6..544002f34b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs @@ -425,7 +425,7 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests public override string? Description => "Agent that produces multiple text chunks"; - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new TestInMemoryAgentSession()); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => @@ -515,7 +515,7 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests public override string? Description => "Test agent"; - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new TestInMemoryAgentSession()); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/TestAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/TestAgent.cs index fd5753ed7e..a597b26304 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/TestAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/TestAgent.cs @@ -11,7 +11,7 @@ internal sealed class TestAgent(string name, string description) : AIAgent public override string? Description => description; - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => new(new DummyAgentSession()); + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new DummyAgentSession()); public override ValueTask DeserializeSessionAsync( JsonElement serializedSession, diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentExtensionsTests.cs index eea65444b8..569d2c421d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentExtensionsTests.cs @@ -382,7 +382,7 @@ public class AgentExtensionsTests this._exceptionToThrow = exceptionToThrow; } - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs index 6c2be9689a..e1ff5f8cbd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs @@ -241,8 +241,8 @@ public partial class ChatClientAgentTests ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } }); - // Create a session using the agent's GetNewSessionAsync method - var session = await agent.GetNewSessionAsync(); + // Create a session using the agent's CreateSessionAsync method + var session = await agent.CreateSessionAsync(); // Act await agent.RunAsync([new(ChatRole.User, "new message")], session: session); @@ -356,7 +356,7 @@ public partial class ChatClientAgentTests ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = (_, _) => new(mockProvider.Object), ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } }); // Act - var session = await agent.GetNewSessionAsync() as ChatClientAgentSession; + var session = await agent.CreateSessionAsync() as ChatClientAgentSession; await agent.RunAsync(requestMessages, session); // Assert @@ -1296,7 +1296,7 @@ public partial class ChatClientAgentTests }); // Act - ChatClientAgentSession? session = await agent.GetNewSessionAsync() as ChatClientAgentSession; + ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession; await agent.RunStreamingAsync([new(ChatRole.User, "test")], session).ToListAsync(); // Assert @@ -1334,7 +1334,7 @@ public partial class ChatClientAgentTests }); // Act & Assert - ChatClientAgentSession? session = await agent.GetNewSessionAsync() as ChatClientAgentSession; + ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession; var exception = await Assert.ThrowsAsync(async () => await agent.RunStreamingAsync([new(ChatRole.User, "test")], session).ToListAsync()); Assert.Equal("Only the ConversationId or ChatHistoryProvider may be set, but not both and switching from one to another is not supported.", exception.Message); } @@ -1391,7 +1391,7 @@ public partial class ChatClientAgentTests }); // Act - var session = await agent.GetNewSessionAsync() as ChatClientAgentSession; + var session = await agent.CreateSessionAsync() as ChatClientAgentSession; var updates = agent.RunStreamingAsync(requestMessages, session); _ = await updates.ToAgentResponseAsync(); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs index e2b7313e7f..a854a76622 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs @@ -158,7 +158,7 @@ public class ChatClientAgent_ChatHistoryManagementTests }); // Act - ChatClientAgentSession? session = await agent.GetNewSessionAsync() as ChatClientAgentSession; + ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession; await agent.RunAsync([new(ChatRole.User, "test")], session); // Assert @@ -200,7 +200,7 @@ public class ChatClientAgent_ChatHistoryManagementTests }); // Act - ChatClientAgentSession? session = await agent.GetNewSessionAsync() as ChatClientAgentSession; + ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession; await agent.RunAsync([new(ChatRole.User, "test")], session); // Assert @@ -248,7 +248,7 @@ public class ChatClientAgent_ChatHistoryManagementTests }); // Act - ChatClientAgentSession? session = await agent.GetNewSessionAsync() as ChatClientAgentSession; + ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession; await Assert.ThrowsAsync(() => agent.RunAsync([new(ChatRole.User, "test")], session)); // Assert @@ -282,7 +282,7 @@ public class ChatClientAgent_ChatHistoryManagementTests }); // Act & Assert - ChatClientAgentSession? session = await agent.GetNewSessionAsync() as ChatClientAgentSession; + ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession; InvalidOperationException exception = await Assert.ThrowsAsync(() => agent.RunAsync([new(ChatRole.User, "test")], session)); Assert.Equal("Only the ConversationId or ChatHistoryProvider may be set, but not both and switching from one to another is not supported.", exception.Message); } @@ -335,7 +335,7 @@ public class ChatClientAgent_ChatHistoryManagementTests }); // Act - ChatClientAgentSession? session = await agent.GetNewSessionAsync() as ChatClientAgentSession; + ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession; AdditionalPropertiesDictionary additionalProperties = new(); additionalProperties.Add(mockOverrideChatHistoryProvider.Object); await agent.RunAsync([new(ChatRole.User, "test")], session, options: new AgentRunOptions { AdditionalProperties = additionalProperties }); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewSessionTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_CreateSessionTests.cs similarity index 83% rename from dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewSessionTests.cs rename to dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_CreateSessionTests.cs index c48b303fa2..86220a6462 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewSessionTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_CreateSessionTests.cs @@ -7,12 +7,12 @@ using Moq; namespace Microsoft.Agents.AI.UnitTests; /// -/// Contains unit tests for the ChatClientAgent.GetNewSessionAsync methods. +/// Contains unit tests for the ChatClientAgent.CreateSessionAsync methods. /// -public class ChatClientAgent_GetNewSessionTests +public class ChatClientAgent_CreateSessionTests { [Fact] - public async Task GetNewSession_UsesAIContextProviderFactory_IfProvidedAsync() + public async Task CreateSession_UsesAIContextProviderFactory_IfProvidedAsync() { // Arrange var mockChatClient = new Mock(); @@ -29,7 +29,7 @@ public class ChatClientAgent_GetNewSessionTests }); // Act - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); // Assert Assert.True(factoryCalled, "AIContextProviderFactory was not called."); @@ -39,7 +39,7 @@ public class ChatClientAgent_GetNewSessionTests } [Fact] - public async Task GetNewSession_UsesChatHistoryProviderFactory_IfProvidedAsync() + public async Task CreateSession_UsesChatHistoryProviderFactory_IfProvidedAsync() { // Arrange var mockChatClient = new Mock(); @@ -56,7 +56,7 @@ public class ChatClientAgent_GetNewSessionTests }); // Act - var session = await agent.GetNewSessionAsync(); + var session = await agent.CreateSessionAsync(); // Assert Assert.True(factoryCalled, "ChatHistoryProviderFactory was not called."); @@ -66,7 +66,7 @@ public class ChatClientAgent_GetNewSessionTests } [Fact] - public async Task GetNewSession_UsesChatHistoryProvider_FromTypedOverloadAsync() + public async Task CreateSession_UsesChatHistoryProvider_FromTypedOverloadAsync() { // Arrange var mockChatClient = new Mock(); @@ -74,7 +74,7 @@ public class ChatClientAgent_GetNewSessionTests var agent = new ChatClientAgent(mockChatClient.Object); // Act - var session = await agent.GetNewSessionAsync(mockChatHistoryProvider.Object); + var session = await agent.CreateSessionAsync(mockChatHistoryProvider.Object); // Assert Assert.IsType(session); @@ -83,7 +83,7 @@ public class ChatClientAgent_GetNewSessionTests } [Fact] - public async Task GetNewSession_UsesConversationId_FromTypedOverloadAsync() + public async Task CreateSession_UsesConversationId_FromTypedOverloadAsync() { // Arrange var mockChatClient = new Mock(); @@ -91,7 +91,7 @@ public class ChatClientAgent_GetNewSessionTests var agent = new ChatClientAgent(mockChatClient.Object); // Act - var session = await agent.GetNewSessionAsync(TestConversationId); + var session = await agent.CreateSessionAsync(TestConversationId); // Assert Assert.IsType(session); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_RunWithCustomOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_RunWithCustomOptionsTests.cs index cff5b6ba78..7a71871b02 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_RunWithCustomOptionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_RunWithCustomOptionsTests.cs @@ -30,7 +30,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")])); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatClientAgentRunOptions options = new(); // Act @@ -59,7 +59,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")])); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatClientAgentRunOptions options = new(); // Act @@ -88,7 +88,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")])); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage message = new(ChatRole.User, "Test message"); ChatClientAgentRunOptions options = new(); @@ -118,7 +118,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")])); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); IEnumerable messages = [new(ChatRole.User, "Message 1"), new(ChatRole.User, "Message 2")]; ChatClientAgentRunOptions options = new(); @@ -179,7 +179,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).Returns(GetAsyncUpdatesAsync()); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatClientAgentRunOptions options = new(); // Act @@ -211,7 +211,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).Returns(GetAsyncUpdatesAsync()); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatClientAgentRunOptions options = new(); // Act @@ -243,7 +243,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).Returns(GetAsyncUpdatesAsync()); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage message = new(ChatRole.User, "Test message"); ChatClientAgentRunOptions options = new(); @@ -276,7 +276,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).Returns(GetAsyncUpdatesAsync()); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); IEnumerable messages = [new ChatMessage(ChatRole.User, "Message 1"), new ChatMessage(ChatRole.User, "Message 2")]; ChatClientAgentRunOptions options = new(); @@ -324,7 +324,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")])); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatClientAgentRunOptions options = new(); // Act @@ -354,7 +354,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")])); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatClientAgentRunOptions options = new(); // Act @@ -384,7 +384,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")])); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); ChatMessage message = new(ChatRole.User, "Test message"); ChatClientAgentRunOptions options = new(); @@ -415,7 +415,7 @@ public sealed partial class ChatClientAgent_RunWithCustomOptionsTests It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")])); ChatClientAgent agent = new(mockChatClient.Object); - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); IEnumerable messages = [new(ChatRole.User, "Message 1"), new(ChatRole.User, "Message 2")]; ChatClientAgentRunOptions options = new(); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/TestAIAgent.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/TestAIAgent.cs index 0ef255155a..cadc7bdc38 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/TestAIAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/TestAIAgent.cs @@ -14,8 +14,8 @@ internal sealed class TestAIAgent : AIAgent public Func? NameFunc; public Func? DescriptionFunc; - public Func DeserializeSessionFunc = delegate { throw new NotSupportedException(); }; - public Func GetNewSessionFunc = delegate { throw new NotSupportedException(); }; + public readonly Func DeserializeSessionFunc = delegate { throw new NotSupportedException(); }; + public readonly Func CreateSessionFunc = delegate { throw new NotSupportedException(); }; public Func, AgentSession?, AgentRunOptions?, CancellationToken, Task> RunAsyncFunc = delegate { throw new NotSupportedException(); }; public Func, AgentSession?, AgentRunOptions?, CancellationToken, IAsyncEnumerable> RunStreamingAsyncFunc = delegate { throw new NotSupportedException(); }; public Func? GetServiceFunc; @@ -27,8 +27,8 @@ internal sealed class TestAIAgent : AIAgent public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => new(this.DeserializeSessionFunc(serializedSession, jsonSerializerOptions)); - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => - new(this.GetNewSessionFunc()); + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => + new(this.CreateSessionFunc()); protected override Task RunCoreAsync(IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => this.RunAsyncFunc(messages, session, options, cancellationToken); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs index ac0899e413..59636d519d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs @@ -135,7 +135,7 @@ public class AgentWorkflowBuilderTests { public override string Name => name; - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new DoubleEchoAgentSession()); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InProcessExecutionTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InProcessExecutionTests.cs index b03660d20c..bb7dbf2dda 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InProcessExecutionTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InProcessExecutionTests.cs @@ -144,7 +144,7 @@ public class InProcessExecutionTests public override string Name { get; } - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => new(new SimpleTestAgentSession()); + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new SimpleTestAgentSession()); public override ValueTask DeserializeSessionAsync(System.Text.Json.JsonElement serializedSession, System.Text.Json.JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => new(new SimpleTestAgentSession()); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RepresentationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RepresentationTests.cs index 0ebc58a395..c6f48b2724 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RepresentationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RepresentationTests.cs @@ -24,7 +24,7 @@ public class RepresentationTests private sealed class TestAgent : AIAgent { - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RoleCheckAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RoleCheckAgent.cs index 732175c8b6..611ae3caa2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RoleCheckAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RoleCheckAgent.cs @@ -19,7 +19,7 @@ internal sealed class RoleCheckAgent(bool allowOtherAssistantRoles, string? id = public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => new(new RoleCheckAgentSession()); - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => new(new RoleCheckAgentSession()); + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new RoleCheckAgentSession()); protected override Task RunCoreAsync(IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => this.RunStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/06_GroupChat_Workflow.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/06_GroupChat_Workflow.cs index fd19797a56..7941dd6b7a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/06_GroupChat_Workflow.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/06_GroupChat_Workflow.cs @@ -60,7 +60,7 @@ internal sealed class HelloAgent(string id = nameof(HelloAgent)) : AIAgent protected override string? IdCore => id; public override string? Name => id; - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new HelloAgentSession()); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/07_GroupChat_Workflow_HostAsAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/07_GroupChat_Workflow_HostAsAgent.cs index 215bd52119..950ce70806 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/07_GroupChat_Workflow_HostAsAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/07_GroupChat_Workflow_HostAsAgent.cs @@ -19,7 +19,7 @@ internal static class Step7EntryPoint for (int i = 0; i < numIterations; i++) { - AgentSession session = await agent.GetNewSessionAsync(); + AgentSession session = await agent.CreateSessionAsync(); await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(session).ConfigureAwait(false)) { if (update.RawRepresentation is WorkflowEvent) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/10_Sequential_HostAsAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/10_Sequential_HostAsAgent.cs index 280eb8c0d4..7dd454ed59 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/10_Sequential_HostAsAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/10_Sequential_HostAsAgent.cs @@ -21,7 +21,7 @@ internal static class Step10EntryPoint { AIAgent hostAgent = WorkflowInstance.AsAgent("echo-workflow", "EchoW", executionEnvironment: executionEnvironment); - AgentSession session = await hostAgent.GetNewSessionAsync(); + AgentSession session = await hostAgent.CreateSessionAsync(); foreach (string input in inputs) { AgentResponse response; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/11_Concurrent_HostAsAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/11_Concurrent_HostAsAgent.cs index cf2566de8d..71b5692d2b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/11_Concurrent_HostAsAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/11_Concurrent_HostAsAgent.cs @@ -33,7 +33,7 @@ internal static class Step11EntryPoint { AIAgent hostAgent = WorkflowInstance.AsAgent("echo-workflow", "EchoW", executionEnvironment: executionEnvironment); - AgentSession session = await hostAgent.GetNewSessionAsync(); + AgentSession session = await hostAgent.CreateSessionAsync(); foreach (string input in inputs) { AgentResponse response; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/12_HandOff_HostAsAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/12_HandOff_HostAsAgent.cs index 7e4ccac59f..e73873ca90 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/12_HandOff_HostAsAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/12_HandOff_HostAsAgent.cs @@ -69,7 +69,7 @@ internal static class Step12EntryPoint { AIAgent hostAgent = WorkflowInstance.AsAgent("echo-workflow", "EchoW", executionEnvironment: executionEnvironment); - AgentSession session = await hostAgent.GetNewSessionAsync(); + AgentSession session = await hostAgent.CreateSessionAsync(); foreach (string input in inputs) { AgentResponse response; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/13_Subworkflow_Checkpointing.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/13_Subworkflow_Checkpointing.cs index f04af720c6..368eb46067 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/13_Subworkflow_Checkpointing.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/13_Subworkflow_Checkpointing.cs @@ -32,7 +32,7 @@ internal static class Step13EntryPoint { AIAgent hostAgent = WorkflowInstance.AsAgent("echo-workflow", "EchoW", executionEnvironment: environment, includeWorkflowOutputsInResponse: true); - session ??= await hostAgent.GetNewSessionAsync(); + session ??= await hostAgent.CreateSessionAsync(); AgentResponse response; ResponseContinuationToken? continuationToken = null; do diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs index f9a4309f86..e6592605f1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs @@ -18,10 +18,10 @@ internal class TestEchoAgent(string? id = null, string? name = null, string? pre public override async ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) { - return serializedSession.Deserialize(jsonSerializerOptions) ?? await this.GetNewSessionAsync(cancellationToken); + return serializedSession.Deserialize(jsonSerializerOptions) ?? await this.CreateSessionAsync(cancellationToken); } - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) => + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new EchoAgentSession()); private static ChatMessage UpdateSession(ChatMessage message, InMemoryAgentSession? session = null) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestReplayAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestReplayAgent.cs index 82358e1198..c79e0f3a8c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestReplayAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestReplayAgent.cs @@ -45,7 +45,7 @@ public class TestReplayAgent(List? messages = null, string? id = nu return result; } - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) => new(new ReplayAgentSession()); public override ValueTask DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestRequestAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestRequestAgent.cs index 226f26ad6c..447c7f5e89 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestRequestAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestRequestAgent.cs @@ -29,7 +29,7 @@ internal sealed class TestRequestAgent(TestAgentRequestType requestType, int unp protected override string? IdCore => id; public override string? Name => name; - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken) => new(requestType switch { TestAgentRequestType.FunctionCall => new TestRequestAgentSession(), @@ -73,7 +73,7 @@ internal sealed class TestRequestAgent(TestAgentRequestType requestType, int unp where TRequest : AIContent where TResponse : AIContent { - this.LastSession = session ??= await this.GetNewSessionAsync(cancellationToken); + this.LastSession = session ??= await this.CreateSessionAsync(cancellationToken); TestRequestAgentSession traSessin = ConvertSession(session); if (traSessin.HasSentRequests) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs index 8c8a543d2c..7bab74c31d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs @@ -46,7 +46,7 @@ public class WorkflowHostSmokeTests return new(new Session(serializedSession, jsonSerializerOptions)); } - public override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateSessionAsync(CancellationToken cancellationToken = default) { return new(new Session()); } From 8d939f8ffaa44b2083ec0b959bb0885b0476f9be Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:39:44 +0900 Subject: [PATCH 14/20] Fix AzureAIClient dropping agent instructions (Responses API) (#3636) --- .../agent_framework_azure_ai/_client.py | 3 ++- .../azure-ai/tests/test_azure_ai_client.py | 25 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) 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 b70cdeafdc..58fb0e0397 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -352,9 +352,10 @@ class AzureAIClient(OpenAIBaseResponsesClient[TAzureAIClientOptions], Generic[TA args["text"] = PromptAgentDefinitionText(format=create_text_format_config(response_format)) # Combine instructions from messages and options + # instructions is accessed from chat_options since the base class excludes it from run_options combined_instructions = [ instructions - for instructions in [messages_instructions, run_options.get("instructions")] + for instructions in [messages_instructions, chat_options.get("instructions") if chat_options else None] if instructions ] if combined_instructions: 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 6277c52e9e..694bcb6604 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -695,16 +695,37 @@ async def test_agent_creation_with_instructions( mock_agent.version = "1.0" mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent) - run_options = {"model": "test-model", "instructions": "Option instructions. "} + run_options = {"model": "test-model"} + chat_options = {"instructions": "Option instructions. "} messages_instructions = "Message instructions. " - await client._get_agent_reference_or_create(run_options, messages_instructions) # type: ignore + await client._get_agent_reference_or_create(run_options, messages_instructions, chat_options) # type: ignore # Verify agent was created with combined instructions call_args = mock_project_client.agents.create_version.call_args assert call_args[1]["definition"].instructions == "Message instructions. Option instructions. " +async def test_agent_creation_with_instructions_from_chat_options( + mock_project_client: MagicMock, +) -> None: + """Test agent creation with instructions passed only via chat_options.""" + client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent") + + mock_agent = MagicMock() + mock_agent.name = "test-agent" + mock_agent.version = "1.0" + mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent) + + run_options = {"model": "test-model"} + chat_options = {"instructions": "Chat options instructions."} + + await client._get_agent_reference_or_create(run_options, None, chat_options) # type: ignore + + call_args = mock_project_client.agents.create_version.call_args + assert call_args[1]["definition"].instructions == "Chat options instructions." + + async def test_agent_creation_with_additional_args( mock_project_client: MagicMock, ) -> None: From f56218fa1e96c321d42acff8c57a214ed53a322b Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Wed, 4 Feb 2026 07:47:40 +0900 Subject: [PATCH 15/20] Python: Add explicit input, output, and workflow_output parameters to @handler, @executor and request_info (#3472) * Support specifying types via handler and executor decorators * Add handling for string types * Fix typing * Address PR feedback * All or nothing for handler typing approach * Fix mypy issues * type support for request info * Fix naming issue * Fix mypy --- .../_workflows/_agent_executor.py | 3 +- .../agent_framework/_workflows/_executor.py | 218 +++++++--- .../_workflows/_function_executor.py | 122 +++++- .../agent_framework/_workflows/_magentic.py | 2 +- .../_workflows/_orchestrator_helpers.py | 2 +- .../_workflows/_request_info_mixin.py | 214 +++++++--- .../_workflows/_typing_utils.py | 91 ++++- .../agent_framework/_workflows/_validation.py | 7 +- .../agent_framework/_workflows/_workflow.py | 7 +- .../_workflows/_workflow_context.py | 14 +- .../_workflows/_workflow_executor.py | 9 +- .../core/tests/workflow/test_executor.py | 382 ++++++++++++++++++ .../tests/workflow/test_function_executor.py | 360 +++++++++++++++++ .../tests/workflow/test_request_info_mixin.py | 168 +++++++- .../core/tests/workflow/test_typing_utils.py | 139 ++++++- .../_start-here/step1_executors_and_edges.py | 100 ++++- ...ff_with_tool_approval_checkpoint_resume.py | 50 +-- 17 files changed, 1718 insertions(+), 170 deletions(-) diff --git a/python/packages/core/agent_framework/_workflows/_agent_executor.py b/python/packages/core/agent_framework/_workflows/_agent_executor.py index 6a355fc92d..80bd4aba43 100644 --- a/python/packages/core/agent_framework/_workflows/_agent_executor.py +++ b/python/packages/core/agent_framework/_workflows/_agent_executor.py @@ -2,6 +2,7 @@ import logging import sys +import types from dataclasses import dataclass from typing import Any, cast @@ -110,7 +111,7 @@ class AgentExecutor(Executor): return self._output_response @property - def workflow_output_types(self) -> list[type[Any]]: + def workflow_output_types(self) -> list[type[Any] | types.UnionType]: # Override to declare AgentResponse as a possible output type only if enabled. if self._output_response: return [AgentResponse] diff --git a/python/packages/core/agent_framework/_workflows/_executor.py b/python/packages/core/agent_framework/_workflows/_executor.py index 49f3dafd06..18adc4b904 100644 --- a/python/packages/core/agent_framework/_workflows/_executor.py +++ b/python/packages/core/agent_framework/_workflows/_executor.py @@ -5,8 +5,9 @@ import copy import functools import inspect import logging +import types from collections.abc import Awaitable, Callable -from typing import Any, TypeVar +from typing import Any, TypeVar, overload from ..observability import create_processing_span from ._events import ( @@ -20,7 +21,7 @@ from ._model_utils import DictConvertible from ._request_info_mixin import RequestInfoMixin from ._runner_context import Message, MessageType, RunnerContext from ._shared_state import SharedState -from ._typing_utils import is_instance_of +from ._typing_utils import is_instance_of, normalize_type_to_list, resolve_type_annotation from ._workflow_context import WorkflowContext, validate_workflow_context_annotation logger = logging.getLogger(__name__) @@ -200,7 +201,9 @@ class Executor(RequestInfoMixin, DictConvertible): from builtins import type as builtin_type - self._handlers: dict[builtin_type[Any], Callable[[Any, WorkflowContext[Any, Any]], Awaitable[None]]] = {} + self._handlers: dict[ + builtin_type[Any] | types.UnionType, Callable[[Any, WorkflowContext[Any, Any]], Awaitable[None]] + ] = {} self._handler_specs: list[dict[str, Any]] = [] if not defer_discovery: self._discover_handlers() @@ -328,32 +331,26 @@ class Executor(RequestInfoMixin, DictConvertible): for attr_name in dir(self.__class__): try: attr = getattr(self.__class__, attr_name) - # Discover @handler methods - if callable(attr) and hasattr(attr, "_handler_spec"): - handler_spec = attr._handler_spec # type: ignore - message_type = handler_spec["message_type"] - - # Keep full generic types for handler registration to avoid conflicts - if self._handlers.get(message_type) is not None: - raise ValueError(f"Duplicate handler for type {message_type} in {self.__class__.__name__}") - - # Get the bound method - bound_method = getattr(self, attr_name) - self._handlers[message_type] = bound_method - - # Add to unified handler specs list - self._handler_specs.append({ - "name": handler_spec["name"], - "message_type": message_type, - "output_types": handler_spec.get("output_types", []), - "workflow_output_types": handler_spec.get("workflow_output_types", []), - "ctx_annotation": handler_spec.get("ctx_annotation"), - "source": "class_method", # Distinguish from instance handlers if needed - }) except AttributeError: - # Skip attributes that may not be accessible + # Skip attributes that may not be accessible (e.g., dynamic descriptors) continue + # Discover @handler methods + if callable(attr) and hasattr(attr, "_handler_spec"): + handler_spec = attr._handler_spec # type: ignore + message_type = handler_spec["message_type"] + + # Keep full generic types for handler registration to avoid conflicts + if self._handlers.get(message_type) is not None: + raise ValueError(f"Duplicate handler for type {message_type} in {self.__class__.__name__}") + + # Get the bound method + bound_method = getattr(self, attr_name) + self._handlers[message_type] = bound_method + + # Add to unified handler specs list + self._handler_specs.append({**handler_spec}) + def can_handle(self, message: Message) -> bool: """Check if the executor can handle a given message type. @@ -382,10 +379,10 @@ class Executor(RequestInfoMixin, DictConvertible): self, name: str, func: Callable[[Any, WorkflowContext[Any]], Awaitable[Any]], - message_type: type, + message_type: type | types.UnionType, ctx_annotation: Any, - output_types: list[type], - workflow_output_types: list[type], + output_types: list[type[Any] | types.UnionType], + workflow_output_types: list[type[Any] | types.UnionType], ) -> None: """Register a handler at instance level. @@ -407,11 +404,10 @@ class Executor(RequestInfoMixin, DictConvertible): "ctx_annotation": ctx_annotation, "output_types": output_types, "workflow_output_types": workflow_output_types, - "source": "instance_method", # Distinguish from class handlers if needed }) @property - def input_types(self) -> list[type[Any]]: + def input_types(self) -> list[type[Any] | types.UnionType]: """Get the list of input types that this executor can handle. Returns: @@ -420,13 +416,13 @@ class Executor(RequestInfoMixin, DictConvertible): return list(self._handlers.keys()) @property - def output_types(self) -> list[type[Any]]: + def output_types(self) -> list[type[Any] | types.UnionType]: """Get the list of output types that this executor can produce via send_message(). Returns: A list of the output types inferred from the handlers' WorkflowContext[T] annotations. """ - output_types: set[type[Any]] = set() + output_types: set[type[Any] | types.UnionType] = set() # Collect output types from all handlers for handler_spec in self._handler_specs + self._response_handler_specs: @@ -436,13 +432,13 @@ class Executor(RequestInfoMixin, DictConvertible): return list(output_types) @property - def workflow_output_types(self) -> list[type[Any]]: + def workflow_output_types(self) -> list[type[Any] | types.UnionType]: """Get the list of workflow output types that this executor can produce via yield_output(). Returns: A list of the workflow output types inferred from handlers' WorkflowContext[T, U] annotations. """ - output_types: set[type[Any]] = set() + output_types: set[type[Any] | types.UnionType] = set() # Collect workflow output types from all handlers for handler_spec in self._handler_specs + self._response_handler_specs: @@ -529,34 +525,134 @@ ExecutorT = TypeVar("ExecutorT", bound="Executor") ContextT = TypeVar("ContextT", bound="WorkflowContext[Any, Any]") +@overload def handler( func: Callable[[ExecutorT, Any, ContextT], Awaitable[Any]], -) -> Callable[[ExecutorT, Any, ContextT], Awaitable[Any]]: +) -> Callable[[ExecutorT, Any, ContextT], Awaitable[Any]]: ... + + +@overload +def handler( + *, + input: type | types.UnionType | str | None = None, + output: type | types.UnionType | str | None = None, + workflow_output: type | types.UnionType | str | None = None, +) -> Callable[ + [Callable[[ExecutorT, Any, ContextT], Awaitable[Any]]], + Callable[[ExecutorT, Any, ContextT], Awaitable[Any]], +]: ... + + +def handler( + func: Callable[[ExecutorT, Any, ContextT], Awaitable[Any]] | None = None, + *, + input: type | types.UnionType | str | None = None, + output: type | types.UnionType | str | None = None, + workflow_output: type | types.UnionType | str | None = None, +) -> ( + Callable[[ExecutorT, Any, ContextT], Awaitable[Any]] + | Callable[ + [Callable[[ExecutorT, Any, ContextT], Awaitable[Any]]], + Callable[[ExecutorT, Any, ContextT], Awaitable[Any]], + ] +): """Decorator to register a handler for an executor. + Type information can be provided in two mutually exclusive ways: + + 1. **Introspection** (default): Types are inferred from function signature annotations. + Use type annotations on the message parameter and WorkflowContext generic parameters. + + 2. **Explicit parameters**: Types are specified via decorator parameters (input, output, + workflow_output). When ANY explicit parameter is provided, ALL types must come from + explicit parameters - introspection is completely disabled. The ``input`` parameter + is required; ``output`` and ``workflow_output`` are optional (default to no outputs). + Args: - func: The function to decorate. Can be None when used without parameters. + func: The function to decorate. Can be None when used with parameters. + input: Explicit input type(s) for this handler. Required when using explicit mode. + Supports union types (e.g., ``str | int``) and string forward references. + output: Explicit output type(s) that can be sent via ``ctx.send_message()``. + Optional; defaults to no outputs if not specified. + workflow_output: Explicit output type(s) that can be yielded via ``ctx.yield_output()``. + Optional; defaults to no outputs if not specified. Returns: The decorated function with handler metadata. Example: - @handler - async def handle_string(self, message: str, ctx: WorkflowContext[str]) -> None: - ... + .. code-block:: python - @handler - async def handle_data(self, message: dict, ctx: WorkflowContext[str | int]) -> None: - ... + # Mode 1: Introspection - types from annotations + @handler + async def handle_string(self, message: str, ctx: WorkflowContext[str]) -> None: ... + + + # Mode 2: Explicit types - ALL types from decorator params + # Note: No type annotations on function parameters when using explicit types + @handler(input=str | int, output=bool) + async def handle_data(self, message, ctx): ... + + + # Explicit with string forward references + @handler(input="MyCustomType | int", output="ResponseType") + async def handle_custom(self, message, ctx): ... + + + # Explicit with all three type parameters + @handler(input=str, output=int, workflow_output=bool) + async def handle_full(self, message, ctx): + await ctx.send_message(42) # int - matches output + await ctx.yield_output(True) # bool - matches workflow_output """ def decorator( func: Callable[[ExecutorT, Any, ContextT], Awaitable[Any]], ) -> Callable[[ExecutorT, Any, ContextT], Awaitable[Any]]: - # Extract the message type and validate using unified validation - message_type, ctx_annotation, inferred_output_types, inferred_workflow_output_types = ( - _validate_handler_signature(func) - ) + # Check if ANY explicit type parameter was provided - if so, use ONLY explicit params. + # This is "all or nothing" - no mixing of explicit params with introspection. + use_explicit_types = input is not None or output is not None or workflow_output is not None + + if use_explicit_types: + # Resolve string forward references using the function's globals + resolved_input_type = resolve_type_annotation(input, func.__globals__) if input is not None else None + resolved_output_type = resolve_type_annotation(output, func.__globals__) if output is not None else None + resolved_workflow_output_type = ( + resolve_type_annotation(workflow_output, func.__globals__) if workflow_output is not None else None + ) + + # Validate signature structure (correct number of params, ctx is WorkflowContext) + # but skip type extraction since we're using explicit types + _validate_handler_signature(func, skip_message_annotation=True) + + # Use explicit types only - missing params default to empty + message_type = resolved_input_type + if message_type is None: + raise ValueError(f"Handler {func.__name__} with explicit type parameters must specify 'input' type") + + final_output_types = normalize_type_to_list(resolved_output_type) if resolved_output_type else [] + final_workflow_output_types = ( + normalize_type_to_list(resolved_workflow_output_type) if resolved_workflow_output_type else [] + ) + # Get ctx_annotation for consistency (even though types come from explicit params) + ctx_annotation = ( + inspect.signature(func).parameters[list(inspect.signature(func).parameters.keys())[2]].annotation + ) + else: + # Use introspection for ALL types - no explicit params provided + introspected_message_type, ctx_annotation, inferred_output_types, inferred_workflow_output_types = ( + _validate_handler_signature(func, skip_message_annotation=False) + ) + + message_type = introspected_message_type + if message_type is None: + raise ValueError( + f"Handler {func.__name__} requires either a message parameter type annotation " + "or explicit type parameters (input, output, workflow_output)" + ) + + final_output_types = inferred_output_types + final_workflow_output_types = inferred_workflow_output_types # Get signature for preservation sig = inspect.signature(func) @@ -574,14 +670,19 @@ def handler( "name": func.__name__, "message_type": message_type, # Keep output_types and workflow_output_types in spec for validators - "output_types": inferred_output_types, - "workflow_output_types": inferred_workflow_output_types, + "output_types": final_output_types, + "workflow_output_types": final_workflow_output_types, "ctx_annotation": ctx_annotation, } return wrapper - return decorator(func) + # Handle both @handler and @handler(...) usage patterns + if func is not None: + # Called as @handler without parentheses + return decorator(func) + # Called as @handler(...) with parentheses + return decorator # endregion: Handler Decorator @@ -589,14 +690,21 @@ def handler( # region Handler Validation -def _validate_handler_signature(func: Callable[..., Any]) -> tuple[type, Any, list[type[Any]], list[type[Any]]]: +def _validate_handler_signature( + func: Callable[..., Any], + *, + skip_message_annotation: bool = False, +) -> tuple[type | None, Any, list[type[Any] | types.UnionType], list[type[Any] | types.UnionType]]: """Validate function signature for executor functions. Args: func: The function to validate + skip_message_annotation: If True, skip validation that message parameter has a type + annotation. Used when input_type is explicitly provided to the @handler decorator. Returns: - Tuple of (message_type, ctx_annotation, output_types, workflow_output_types) + Tuple of (message_type, ctx_annotation, output_types, workflow_output_types). + message_type may be None if skip_message_annotation is True and no annotation exists. Raises: ValueError: If the function signature is invalid @@ -609,9 +717,9 @@ def _validate_handler_signature(func: Callable[..., Any]) -> tuple[type, Any, li if len(params) != expected_counts: raise ValueError(f"Handler {func.__name__} must have {param_description}. Got {len(params)} parameters.") - # Check message parameter has type annotation + # Check message parameter has type annotation (unless skipped) message_param = params[1] - if message_param.annotation == inspect.Parameter.empty: + if not skip_message_annotation and message_param.annotation == inspect.Parameter.empty: raise ValueError(f"Handler {func.__name__} must have a type annotation for the message parameter") # Validate ctx parameter is WorkflowContext and extract type args @@ -620,7 +728,7 @@ def _validate_handler_signature(func: Callable[..., Any]) -> tuple[type, Any, li ctx_param.annotation, f"parameter '{ctx_param.name}'", "Handler" ) - message_type = message_param.annotation + message_type = message_param.annotation if message_param.annotation != inspect.Parameter.empty else None ctx_annotation = ctx_param.annotation return message_type, ctx_annotation, output_types, workflow_output_types diff --git a/python/packages/core/agent_framework/_workflows/_function_executor.py b/python/packages/core/agent_framework/_workflows/_function_executor.py index d7b68c10fd..cac77d8173 100644 --- a/python/packages/core/agent_framework/_workflows/_function_executor.py +++ b/python/packages/core/agent_framework/_workflows/_function_executor.py @@ -18,11 +18,13 @@ Design Pattern: import asyncio import inspect import sys +import types import typing from collections.abc import Awaitable, Callable from typing import Any from ._executor import Executor +from ._typing_utils import normalize_type_to_list, resolve_type_annotation from ._workflow_context import WorkflowContext, validate_workflow_context_annotation if sys.version_info >= (3, 11): @@ -41,12 +43,32 @@ class FunctionExecutor(Executor): blocking the event loop. """ - def __init__(self, func: Callable[..., Any], id: str | None = None): + def __init__( + self, + func: Callable[..., Any], + id: str | None = None, + *, + input: type | types.UnionType | str | None = None, + output: type | types.UnionType | str | None = None, + workflow_output: type | types.UnionType | str | None = None, + ): """Initialize the FunctionExecutor with a user-defined function. Args: func: The function to wrap as an executor (can be sync or async) id: Optional executor ID. If None, uses the function name. + input: Optional explicit input type(s) for this executor. Supports union types + (e.g., ``str | int``) and string forward references (e.g., ``"MyType | int"``). + When provided, takes precedence over introspection from the function's message + parameter annotation. + output: Optional explicit output type(s) that can be sent via ``ctx.send_message()``. + Supports union types (e.g., ``str | int``) and string forward references. + When provided, takes precedence over introspection from the ``WorkflowContext`` + first generic parameter (T_Out). + workflow_output: Optional explicit output type(s) that can be yielded via + ``ctx.yield_output()``. Supports union types (e.g., ``str | int``) and string + forward references. When provided, takes precedence over introspection from the + ``WorkflowContext`` second generic parameter (T_W_Out). Raises: ValueError: If func is a staticmethod or classmethod (use @handler on instance methods instead) @@ -60,8 +82,37 @@ class FunctionExecutor(Executor): f"or create an Executor subclass and use @handler on instance methods instead." ) + # Resolve string forward references using the function's globals + resolved_input_type = resolve_type_annotation(input, func.__globals__) if input is not None else None + resolved_output_type = resolve_type_annotation(output, func.__globals__) if output is not None else None + resolved_workflow_output_type = ( + resolve_type_annotation(workflow_output, func.__globals__) if workflow_output is not None else None + ) + # Validate function signature and extract types - message_type, ctx_annotation, output_types, workflow_output_types = _validate_function_signature(func) + introspected_message_type, ctx_annotation, inferred_output_types, inferred_workflow_output_types = ( + _validate_function_signature(func, skip_message_annotation=resolved_input_type is not None) + ) + + # Use explicit types if provided, otherwise fall back to introspection + message_type = resolved_input_type if resolved_input_type is not None else introspected_message_type + output_types: list[type[Any] | types.UnionType] = ( + normalize_type_to_list(resolved_output_type) + if resolved_output_type is not None + else list(inferred_output_types) + ) + final_workflow_output_types: list[type[Any] | types.UnionType] = ( + normalize_type_to_list(resolved_workflow_output_type) + if resolved_workflow_output_type is not None + else list(inferred_workflow_output_types) + ) + + # Validate that we have a message type - provides a clear error if type information is missing + if message_type is None: + raise ValueError( + f"Function {func.__name__} requires either a message parameter type annotation " + "or an explicit input_type parameter" + ) # Store the original function self._original_func = func @@ -106,7 +157,7 @@ class FunctionExecutor(Executor): message_type=message_type, ctx_annotation=ctx_annotation, output_types=output_types, - workflow_output_types=workflow_output_types, + workflow_output_types=final_workflow_output_types, ) # Now we can safely call _discover_handlers (it won't find any class-level handlers) @@ -127,11 +178,22 @@ def executor(func: Callable[..., Any]) -> FunctionExecutor: ... @overload -def executor(*, id: str | None = None) -> Callable[[Callable[..., Any]], FunctionExecutor]: ... +def executor( + *, + id: str | None = None, + input: type | types.UnionType | str | None = None, + output: type | types.UnionType | str | None = None, + workflow_output: type | types.UnionType | str | None = None, +) -> Callable[[Callable[..., Any]], FunctionExecutor]: ... def executor( - func: Callable[..., Any] | None = None, *, id: str | None = None + func: Callable[..., Any] | None = None, + *, + id: str | None = None, + input: type | types.UnionType | str | None = None, + output: type | types.UnionType | str | None = None, + workflow_output: type | types.UnionType | str | None = None, ) -> Callable[[Callable[..., Any]], FunctionExecutor] | FunctionExecutor: """Decorator that converts a standalone function into a FunctionExecutor instance. @@ -162,6 +224,25 @@ def executor( return data.upper() + # Using explicit types (takes precedence over introspection): + # Note: No type annotations on function parameters when using explicit types + @executor(id="my_executor", input=str | int, output=bool) + async def process(message, ctx): + await ctx.send_message(True) + + + # Using string forward references: + @executor(input="MyCustomType | int", output="ResponseType") + async def process(message, ctx): ... + + + # Specifying both output types (send_message and yield_output): + @executor(input=str, output=int, workflow_output=bool) + async def process(message, ctx): + await ctx.send_message(42) # int - matches output + await ctx.yield_output(True) # bool - matches workflow_output + + # For class-based executors, use @handler instead: class MyExecutor(Executor): def __init__(self): @@ -174,6 +255,18 @@ def executor( Args: func: The function to decorate (when used without parentheses) id: Optional custom ID for the executor. If None, uses the function name. + input: Optional explicit input type(s) for this executor. Supports union types + (e.g., ``str | int``) and string forward references (e.g., ``"MyType | int"``). + When provided, takes precedence over introspection from the function's message + parameter annotation. + output: Optional explicit output type(s) that can be sent via ``ctx.send_message()``. + Supports union types (e.g., ``str | int``) and string forward references. + When provided, takes precedence over introspection from the ``WorkflowContext`` + first generic parameter (T_Out). + workflow_output: Optional explicit output type(s) that can be yielded via + ``ctx.yield_output()``. Supports union types (e.g., ``str | int``) and string + forward references. When provided, takes precedence over introspection from the + ``WorkflowContext`` second generic parameter (T_W_Out). Returns: A FunctionExecutor instance that can be wired into a Workflow. @@ -183,7 +276,7 @@ def executor( """ def wrapper(func: Callable[..., Any]) -> FunctionExecutor: - return FunctionExecutor(func, id=id) + return FunctionExecutor(func, id=id, input=input, output=output, workflow_output=workflow_output) # If func is provided, this means @executor was used without parentheses if func is not None: @@ -198,14 +291,21 @@ def executor( # region Function Validation -def _validate_function_signature(func: Callable[..., Any]) -> tuple[type, Any, list[type[Any]], list[type[Any]]]: +def _validate_function_signature( + func: Callable[..., Any], + *, + skip_message_annotation: bool = False, +) -> tuple[type | None, Any, list[type[Any] | types.UnionType], list[type[Any] | types.UnionType]]: """Validate function signature for executor functions. Args: func: The function to validate + skip_message_annotation: If True, skip validation that message parameter has a type + annotation. Used when input is explicitly provided to the @executor decorator. Returns: - Tuple of (message_type, ctx_annotation, output_types, workflow_output_types) + Tuple of (message_type, ctx_annotation, output_types, workflow_output_types). + message_type may be None if skip_message_annotation is True and no annotation exists. Raises: ValueError: If the function signature is invalid @@ -220,13 +320,15 @@ def _validate_function_signature(func: Callable[..., Any]) -> tuple[type, Any, l f"Function instance {func.__name__} must have {param_description}. Got {len(params)} parameters." ) - # Check message parameter has type annotation + # Check message parameter has type annotation (unless skipped) message_param = params[0] - if message_param.annotation == inspect.Parameter.empty: + if not skip_message_annotation and message_param.annotation == inspect.Parameter.empty: raise ValueError(f"Function instance {func.__name__} must have a type annotation for the message parameter") type_hints = typing.get_type_hints(func) message_type = type_hints.get(message_param.name, message_param.annotation) + if message_type == inspect.Parameter.empty: + message_type = None # Check if there's a context parameter if len(params) == 2: diff --git a/python/packages/core/agent_framework/_workflows/_magentic.py b/python/packages/core/agent_framework/_workflows/_magentic.py index 8503aae2ce..eff87fd5f0 100644 --- a/python/packages/core/agent_framework/_workflows/_magentic.py +++ b/python/packages/core/agent_framework/_workflows/_magentic.py @@ -1367,7 +1367,7 @@ class MagenticBuilder: - `.with_plan_review()` - Review and approve/revise plans before execution - `.with_human_input_on_stall()` - Intervene when workflow stalls - - Tool approval via `FunctionApprovalRequestContent` - Approve individual tool calls + - Tool approval via `function_approval_request` - Approve individual tool calls These emit `MagenticHumanInterventionRequest` events that provide structured decision options (APPROVE, REVISE, CONTINUE, REPLAN, GUIDANCE) appropriate diff --git a/python/packages/core/agent_framework/_workflows/_orchestrator_helpers.py b/python/packages/core/agent_framework/_workflows/_orchestrator_helpers.py index 09f118a6c6..82f6532ea2 100644 --- a/python/packages/core/agent_framework/_workflows/_orchestrator_helpers.py +++ b/python/packages/core/agent_framework/_workflows/_orchestrator_helpers.py @@ -23,7 +23,7 @@ def clean_conversation_for_handoff(conversation: list[ChatMessage]) -> list[Chat This creates a cleaned copy removing ALL tool-related content. Removes: - - FunctionApprovalRequestContent and FunctionCallContent from assistant messages + - function_approval_request and function_call from assistant messages - Tool response messages (Role.TOOL) - Messages with only tool calls and no text diff --git a/python/packages/core/agent_framework/_workflows/_request_info_mixin.py b/python/packages/core/agent_framework/_workflows/_request_info_mixin.py index d4c5f6d2bc..ac7132e2fe 100644 --- a/python/packages/core/agent_framework/_workflows/_request_info_mixin.py +++ b/python/packages/core/agent_framework/_workflows/_request_info_mixin.py @@ -4,13 +4,21 @@ import contextlib import functools import inspect import logging +import sys +import types from builtins import type as builtin_type from collections.abc import Awaitable, Callable -from typing import TYPE_CHECKING, Any, TypeVar +from types import UnionType +from typing import TYPE_CHECKING, Any, TypeVar, cast -from ._typing_utils import is_instance_of, is_type_compatible +from ._typing_utils import is_instance_of, is_type_compatible, normalize_type_to_list, resolve_type_annotation from ._workflow_context import WorkflowContext, validate_workflow_context_annotation +if sys.version_info >= (3, 11): + from typing import overload # pragma: no cover +else: + from typing_extensions import overload # pragma: no cover + if TYPE_CHECKING: from ._executor import Executor @@ -86,15 +94,7 @@ class RequestInfoMixin: ) self._response_handlers[request_type, response_type] = getattr(self, attr_name) - self._response_handler_specs.append({ - "name": handler_spec["name"], - "request_type": request_type, - "response_type": response_type, - "output_types": handler_spec.get("output_types", []), - "workflow_output_types": handler_spec.get("workflow_output_types", []), - "ctx_annotation": handler_spec.get("ctx_annotation"), - "source": "class_method", # Distinguish from instance handlers if needed - }) + self._response_handler_specs.append({**handler_spec, "source": "class_method"}) except AttributeError: continue # Skip non-callable attributes or those without handler spec @@ -110,13 +110,64 @@ ContextT = TypeVar("ContextT", bound="WorkflowContext[Any, Any]") # region Handler Decorator +@overload def response_handler( func: Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]], -) -> Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]]: +) -> Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]]: ... + + +@overload +def response_handler( + func: None = None, + *, + request: type | types.UnionType | str | None = None, + response: type | types.UnionType | str | None = None, + output: type | types.UnionType | str | None = None, + workflow_output: type | types.UnionType | str | None = None, +) -> Callable[ + [Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]]], + Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]], +]: ... + + +def response_handler( + func: Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]] | None = None, + *, + request: type | types.UnionType | str | None = None, + response: type | types.UnionType | str | None = None, + output: type | types.UnionType | str | None = None, + workflow_output: type | types.UnionType | str | None = None, +) -> ( + Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]] + | Callable[ + [Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]]], + Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]], + ] +): """Decorator to register a handler to handle responses for a request. + Type information can be provided in two mutually exclusive ways: + + 1. **Introspection** (default): Types are inferred from function signature annotations. + Use type annotations on the original_request, response parameters and WorkflowContext + generic parameters. + + 2. **Explicit parameters**: Types are specified via decorator parameters (request, response, + output, workflow_output). When ANY explicit parameter is provided, ALL types must come + from explicit parameters - introspection is completely disabled. The ``request`` and + ``response`` parameters are required; ``output`` and ``workflow_output`` are optional + (default to no outputs). + Args: - func: The function to decorate. + func: The function to decorate. Can be None when used with parameters. + request: Explicit request type for this handler (the original_request parameter type). + Required when using explicit mode. Supports union types and string forward references. + response: Explicit response type for this handler (the response parameter type). + Required when using explicit mode. Supports union types and string forward references. + output: Explicit output type(s) that can be sent via ``ctx.send_message()``. + Optional; defaults to no outputs if not specified. + workflow_output: Explicit output type(s) that can be yielded via ``ctx.yield_output()``. + Optional; defaults to no outputs if not specified. Returns: The decorated function with handler metadata. @@ -124,6 +175,7 @@ def response_handler( Example: .. code-block:: python + # Mode 1: Introspection - types from annotations @handler async def run(self, message: int, context: WorkflowContext[str]) -> None: # Example of a handler that sends a request @@ -143,31 +195,84 @@ def response_handler( ... - @response_handler - async def handle_response( - self, - original_request: CustomRequest, - response: dict, - context: WorkflowContext[int], - ) -> None: - # Example of a response handler for a request expecting a dict response - ... + # Mode 2: Explicit types - ALL types from decorator params + # Note: No type annotations on function parameters when using explicit types + @response_handler(request=CustomRequest, response=dict, output=int) + async def handle_response(self, original_request, response, context): + # Example of a response handler with explicit types + await context.send_message(42) + + + # Explicit with string forward references + @response_handler(request="MyRequest", response="MyResponse") + async def handle_response(self, original_request, response, context): ... """ def decorator( func: Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]], ) -> Callable[[ExecutorT, Any, Any, ContextT], Awaitable[None]]: - request_type, response_type, ctx_annotation, inferred_output_types, inferred_workflow_output_types = ( - _validate_response_handler_signature(func) + # Check if ANY explicit type parameter was provided - if so, use ONLY explicit params. + # This is "all or nothing" - no mixing of explicit params with introspection. + use_explicit_types = ( + request is not None or response is not None or output is not None or workflow_output is not None ) + if use_explicit_types: + # Resolve string forward references using the function's globals + resolved_request_type = resolve_type_annotation(request, func.__globals__) if request is not None else None + resolved_response_type = ( + resolve_type_annotation(response, func.__globals__) if response is not None else None + ) + resolved_output_type = resolve_type_annotation(output, func.__globals__) if output is not None else None + resolved_workflow_output_type = ( + resolve_type_annotation(workflow_output, func.__globals__) if workflow_output is not None else None + ) + + # Validate signature structure but skip type extraction + _validate_response_handler_signature(func, skip_annotations=True) + + # Validate required parameters + if resolved_request_type is None: + raise ValueError( + f"Response handler {func.__name__} with explicit type parameters must specify 'request' type" + ) + if resolved_response_type is None: + raise ValueError( + f"Response handler {func.__name__} with explicit type parameters must specify 'response' type" + ) + + final_request_type = resolved_request_type + final_response_type = resolved_response_type + final_output_types = normalize_type_to_list(resolved_output_type) if resolved_output_type else [] + final_workflow_output_types = ( + normalize_type_to_list(resolved_workflow_output_type) if resolved_workflow_output_type else [] + ) + # Get ctx_annotation for consistency + ctx_annotation = ( + inspect.signature(func).parameters[list(inspect.signature(func).parameters.keys())[3]].annotation + ) + if ctx_annotation == inspect.Parameter.empty: + ctx_annotation = None + else: + # Use introspection - all types from annotations + ( + inferred_request_type, + inferred_response_type, + ctx_annotation, + final_output_types, + final_workflow_output_types, + ) = _validate_response_handler_signature(func) + # In introspection mode, validation ensures these are not None (raises ValueError if missing) + final_request_type = cast(type, inferred_request_type) + final_response_type = cast(type, inferred_response_type) + # Get signature for preservation sig = inspect.signature(func) @functools.wraps(func) - async def wrapper(self: ExecutorT, original_request: Any, response: Any, ctx: ContextT) -> Any: + async def wrapper(self: ExecutorT, original_request: Any, response_msg: Any, ctx: ContextT) -> Any: """Wrapper function to call the handler.""" - return await func(self, original_request, response, ctx) + return await func(self, original_request, response_msg, ctx) # Preserve the original function signature for introspection during validation with contextlib.suppress(AttributeError, TypeError): @@ -175,17 +280,22 @@ def response_handler( wrapper._response_handler_spec = { # type: ignore "name": func.__name__, - "request_type": request_type, - "response_type": response_type, + "request_type": final_request_type, + "response_type": final_response_type, # Keep output_types and workflow_output_types in spec for validators - "output_types": inferred_output_types, - "workflow_output_types": inferred_workflow_output_types, + "output_types": final_output_types, + "workflow_output_types": final_workflow_output_types, "ctx_annotation": ctx_annotation, } return wrapper - return decorator(func) + # If func is provided, this means @response_handler was used without parentheses + if func is not None: + return decorator(func) + + # Otherwise, return the wrapper for @response_handler(...) with parameters + return decorator # endregion: Handler Decorator @@ -195,14 +305,19 @@ def response_handler( def _validate_response_handler_signature( func: Callable[..., Any], -) -> tuple[type, type, Any, list[type[Any]], list[type[Any]]]: - """Validate function signature for executor functions. + *, + skip_annotations: bool = False, +) -> tuple[type | None, type | None, Any, list[type[Any] | UnionType], list[type[Any] | UnionType]]: + """Validate function signature for response handler functions. Args: func: The function to validate + skip_annotations: If True, skip validation that request/response parameters have type + annotations. Used when types are explicitly provided to the @response_handler decorator. Returns: - Tuple of (request_type, response_type, ctx_annotation, output_types, workflow_output_types) + Tuple of (request_type, response_type, ctx_annotation, output_types, workflow_output_types). + request_type and response_type may be None if skip_annotations is True and no annotations exist. Raises: ValueError: If the function signature is invalid @@ -215,33 +330,38 @@ def _validate_response_handler_signature( # to the original request when registering the handler, while maintaining # the order of parameters as if the response handler is a normal handler. expected_counts = 4 # self, original_request, message, ctx - param_description = "(self, original_request: TRequest, message: TResponse, ctx: WorkflowContext[U, V])" + param_description = "(self, original_request, response, ctx)" if len(params) != expected_counts: raise ValueError( f"Response handler {func.__name__} must have {param_description}. Got {len(params)} parameters." ) - # Check original_request parameter exists + # Check original_request parameter exists and has annotation (unless skipped) original_request_param = params[1] - if original_request_param.annotation == inspect.Parameter.empty: + if not skip_annotations and original_request_param.annotation == inspect.Parameter.empty: raise ValueError( f"Response handler {func.__name__} must have a type annotation for the original_request parameter" ) - # Check response parameter has type annotation + # Check response parameter has type annotation (unless skipped) response_param = params[2] - if response_param.annotation == inspect.Parameter.empty: - raise ValueError(f"Response handler {func.__name__} must have a type annotation for the message parameter") + if not skip_annotations and response_param.annotation == inspect.Parameter.empty: + raise ValueError(f"Response handler {func.__name__} must have a type annotation for the response parameter") - # Validate ctx parameter is WorkflowContext and extract type args + # Validate ctx parameter is WorkflowContext and extract type args (if annotated) ctx_param = params[3] - output_types, workflow_output_types = validate_workflow_context_annotation( - ctx_param.annotation, f"parameter '{ctx_param.name}'", "Response handler" - ) + if ctx_param.annotation != inspect.Parameter.empty: + output_types, workflow_output_types = validate_workflow_context_annotation( + ctx_param.annotation, f"parameter '{ctx_param.name}'", "Response handler" + ) + else: + output_types, workflow_output_types = [], [] - request_type = original_request_param.annotation - response_type = response_param.annotation - ctx_annotation = ctx_param.annotation + request_type = ( + original_request_param.annotation if original_request_param.annotation != inspect.Parameter.empty else None + ) + response_type = response_param.annotation if response_param.annotation != inspect.Parameter.empty else None + ctx_annotation = ctx_param.annotation if ctx_param.annotation != inspect.Parameter.empty else None return request_type, response_type, ctx_annotation, output_types, workflow_output_types diff --git a/python/packages/core/agent_framework/_workflows/_typing_utils.py b/python/packages/core/agent_framework/_workflows/_typing_utils.py index 5619fb9bf3..3fe42fd053 100644 --- a/python/packages/core/agent_framework/_workflows/_typing_utils.py +++ b/python/packages/core/agent_framework/_workflows/_typing_utils.py @@ -1,14 +1,99 @@ # Copyright (c) Microsoft. All rights reserved. -import logging from types import UnionType from typing import Any, TypeVar, Union, cast, get_args, get_origin -logger = logging.getLogger(__name__) - T = TypeVar("T") +def resolve_type_annotation( + type_annotation: type[Any] | UnionType | str | None, + globalns: dict[str, Any] | None = None, + localns: dict[str, Any] | None = None, +) -> type[Any] | UnionType | None: + """Resolve a type annotation, including string forward references. + + Args: + type_annotation: A type, union type, string forward reference, or None + globalns: Global namespace for resolving forward references (typically func.__globals__) + localns: Local namespace for resolving forward references + + Returns: + The resolved type annotation. For string annotations, evaluates them in the + provided namespace. Returns None if type_annotation is None. + + Raises: + NameError: If a forward reference cannot be resolved in the provided namespaces + SyntaxError: If a string annotation contains invalid Python syntax + + Note: + This function uses eval() to resolve string type annotations. This is the same + approach used by Python's typing.get_type_hints() and typing.ForwardRef internally. + Security is managed by: (1) strings come from decorator parameters in source code, + not runtime user input, and (2) the eval namespace is restricted to the function's + module globals plus Union/Optional from typing. + + Examples: + - resolve_type_annotation(str) -> str + - resolve_type_annotation("str | int", {"str": str, "int": int}) -> str | int + - resolve_type_annotation("MyClass", {"MyClass": MyClass}) -> MyClass + """ + if type_annotation is None: + return None + + if isinstance(type_annotation, str): + # Resolve string forward reference by evaluating it. + # This uses eval() which is the same approach as Python's typing.get_type_hints() + # and typing.ForwardRef._evaluate(). The namespace is restricted to the function's + # globals plus typing constructs, and input comes from developer source code. + eval_globalns = globalns.copy() if globalns else {} + eval_globalns.setdefault("Union", Union) + eval_globalns.setdefault("Optional", __import__("typing").Optional) + + try: + return cast( + "type[Any] | UnionType", + eval(type_annotation, eval_globalns, localns), # noqa: S307 # nosec B307 + ) + except NameError as e: + raise NameError( + f"Could not resolve type annotation '{type_annotation}'. " + f"Make sure the type is defined or imported. Original error: {e}" + ) from e + + return type_annotation + + +def normalize_type_to_list(type_annotation: type[Any] | UnionType | None) -> list[type[Any] | UnionType]: + """Normalize a type annotation (possibly a union) to a list of concrete types. + + Args: + type_annotation: A type, union type (using | or Union[]), or None + + Returns: + A list of types. For union types, returns all members. + For None, returns an empty list. + For Optional[T] (Union[T, None]), returns [T, type(None)]. + + Examples: + - normalize_type_to_list(str) -> [str] + - normalize_type_to_list(str | int) -> [str, int] + - normalize_type_to_list(Union[str, int]) -> [str, int] + - normalize_type_to_list(None) -> [] + """ + if type_annotation is None: + return [] + + origin = get_origin(type_annotation) + + # Handle Union types (str | int or Union[str, int]) + if origin is Union or origin is UnionType: + return list(get_args(type_annotation)) + + # Single type + return [type_annotation] + + def is_instance_of(data: Any, target_type: type | UnionType | Any) -> bool: """Check if the data is an instance of the target type. diff --git a/python/packages/core/agent_framework/_workflows/_validation.py b/python/packages/core/agent_framework/_workflows/_validation.py index fc59bb94e1..ff8a74028d 100644 --- a/python/packages/core/agent_framework/_workflows/_validation.py +++ b/python/packages/core/agent_framework/_workflows/_validation.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. import logging +import types from collections import defaultdict from collections.abc import Sequence from enum import Enum @@ -55,8 +56,8 @@ class TypeCompatibilityError(WorkflowValidationError): self, source_executor_id: str, target_executor_id: str, - source_types: list[type[Any]], - target_types: list[type[Any]], + source_types: list[type[Any] | types.UnionType], + target_types: list[type[Any] | types.UnionType], ): # Use a placeholder for incompatible types - will be computed in WorkflowGraphValidator super().__init__( @@ -253,7 +254,7 @@ class WorkflowGraphValidator: # Check if any source output type is compatible with any target input type compatible = False - compatible_pairs: list[tuple[type[Any], type[Any]]] = [] + compatible_pairs: list[tuple[type[Any] | types.UnionType, type[Any] | types.UnionType]] = [] for source_type in source_output_types: for target_type in target_input_types: diff --git a/python/packages/core/agent_framework/_workflows/_workflow.py b/python/packages/core/agent_framework/_workflows/_workflow.py index e7b744265d..bd14dc6bcc 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow.py +++ b/python/packages/core/agent_framework/_workflows/_workflow.py @@ -5,6 +5,7 @@ import functools import hashlib import json import logging +import types import uuid from collections.abc import AsyncIterable, Awaitable, Callable from typing import Any @@ -815,7 +816,7 @@ class Workflow(DictConvertible): return self._graph_signature_hash @property - def input_types(self) -> list[type[Any]]: + def input_types(self) -> list[type[Any] | types.UnionType]: """Get the input types of the workflow. The input types are the list of input types of the start executor. @@ -827,7 +828,7 @@ class Workflow(DictConvertible): return start_executor.input_types @property - def output_types(self) -> list[type[Any]]: + def output_types(self) -> list[type[Any] | types.UnionType]: """Get the output types of the workflow. The output types are the list of all workflow output types from executors @@ -836,7 +837,7 @@ class Workflow(DictConvertible): Returns: A list of output types that the workflow can produce. """ - output_types: set[type[Any]] = set() + output_types: set[type[Any] | types.UnionType] = set() for executor in self.executors.values(): workflow_output_types = executor.workflow_output_types diff --git a/python/packages/core/agent_framework/_workflows/_workflow_context.py b/python/packages/core/agent_framework/_workflows/_workflow_context.py index 893f0ccfe9..708cdf3c51 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_context.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_context.py @@ -38,7 +38,9 @@ T_W_Out = TypeVar("T_W_Out", default=Never) logger = logging.getLogger(__name__) -def infer_output_types_from_ctx_annotation(ctx_annotation: Any) -> tuple[list[type[Any]], list[type[Any]]]: +def infer_output_types_from_ctx_annotation( + ctx_annotation: Any, +) -> tuple[list[type[Any] | UnionType], list[type[Any] | UnionType]]: """Infer message types and workflow output types from the WorkflowContext generic parameters. Examples: @@ -81,8 +83,8 @@ def infer_output_types_from_ctx_annotation(ctx_annotation: Any) -> tuple[list[ty return [cast(type[Any], Any)], [] if t_origin in (Union, UnionType): - message_types = [arg for arg in get_args(t) if arg is not Any and arg is not Never] - return message_types, [] + msg_types: list[type[Any] | UnionType] = [arg for arg in get_args(t) if arg is not Any and arg is not Never] + return msg_types, [] if t is Never: return [], [] @@ -92,7 +94,7 @@ def infer_output_types_from_ctx_annotation(ctx_annotation: Any) -> tuple[list[ty t_out, t_w_out = args[:2] # Take first two args in case there are more # Process T_Out for message_types - message_types = [] + message_types: list[type[Any] | UnionType] = [] t_out_origin = get_origin(t_out) if t_out is Any: message_types = [cast(type[Any], Any)] @@ -103,7 +105,7 @@ def infer_output_types_from_ctx_annotation(ctx_annotation: Any) -> tuple[list[ty message_types = [t_out] # Process T_W_Out for workflow_output_types - workflow_output_types = [] + workflow_output_types: list[type[Any] | UnionType] = [] t_w_out_origin = get_origin(t_w_out) if t_w_out is Any: workflow_output_types = [cast(type[Any], Any)] @@ -129,7 +131,7 @@ def validate_workflow_context_annotation( annotation: Any, parameter_name: str, context_description: str, -) -> tuple[list[type[Any]], list[type[Any]]]: +) -> tuple[list[type[Any] | UnionType], list[type[Any] | UnionType]]: """Validate a WorkflowContext annotation and return inferred types. Args: diff --git a/python/packages/core/agent_framework/_workflows/_workflow_executor.py b/python/packages/core/agent_framework/_workflows/_workflow_executor.py index 2b2426b3eb..2453620cfd 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_executor.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_executor.py @@ -3,6 +3,7 @@ import asyncio import logging import sys +import types import uuid from dataclasses import dataclass from typing import TYPE_CHECKING, Any @@ -302,13 +303,13 @@ class WorkflowExecutor(Executor): self._propagate_request = propagate_request @property - def input_types(self) -> list[type[Any]]: + def input_types(self) -> list[type[Any] | types.UnionType]: """Get the input types based on the underlying workflow's input types plus WorkflowExecutor-specific types. Returns: A list of input types that the WorkflowExecutor can accept. """ - input_types = list(self.workflow.input_types) + input_types: list[type[Any] | types.UnionType] = list(self.workflow.input_types) # WorkflowExecutor can also handle SubWorkflowResponseMessage for sub-workflow responses if SubWorkflowResponseMessage not in input_types: @@ -317,7 +318,7 @@ class WorkflowExecutor(Executor): return input_types @property - def output_types(self) -> list[type[Any]]: + def output_types(self) -> list[type[Any] | types.UnionType]: """Get the output types based on the underlying workflow's output types. Returns: @@ -325,7 +326,7 @@ class WorkflowExecutor(Executor): Includes the SubWorkflowRequestMessage type if any executor in the sub-workflow is request-response capable. """ - output_types = list(self.workflow.output_types) + output_types: list[type[Any] | types.UnionType] = list(self.workflow.output_types) is_request_response_capable = any( executor.is_request_response_capable for executor in self.workflow.executors.values() diff --git a/python/packages/core/tests/workflow/test_executor.py b/python/packages/core/tests/workflow/test_executor.py index 1988722a5d..e7c2a31aec 100644 --- a/python/packages/core/tests/workflow/test_executor.py +++ b/python/packages/core/tests/workflow/test_executor.py @@ -1,5 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. +from dataclasses import dataclass + import pytest from typing_extensions import Never @@ -17,6 +19,27 @@ from agent_framework import ( ) +# Module-level types for string forward reference tests +@dataclass +class ForwardRefMessage: + content: str + + +@dataclass +class ForwardRefTypeA: + value: str + + +@dataclass +class ForwardRefTypeB: + value: int + + +@dataclass +class ForwardRefResponse: + result: str + + def test_executor_without_id(): """Test that an executor without an ID raises an error when trying to run.""" @@ -537,3 +560,362 @@ async def test_executor_invoked_event_data_not_mutated_by_handler(): f"{[m.text for m in mutator_invoked.data]}" ) assert mutator_invoked.data[0].text == "hello" + + +# region: Tests for @handler decorator with explicit input_type and output_type + + +class TestHandlerExplicitTypes: + """Test suite for @handler decorator with explicit input_type and output_type parameters.""" + + def test_handler_with_explicit_input_type(self): + """Test that explicit input_type takes precedence over introspection.""" + from typing import Any + + class ExplicitInputExecutor(Executor): + @handler(input=str) + async def handle(self, message: Any, ctx: WorkflowContext) -> None: + pass + + exec_instance = ExplicitInputExecutor(id="explicit_input") + + # Handler should be registered for str (explicit), not Any (introspected) + assert str in exec_instance._handlers + assert len(exec_instance._handlers) == 1 + + # Can handle str messages + assert exec_instance.can_handle(Message(data="hello", source_id="mock")) + # Cannot handle int messages (since explicit type is str) + assert not exec_instance.can_handle(Message(data=42, source_id="mock")) + + def test_handler_with_explicit_output_type(self): + """Test that explicit output works when input is also specified.""" + + class ExplicitOutputExecutor(Executor): + @handler(input=str, output=int) + async def handle(self, message: str, ctx: WorkflowContext[str]) -> None: + pass + + exec_instance = ExplicitOutputExecutor(id="explicit_output") + + # Handler spec should have int as output type (explicit) + handler_func = exec_instance._handlers[str] + assert handler_func._handler_spec["output_types"] == [int] + + # Executor output_types property should reflect explicit type + assert int in exec_instance.output_types + assert str not in exec_instance.output_types + + def test_handler_with_explicit_input_and_output_types(self): + """Test that both explicit input_type and output_type work together.""" + from typing import Any + + class ExplicitBothExecutor(Executor): + @handler(input=dict, output=list) + async def handle(self, message: Any, ctx: WorkflowContext) -> None: + pass + + exec_instance = ExplicitBothExecutor(id="explicit_both") + + # Handler should be registered for dict (explicit input type) + assert dict in exec_instance._handlers + assert len(exec_instance._handlers) == 1 + + # Output type should be list (explicit) + handler_func = exec_instance._handlers[dict] + assert handler_func._handler_spec["output_types"] == [list] + + # Verify can_handle + assert exec_instance.can_handle(Message(data={"key": "value"}, source_id="mock")) + assert not exec_instance.can_handle(Message(data="string", source_id="mock")) + + def test_handler_with_explicit_union_input_type(self): + """Test that explicit union input_type is handled correctly.""" + from typing import Any + + class UnionInputExecutor(Executor): + @handler(input=str | int) + async def handle(self, message: Any, ctx: WorkflowContext) -> None: + pass + + exec_instance = UnionInputExecutor(id="union_input") + + # Handler should be registered for the union type + # The union type itself is stored as the key + assert len(exec_instance._handlers) == 1 + + # Can handle both str and int messages + assert exec_instance.can_handle(Message(data="hello", source_id="mock")) + assert exec_instance.can_handle(Message(data=42, source_id="mock")) + # Cannot handle float + assert not exec_instance.can_handle(Message(data=3.14, source_id="mock")) + + def test_handler_with_explicit_union_output_type(self): + """Test that explicit union output is normalized to a list.""" + from typing import Any + + class UnionOutputExecutor(Executor): + @handler(input=bytes, output=str | int | bool) + async def handle(self, message: Any, ctx: WorkflowContext) -> None: + pass + + exec_instance = UnionOutputExecutor(id="union_output") + + # Output types should be a list with all union members + assert set(exec_instance.output_types) == {str, int, bool} + + def test_handler_explicit_types_precedence_over_introspection(self): + """Test that explicit types always take precedence over introspected types.""" + + class PrecedenceExecutor(Executor): + # Introspection would give: input=str, output=[int] + # Explicit gives: input=bytes, output=[float] + @handler(input=bytes, output=float) + async def handle(self, message: str, ctx: WorkflowContext[int]) -> None: + pass + + exec_instance = PrecedenceExecutor(id="precedence") + + # Should use explicit input type (bytes), not introspected (str) + assert bytes in exec_instance._handlers + assert str not in exec_instance._handlers + + # Should use explicit output type (float), not introspected (int) + assert float in exec_instance.output_types + assert int not in exec_instance.output_types + + def test_handler_fallback_to_introspection_when_no_explicit_types(self): + """Test that introspection is used when no explicit types are provided.""" + + class IntrospectedExecutor(Executor): + @handler + async def handle(self, message: str, ctx: WorkflowContext[int]) -> None: + pass + + exec_instance = IntrospectedExecutor(id="introspected") + + # Should use introspected types + assert str in exec_instance._handlers + assert int in exec_instance.output_types + + def test_handler_explicit_mode_requires_input(self): + """Test that using any explicit type param requires input to be specified.""" + + # Only explicit input - output defaults to empty (no introspection) + class OnlyInputExecutor(Executor): + @handler(input=bytes) + async def handle(self, message: str, ctx: WorkflowContext[int]) -> None: + pass + + exec_input = OnlyInputExecutor(id="only_input") + assert bytes in exec_input._handlers # Explicit + assert exec_input.output_types == [] # No output types (not introspected) + + # Only explicit output without input should raise error + with pytest.raises(ValueError, match="must specify 'input' type"): + + class OnlyOutputExecutor(Executor): + @handler(output=float) + async def handle(self, message: str, ctx: WorkflowContext[int]) -> None: + pass + + # Only explicit workflow_output without input should raise error + with pytest.raises(ValueError, match="must specify 'input' type"): + + class OnlyWorkflowOutputExecutor(Executor): + @handler(workflow_output=bool) + async def handle(self, message: str, ctx: WorkflowContext[int, str]) -> None: + pass + + def test_handler_explicit_input_type_allows_no_message_annotation(self): + """Test that explicit input_type allows handler without message type annotation.""" + + class NoAnnotationExecutor(Executor): + @handler(input=str) + async def handle(self, message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + exec_instance = NoAnnotationExecutor(id="no_annotation") + + # Should work with explicit input_type + assert str in exec_instance._handlers + assert exec_instance.can_handle(Message(data="hello", source_id="mock")) + + def test_handler_multiple_handlers_mixed_explicit_and_introspected(self): + """Test executor with multiple handlers, some with explicit types and some introspected.""" + + class MixedExecutor(Executor): + @handler(input=str, output=int) + async def handle_explicit(self, message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + @handler + async def handle_introspected(self, message: float, ctx: WorkflowContext[bool]) -> None: + pass + + exec_instance = MixedExecutor(id="mixed") + + # Should have both handlers + assert len(exec_instance._handlers) == 2 + assert str in exec_instance._handlers # Explicit + assert float in exec_instance._handlers # Introspected + + # Should have both output types + assert int in exec_instance.output_types # Explicit + assert bool in exec_instance.output_types # Introspected + + def test_handler_with_string_forward_reference_input_type(self): + """Test that string forward references work for input_type.""" + + class StringRefExecutor(Executor): + @handler(input="ForwardRefMessage") + async def handle(self, message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + exec_instance = StringRefExecutor(id="string_ref") + + # Should resolve the string to the actual type + assert ForwardRefMessage in exec_instance._handlers + assert exec_instance.can_handle(Message(data=ForwardRefMessage("hello"), source_id="mock")) + + def test_handler_with_string_forward_reference_union(self): + """Test that string forward references work with union types.""" + + class StringUnionExecutor(Executor): + @handler(input="ForwardRefTypeA | ForwardRefTypeB") + async def handle(self, message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + exec_instance = StringUnionExecutor(id="string_union") + + # Should handle both types + assert exec_instance.can_handle(Message(data=ForwardRefTypeA("hello"), source_id="mock")) + assert exec_instance.can_handle(Message(data=ForwardRefTypeB(42), source_id="mock")) + + def test_handler_with_string_forward_reference_output_type(self): + """Test that string forward references work for output_type.""" + + class StringOutputExecutor(Executor): + @handler(input=str, output="ForwardRefResponse") + async def handle(self, message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + exec_instance = StringOutputExecutor(id="string_output") + + # Should resolve the string output type + assert ForwardRefResponse in exec_instance.output_types + + def test_handler_with_explicit_workflow_output_type(self): + """Test that explicit workflow_output works when input is also specified.""" + + class ExplicitWorkflowOutputExecutor(Executor): + @handler(input=str, workflow_output=bool) + async def handle(self, message: str, ctx: WorkflowContext[int]) -> None: + pass + + exec_instance = ExplicitWorkflowOutputExecutor(id="explicit_workflow_output") + + # Handler spec should have bool as workflow_output_type (explicit) + handler_func = exec_instance._handlers[str] + assert handler_func._handler_spec["workflow_output_types"] == [bool] + + # Executor workflow_output_types property should reflect explicit type + assert bool in exec_instance.workflow_output_types + # output_types should be empty (explicit mode, output not specified) + assert exec_instance.output_types == [] + + def test_handler_with_explicit_workflow_output_and_output(self): + """Test that explicit workflow_output works alongside explicit output.""" + + class PrecedenceExecutor(Executor): + @handler(input=int, output=float, workflow_output=str) + async def handle(self, message: int, ctx: WorkflowContext[int, bool]) -> None: + pass + + exec_instance = PrecedenceExecutor(id="precedence") + + # All types should come from explicit params + assert int in exec_instance._handlers + assert float in exec_instance.output_types + assert str in exec_instance.workflow_output_types + # Introspected types should NOT be present + assert bool not in exec_instance.workflow_output_types + + def test_handler_with_all_explicit_types(self): + """Test that all three explicit type parameters work together.""" + from typing import Any + + class AllExplicitExecutor(Executor): + @handler(input=str, output=int, workflow_output=bool) + async def handle(self, message: Any, ctx: WorkflowContext) -> None: + pass + + exec_instance = AllExplicitExecutor(id="all_explicit") + + # Check input type + assert str in exec_instance._handlers + assert exec_instance.can_handle(Message(data="hello", source_id="mock")) + + # Check output_type + assert int in exec_instance.output_types + + # Check workflow_output_type + assert bool in exec_instance.workflow_output_types + + def test_handler_with_union_workflow_output_type(self): + """Test that union types work for workflow_output.""" + + class UnionWorkflowOutputExecutor(Executor): + @handler(input=str, workflow_output=str | int) + async def handle(self, message: str, ctx: WorkflowContext) -> None: + pass + + exec_instance = UnionWorkflowOutputExecutor(id="union_workflow_output") + + # Should include both types from union + assert str in exec_instance.workflow_output_types + assert int in exec_instance.workflow_output_types + + def test_handler_with_string_forward_reference_workflow_output_type(self): + """Test that string forward references work for workflow_output_type.""" + + class StringWorkflowOutputExecutor(Executor): + @handler(input=str, workflow_output="ForwardRefResponse") + async def handle(self, message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + exec_instance = StringWorkflowOutputExecutor(id="string_workflow_output") + + # Should resolve the string workflow_output_type + assert ForwardRefResponse in exec_instance.workflow_output_types + + def test_handler_with_string_forward_reference_union_workflow_output_type(self): + """Test that string forward reference union types work for workflow_output_type.""" + + class StringUnionWorkflowOutputExecutor(Executor): + @handler(input=str, workflow_output="ForwardRefTypeA | ForwardRefTypeB") + async def handle(self, message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + exec_instance = StringUnionWorkflowOutputExecutor(id="string_union_workflow_output") + + # Should resolve both types from string union + assert ForwardRefTypeA in exec_instance.workflow_output_types + assert ForwardRefTypeB in exec_instance.workflow_output_types + + def test_handler_fallback_to_introspection_for_workflow_output_type(self): + """Test that workflow_output_type falls back to introspection when not explicitly provided.""" + + class IntrospectedWorkflowOutputExecutor(Executor): + @handler + async def handle(self, message: str, ctx: WorkflowContext[int, bool]) -> None: + pass + + exec_instance = IntrospectedWorkflowOutputExecutor(id="introspected_workflow_output") + + # Should use introspected types from WorkflowContext[int, bool] + assert int in exec_instance.output_types + assert bool in exec_instance.workflow_output_types + + +# endregion: Tests for @handler decorator with explicit input_type and output_type diff --git a/python/packages/core/tests/workflow/test_function_executor.py b/python/packages/core/tests/workflow/test_function_executor.py index a034f42a38..a06f1445e1 100644 --- a/python/packages/core/tests/workflow/test_function_executor.py +++ b/python/packages/core/tests/workflow/test_function_executor.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. +from dataclasses import dataclass from typing import Any import pytest @@ -14,6 +15,27 @@ from agent_framework import ( ) +# Module-level types for string forward reference tests +@dataclass +class FuncExecForwardRefMessage: + content: str + + +@dataclass +class FuncExecForwardRefTypeA: + value: str + + +@dataclass +class FuncExecForwardRefTypeB: + value: int + + +@dataclass +class FuncExecForwardRefResponse: + result: str + + class TestFunctionExecutor: """Test suite for FunctionExecutor and @executor decorator.""" @@ -535,3 +557,341 @@ class TestFunctionExecutor: async_static = static_wrapped assert asyncio.iscoroutinefunction(C.async_static) # Works via descriptor protocol + + +class TestExecutorExplicitTypes: + """Test suite for @executor decorator with explicit input_type and output_type parameters.""" + + def test_executor_with_explicit_input_type(self): + """Test that explicit input_type takes precedence over introspection.""" + + @executor(input=str) + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + # Handler should be registered for str (explicit) + assert str in process._handlers + assert len(process._handlers) == 1 + + # Can handle str messages + assert process.can_handle(Message(data="hello", source_id="mock")) + # Cannot handle int messages + assert not process.can_handle(Message(data=42, source_id="mock")) + + def test_executor_with_explicit_output_type(self): + """Test that explicit output_type takes precedence over introspection.""" + + @executor(output=int) + async def process(message: str, ctx: WorkflowContext[str]) -> None: + pass + + # Handler spec should have int as output type (explicit), not str (introspected) + spec = process._handler_specs[0] + assert spec["output_types"] == [int] + + # Executor output_types property should reflect explicit type + assert int in process.output_types + assert str not in process.output_types + + def test_executor_with_explicit_input_and_output_types(self): + """Test that both explicit input_type and output_type work together.""" + + @executor(id="explicit_both", input=dict, output=list) + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + # Handler should be registered for dict (explicit input type) + assert dict in process._handlers + assert len(process._handlers) == 1 + + # Output type should be list (explicit) + spec = process._handler_specs[0] + assert spec["output_types"] == [list] + + # Verify can_handle + assert process.can_handle(Message(data={"key": "value"}, source_id="mock")) + assert not process.can_handle(Message(data="string", source_id="mock")) + + def test_executor_with_explicit_union_input_type(self): + """Test that explicit union input_type is handled correctly.""" + + @executor(input=str | int) + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + # Handler should be registered for the union type + assert len(process._handlers) == 1 + + # Can handle both str and int messages + assert process.can_handle(Message(data="hello", source_id="mock")) + assert process.can_handle(Message(data=42, source_id="mock")) + # Cannot handle float + assert not process.can_handle(Message(data=3.14, source_id="mock")) + + def test_executor_with_explicit_union_output_type(self): + """Test that explicit union output_type is normalized to a list.""" + + @executor(output=str | int | bool) + async def process(message: Any, ctx: WorkflowContext) -> None: + pass + + # Output types should be a list with all union members + assert set(process.output_types) == {str, int, bool} + + def test_executor_explicit_types_precedence_over_introspection(self): + """Test that explicit types always take precedence over introspected types.""" + + # Introspection would give: input=str, output=[int] + # Explicit gives: input=bytes, output=[float] + @executor(input=bytes, output=float) + async def process(message: str, ctx: WorkflowContext[int]) -> None: + pass + + # Should use explicit input type (bytes), not introspected (str) + assert bytes in process._handlers + assert str not in process._handlers + + # Should use explicit output type (float), not introspected (int) + assert float in process.output_types + assert int not in process.output_types + + def test_executor_fallback_to_introspection_when_no_explicit_types(self): + """Test that introspection is used when no explicit types are provided.""" + + @executor + async def process(message: str, ctx: WorkflowContext[int]) -> None: + pass + + # Should use introspected types + assert str in process._handlers + assert int in process.output_types + + def test_executor_partial_explicit_types(self): + """Test that partial explicit types work (only input_type or only output_type).""" + + # Only explicit input_type, introspect output_type + @executor(input=bytes) + async def process_input(message: str, ctx: WorkflowContext[int]) -> None: + pass + + assert bytes in process_input._handlers # Explicit + assert int in process_input.output_types # Introspected + + # Only explicit output_type, introspect input_type + @executor(output=float) + async def process_output(message: str, ctx: WorkflowContext[int]) -> None: + pass + + assert str in process_output._handlers # Introspected + assert float in process_output.output_types # Explicit + assert int not in process_output.output_types # Not introspected when explicit provided + + def test_executor_explicit_input_type_allows_no_message_annotation(self): + """Test that explicit input_type allows function without message type annotation.""" + + @executor(input=str) + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + # Should work with explicit input_type + assert str in process._handlers + assert process.can_handle(Message(data="hello", source_id="mock")) + + def test_executor_explicit_types_with_id(self): + """Test that explicit types work together with id parameter.""" + + @executor(id="custom_id", input=bytes, output=int) + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + assert process.id == "custom_id" + assert bytes in process._handlers + assert int in process.output_types + + def test_executor_explicit_types_with_single_param_function(self): + """Test that explicit input_type works with single-parameter functions.""" + + @executor(input=str) + async def process(message): # type: ignore[no-untyped-def] + return message.upper() + + # Should work with explicit input_type + assert str in process._handlers + assert process.can_handle(Message(data="hello", source_id="mock")) + assert not process.can_handle(Message(data=42, source_id="mock")) + + def test_executor_explicit_types_with_sync_function(self): + """Test that explicit types work with synchronous functions.""" + + @executor(input=int, output=str) + def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + assert int in process._handlers + assert str in process.output_types + + def test_function_executor_constructor_with_explicit_types(self): + """Test FunctionExecutor constructor with explicit input_type and output_type.""" + + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + func_exec = FunctionExecutor(process, id="test", input=dict, output=list) + + assert dict in func_exec._handlers + spec = func_exec._handler_specs[0] + assert spec["message_type"] is dict + assert spec["output_types"] == [list] + + def test_executor_explicit_union_types_via_typing_union(self): + """Test that Union[] syntax also works for explicit types.""" + from typing import Union + + @executor(input=Union[str, int], output=Union[bool, float]) + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + # Can handle both str and int + assert process.can_handle(Message(data="hello", source_id="mock")) + assert process.can_handle(Message(data=42, source_id="mock")) + + # Output types should include both + assert set(process.output_types) == {bool, float} + + def test_executor_with_string_forward_reference_input_type(self): + """Test that string forward references work for input_type.""" + + @executor(input="FuncExecForwardRefMessage") + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + # Should resolve the string to the actual type + assert FuncExecForwardRefMessage in process._handlers + assert process.can_handle(Message(data=FuncExecForwardRefMessage("hello"), source_id="mock")) + + def test_executor_with_string_forward_reference_union(self): + """Test that string forward references work with union types.""" + + @executor(input="FuncExecForwardRefTypeA | FuncExecForwardRefTypeB") + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + # Should handle both types + assert process.can_handle(Message(data=FuncExecForwardRefTypeA("hello"), source_id="mock")) + assert process.can_handle(Message(data=FuncExecForwardRefTypeB(42), source_id="mock")) + + def test_executor_with_string_forward_reference_output_type(self): + """Test that string forward references work for output_type.""" + + @executor(input=str, output="FuncExecForwardRefResponse") + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + # Should resolve the string output type + assert FuncExecForwardRefResponse in process.output_types + + def test_executor_with_explicit_workflow_output_type(self): + """Test that explicit workflow_output_type takes precedence over introspection.""" + + @executor(workflow_output=bool) + async def process(message: str, ctx: WorkflowContext[int]) -> None: + pass + + # Handler spec should have bool as workflow_output_type (explicit) + spec = process._handler_specs[0] + assert spec["workflow_output_types"] == [bool] + + # Executor workflow_output_types property should reflect explicit type + assert bool in process.workflow_output_types + # output_types should still come from introspection (int from WorkflowContext[int]) + assert int in process.output_types + + def test_executor_with_explicit_workflow_output_type_precedence(self): + """Test that explicit workflow_output_type overrides introspected WorkflowContext second param.""" + + @executor(workflow_output=str) + async def process(message: int, ctx: WorkflowContext[int, bool]) -> None: + pass + + # workflow_output_types should be str (explicit), not bool (introspected from ctx) + assert str in process.workflow_output_types + assert bool not in process.workflow_output_types + + def test_executor_with_all_explicit_types(self): + """Test that all three explicit type parameters work together.""" + from typing import Any + + @executor(input=str, output=int, workflow_output=bool) + async def process(message: Any, ctx: WorkflowContext) -> None: + pass + + # Check input type + assert str in process._handlers + assert process.can_handle(Message(data="hello", source_id="mock")) + + # Check output_type + assert int in process.output_types + + # Check workflow_output_type + assert bool in process.workflow_output_types + + def test_executor_with_union_workflow_output_type(self): + """Test that union types work for workflow_output_type.""" + + @executor(workflow_output=str | int) + async def process(message: str, ctx: WorkflowContext) -> None: + pass + + # Should include both types from union + assert str in process.workflow_output_types + assert int in process.workflow_output_types + + def test_executor_with_string_forward_reference_workflow_output_type(self): + """Test that string forward references work for workflow_output_type.""" + + @executor(input=str, workflow_output="FuncExecForwardRefResponse") + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + # Should resolve the string workflow_output_type + assert FuncExecForwardRefResponse in process.workflow_output_types + + def test_executor_with_string_forward_reference_union_workflow_output_type(self): + """Test that string forward reference union types work for workflow_output_type.""" + + @executor(input=str, workflow_output="FuncExecForwardRefTypeA | FuncExecForwardRefTypeB") + async def process(message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] + pass + + # Should resolve both types from string union + assert FuncExecForwardRefTypeA in process.workflow_output_types + assert FuncExecForwardRefTypeB in process.workflow_output_types + + def test_executor_fallback_to_introspection_for_workflow_output_type(self): + """Test that workflow_output_type falls back to introspection when not explicitly provided.""" + + @executor + async def process(message: str, ctx: WorkflowContext[int, bool]) -> None: + pass + + # Should use introspected types from WorkflowContext[int, bool] + assert int in process.output_types + assert bool in process.workflow_output_types + + def test_function_executor_constructor_with_workflow_output_type(self): + """Test FunctionExecutor constructor accepts workflow_output_type parameter.""" + + async def my_func(message: str, ctx: WorkflowContext) -> None: + pass + + exec_instance = FunctionExecutor( + my_func, + id="test_constructor", + input=str, + output=int, + workflow_output=bool, + ) + + assert str in exec_instance._handlers + assert int in exec_instance.output_types + assert bool in exec_instance.workflow_output_types diff --git a/python/packages/core/tests/workflow/test_request_info_mixin.py b/python/packages/core/tests/workflow/test_request_info_mixin.py index d5528f721d..23b7663a0c 100644 --- a/python/packages/core/tests/workflow/test_request_info_mixin.py +++ b/python/packages/core/tests/workflow/test_request_info_mixin.py @@ -247,7 +247,6 @@ class TestRequestInfoMixin: assert "output_types" in spec assert "workflow_output_types" in spec assert "ctx_annotation" in spec - assert spec["source"] == "class_method" def test_multiple_discovery_calls_raise_error(self): """Test that multiple calls to _discover_response_handlers raise an error for duplicates.""" @@ -786,3 +785,170 @@ class TestRequestInfoMixin: # Should not support unregistered combinations assert child.is_request_supported(str, str) is False assert child.is_request_supported(int, str) is False + + +class TestResponseHandlerExplicitTypes: + """Test cases for response_handler with explicit type parameters.""" + + def test_response_handler_with_explicit_types(self): + """Test response_handler with explicit request and response types.""" + + @response_handler(request=str, response=int) + async def test_handler(self, original_request, response, ctx) -> None: + pass + + spec = test_handler._response_handler_spec # type: ignore[reportAttributeAccessIssue] + assert spec["name"] == "test_handler" + assert spec["request_type"] is str + assert spec["response_type"] is int + + def test_response_handler_with_explicit_output_types(self): + """Test response_handler with explicit output and workflow_output types.""" + + @response_handler(request=str, response=int, output=bool, workflow_output=float) + async def test_handler(self, original_request, response, ctx) -> None: + pass + + spec = test_handler._response_handler_spec # type: ignore[reportAttributeAccessIssue] + assert spec["request_type"] is str + assert spec["response_type"] is int + assert bool in spec["output_types"] + assert float in spec["workflow_output_types"] + + def test_response_handler_with_union_types(self): + """Test response_handler with union types.""" + + @response_handler(request=str | int, response=bool | float) + async def test_handler(self, original_request, response, ctx) -> None: + pass + + spec = test_handler._response_handler_spec # type: ignore[reportAttributeAccessIssue] + assert spec["request_type"] == str | int + assert spec["response_type"] == bool | float + + def test_response_handler_with_string_forward_references(self): + """Test response_handler with string forward references.""" + + @response_handler(request="str", response="int") + async def test_handler(self, original_request, response, ctx) -> None: + pass + + spec = test_handler._response_handler_spec # type: ignore[reportAttributeAccessIssue] + assert spec["request_type"] is str + assert spec["response_type"] is int + + def test_response_handler_explicit_missing_request_raises_error(self): + """Test that using explicit types without request raises an error.""" + with pytest.raises(ValueError, match="must specify 'request' type"): + + @response_handler(response=int) + async def test_handler(self, original_request, response, ctx) -> None: + pass + + def test_response_handler_explicit_missing_response_raises_error(self): + """Test that using explicit types without response raises an error.""" + with pytest.raises(ValueError, match="must specify 'response' type"): + + @response_handler(request=str) + async def test_handler(self, original_request, response, ctx) -> None: + pass + + def test_response_handler_explicit_only_output_raises_error(self): + """Test that using only output without request/response raises an error.""" + with pytest.raises(ValueError, match="must specify 'request' type"): + + @response_handler(output=bool) + async def test_handler(self, original_request, response, ctx) -> None: + pass + + def test_executor_with_explicit_response_handlers(self): + """Test an executor with explicit type response handlers.""" + + class TestExecutor(Executor): + def __init__(self): + super().__init__(id="test_executor") + + @handler + async def dummy_handler(self, message: str, ctx: WorkflowContext) -> None: + pass + + @response_handler(request=str, response=int, output=bool) + async def handle_explicit(self, original_request, response, ctx) -> None: + pass + + executor = TestExecutor() + + # Should be request-response capable + assert executor.is_request_response_capable is True + + # Should have registered handler + response_handlers = executor._response_handlers # type: ignore[reportAttributeAccessIssue] + assert len(response_handlers) == 1 + assert (str, int) in response_handlers + + # Check specs + specs = executor._response_handler_specs # type: ignore[reportAttributeAccessIssue] + assert len(specs) == 1 + assert specs[0]["request_type"] is str + assert specs[0]["response_type"] is int + assert bool in specs[0]["output_types"] + + def test_response_handler_explicit_callable(self): + """Test that explicit type response handlers can be called.""" + + class TestExecutor(Executor): + def __init__(self): + super().__init__(id="test_executor") + self.handled_request = None + self.handled_response = None + + @handler + async def dummy_handler(self, message: str, ctx: WorkflowContext) -> None: + pass + + @response_handler(request=str, response=int) + async def handle_response(self, original_request, response, ctx) -> None: + self.handled_request = original_request + self.handled_response = response + + executor = TestExecutor() + + # Get the handler + response_handler_func = executor._response_handlers[(str, int)] # type: ignore[reportAttributeAccessIssue] + + # Call the handler + asyncio.run(response_handler_func("test_request", 42, None)) # type: ignore[reportArgumentType] + + assert executor.handled_request == "test_request" + assert executor.handled_response == 42 + + def test_mixed_introspection_and_explicit_handlers(self): + """Test executor with both introspection and explicit type handlers.""" + + class TestExecutor(Executor): + def __init__(self): + super().__init__(id="test_executor") + + @handler + async def dummy_handler(self, message: str, ctx: WorkflowContext) -> None: + pass + + # Introspection-based handler + @response_handler + async def handle_introspection( + self, original_request: str, response: int, ctx: WorkflowContext[str] + ) -> None: + pass + + # Explicit type handler + @response_handler(request=dict, response=bool) + async def handle_explicit(self, original_request, response, ctx) -> None: + pass + + executor = TestExecutor() + + # Should have both handlers + response_handlers = executor._response_handlers # type: ignore[reportAttributeAccessIssue] + assert len(response_handlers) == 2 + assert (str, int) in response_handlers + assert (dict, bool) in response_handlers diff --git a/python/packages/core/tests/workflow/test_typing_utils.py b/python/packages/core/tests/workflow/test_typing_utils.py index 4294f35f4b..3e8d1051e7 100644 --- a/python/packages/core/tests/workflow/test_typing_utils.py +++ b/python/packages/core/tests/workflow/test_typing_utils.py @@ -1,16 +1,153 @@ # Copyright (c) Microsoft. All rights reserved. from dataclasses import dataclass -from typing import Any, Generic, TypeVar, Union +from typing import Any, Generic, Optional, TypeVar, Union + +import pytest from agent_framework import RequestInfoEvent from agent_framework._workflows._typing_utils import ( deserialize_type, is_instance_of, is_type_compatible, + normalize_type_to_list, + resolve_type_annotation, serialize_type, ) +# region: normalize_type_to_list tests + + +def test_normalize_type_to_list_single_type() -> None: + """Test normalize_type_to_list with single types.""" + assert normalize_type_to_list(str) == [str] + assert normalize_type_to_list(int) == [int] + assert normalize_type_to_list(float) == [float] + assert normalize_type_to_list(bool) == [bool] + assert normalize_type_to_list(list) == [list] + assert normalize_type_to_list(dict) == [dict] + + +def test_normalize_type_to_list_none() -> None: + """Test normalize_type_to_list with None returns empty list.""" + assert normalize_type_to_list(None) == [] + + +def test_normalize_type_to_list_union_pipe_syntax() -> None: + """Test normalize_type_to_list with union types using | syntax.""" + result = normalize_type_to_list(str | int) + assert set(result) == {str, int} + + result = normalize_type_to_list(str | int | bool) + assert set(result) == {str, int, bool} + + +def test_normalize_type_to_list_union_typing_syntax() -> None: + """Test normalize_type_to_list with Union[] from typing module.""" + result = normalize_type_to_list(Union[str, int]) + assert set(result) == {str, int} + + result = normalize_type_to_list(Union[str, int, bool]) + assert set(result) == {str, int, bool} + + +def test_normalize_type_to_list_optional() -> None: + """Test normalize_type_to_list with Optional types (Union[T, None]).""" + # Optional[str] is Union[str, None] + result = normalize_type_to_list(Optional[str]) + assert str in result + assert type(None) in result + assert len(result) == 2 + + # str | None is equivalent + result = normalize_type_to_list(str | None) + assert str in result + assert type(None) in result + assert len(result) == 2 + + +def test_normalize_type_to_list_custom_types() -> None: + """Test normalize_type_to_list with custom class types.""" + + @dataclass + class CustomMessage: + content: str + + result = normalize_type_to_list(CustomMessage) + assert result == [CustomMessage] + + result = normalize_type_to_list(CustomMessage | str) + assert set(result) == {CustomMessage, str} + + +# endregion: normalize_type_to_list tests + + +# region: resolve_type_annotation tests + + +def test_resolve_type_annotation_none() -> None: + """Test resolve_type_annotation with None returns None.""" + assert resolve_type_annotation(None) is None + + +def test_resolve_type_annotation_actual_types() -> None: + """Test resolve_type_annotation passes through actual types unchanged.""" + assert resolve_type_annotation(str) is str + assert resolve_type_annotation(int) is int + assert resolve_type_annotation(str | int) == str | int + + +def test_resolve_type_annotation_string_builtin() -> None: + """Test resolve_type_annotation resolves string references to builtin types.""" + result = resolve_type_annotation("str", {"str": str}) + assert result is str + + result = resolve_type_annotation("int", {"int": int}) + assert result is int + + +def test_resolve_type_annotation_string_union() -> None: + """Test resolve_type_annotation resolves string union types.""" + result = resolve_type_annotation("str | int", {"str": str, "int": int}) + assert result == str | int + + +def test_resolve_type_annotation_string_custom_type() -> None: + """Test resolve_type_annotation resolves string references to custom types.""" + + @dataclass + class MyCustomType: + value: int + + result = resolve_type_annotation("MyCustomType", {"MyCustomType": MyCustomType}) + assert result is MyCustomType + + result = resolve_type_annotation("MyCustomType | str", {"MyCustomType": MyCustomType, "str": str}) + assert set(result.__args__) == {MyCustomType, str} # type: ignore[union-attr] + + +def test_resolve_type_annotation_string_typing_union() -> None: + """Test resolve_type_annotation resolves Union[] syntax in strings.""" + result = resolve_type_annotation("Union[str, int]", {"str": str, "int": int}) + assert set(result.__args__) == {str, int} # type: ignore[union-attr] + + +def test_resolve_type_annotation_string_optional() -> None: + """Test resolve_type_annotation resolves Optional[] syntax in strings.""" + result = resolve_type_annotation("Optional[str]", {"str": str}) + assert str in result.__args__ # type: ignore[union-attr] + assert type(None) in result.__args__ # type: ignore[union-attr] + + +def test_resolve_type_annotation_unresolvable_raises() -> None: + """Test resolve_type_annotation raises NameError for unresolvable types.""" + with pytest.raises(NameError, match="Could not resolve type annotation"): + resolve_type_annotation("NonExistentType", {}) + + +# endregion: resolve_type_annotation tests + def test_basic_types() -> None: """Test basic built-in types.""" diff --git a/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py b/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py index fe031ab548..f5094e9040 100644 --- a/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py +++ b/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py @@ -34,6 +34,18 @@ What this example shows Simple steps can use this form; a terminal step can yield output using ctx.yield_output() to provide workflow results. +- Explicit type parameters with @handler: + Instead of relying on type introspection from function signatures, you can explicitly + specify `input`, `output`, and/or `workflow_output` on the @handler decorator. + This is "all or nothing": when ANY explicit parameter is provided, ALL types come + from explicit parameters (introspection is disabled). The `input` parameter is + required; `output` and `workflow_output` are optional. + + Examples: + @handler(input=str | int) # Accepts str or int, no outputs + @handler(input=str, output=int) # Accepts str, outputs int + @handler(input=str, output=int, workflow_output=bool) # All three specified + - Fluent WorkflowBuilder API: add_edge(A, B) to connect nodes, set_start_executor(A), then build() -> Workflow. @@ -46,8 +58,8 @@ Prerequisites """ -# Example 1: A custom Executor subclass -# ------------------------------------ +# Example 1: A custom Executor subclass using introspection (traditional approach) +# --------------------------------------------------------------------------------- # # Subclassing Executor lets you define a named node with lifecycle hooks if needed. # The work itself is implemented in an async method decorated with @handler. @@ -71,14 +83,15 @@ class UpperCase(Executor): Note: The WorkflowContext is parameterized with the type this handler will emit. Here WorkflowContext[str] means downstream nodes should expect str. """ + result = text.upper() # Send the result to the next executor in the workflow. await ctx.send_message(result) -# Example 2: A standalone function-based executor -# ----------------------------------------------- +# Example 2: A standalone function-based executor using introspection +# -------------------------------------------------------------------- # # For simple steps you can skip subclassing and define an async function with the # same signature pattern (typed input + WorkflowContext[T_Out, T_W_Out]) and decorate it with @@ -102,30 +115,95 @@ async def reverse_text(text: str, ctx: WorkflowContext[Never, str]) -> None: await ctx.yield_output(result) -async def main(): - """Build and run a simple 2-step workflow using the fluent builder API.""" +# Example 3: Using explicit type parameters on @handler +# ----------------------------------------------------- +# +# Instead of relying on type introspection, you can explicitly specify input, +# output, and/or workflow_output on the @handler decorator. This is "all or nothing": +# when ANY explicit parameter is provided, ALL types come from explicit parameters +# (introspection is completely disabled). The input parameter is required. +# +# This is useful when: +# - You want to accept multiple types (union types) without complex type annotations +# - The function signature uses Any or a base type for flexibility +# - You want to decouple the runtime type routing from the static type annotations + +class ExclamationAdder(Executor): + """An executor that adds exclamation marks, demonstrating explicit @handler types. + + This example shows how to use explicit input and output parameters + on the @handler decorator instead of relying on introspection from the function + signature. This approach is especially useful for union types. + """ + + def __init__(self, id: str): + super().__init__(id=id) + + @handler(input=str, output=str) + async def add_exclamation(self, message: str, ctx: WorkflowContext) -> None: + """Add exclamation marks to the input. + + Note: The input=str and output=str are explicitly specified on @handler, + so the framework uses those instead of introspecting the function signature. + The WorkflowContext here has no type parameters because the explicit types + on @handler take precedence. + """ + result = f"{message}!!!" + await ctx.send_message(result) + + +async def main(): + """Build and run workflows using the fluent builder API.""" + + # Workflow 1: Using introspection-based type detection + # ----------------------------------------------------- upper_case = UpperCase(id="upper_case_executor") # Build the workflow using a fluent pattern: # 1) add_edge(from_node, to_node) defines a directed edge upper_case -> reverse_text # 2) set_start_executor(node) declares the entry point # 3) build() finalizes and returns an immutable Workflow object - workflow = WorkflowBuilder().add_edge(upper_case, reverse_text).set_start_executor(upper_case).build() + workflow1 = WorkflowBuilder().add_edge(upper_case, reverse_text).set_start_executor(upper_case).build() # Run the workflow by sending the initial message to the start node. # The run(...) call returns an event collection; its get_outputs() method # retrieves the outputs yielded by any terminal nodes. - events = await workflow.run("hello world") - print(events.get_outputs()) - # Summarize the final run state (e.g., IDLE) - print("Final state:", events.get_final_state()) + print("Workflow 1 (introspection-based types):") + events1 = await workflow1.run("hello world") + print(events1.get_outputs()) + print("Final state:", events1.get_final_state()) + + # Workflow 2: Using explicit type parameters on @handler + # ------------------------------------------------------- + exclamation_adder = ExclamationAdder(id="exclamation_adder") + + # This workflow demonstrates the explicit input/output feature: + # exclamation_adder uses @handler(input=str, output=str) to + # explicitly declare types instead of relying on introspection. + workflow2 = ( + WorkflowBuilder() + .add_edge(upper_case, exclamation_adder) + .add_edge(exclamation_adder, reverse_text) + .set_start_executor(upper_case) + .build() + ) + + print("\nWorkflow 2 (explicit @handler types):") + events2 = await workflow2.run("hello world") + print(events2.get_outputs()) + print("Final state:", events2.get_final_state()) """ Sample Output: + Workflow 1 (introspection-based types): ['DLROW OLLEH'] Final state: WorkflowRunState.IDLE + + Workflow 2 (explicit @handler types): + ['!!!DLROW OLLEH'] + Final state: WorkflowRunState.IDLE """ diff --git a/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py b/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py index 694fd868cf..145504bdce 100644 --- a/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py +++ b/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py @@ -11,8 +11,8 @@ from agent_framework import ( ChatMessage, Content, FileCheckpointStorage, + HandoffAgentUserRequest, HandoffBuilder, - HandoffUserInputRequest, RequestInfoEvent, Workflow, WorkflowOutputEvent, @@ -102,7 +102,7 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> tuple[Workflow name="checkpoint_handoff_demo", participants=[triage, refund, order], ) - .set_coordinator("triage_agent") + .with_start_agent(triage) .with_checkpointing(checkpoint_storage) .with_termination_condition( # Terminate after 5 user messages for this demo @@ -114,25 +114,27 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> tuple[Workflow return workflow, triage, refund, order -def _print_handoff_request(request: HandoffUserInputRequest, request_id: str) -> None: +def _print_handoff_agent_user_request(response: AgentResponse) -> None: + """Display the agent's response messages when requesting user input.""" + if not response.messages: + print("(No agent messages)") + return + + print("\n[Agent is requesting your input...]") + for message in response.messages: + if not message.text: + continue + speaker = message.author_name or message.role.value + print(f" {speaker}: {message.text}") + + +def _print_handoff_request(request: HandoffAgentUserRequest, request_id: str) -> None: """Log pending handoff request details for debugging.""" print(f"\n{'=' * 60}") print("WORKFLOW PAUSED - User input needed") print(f"Request ID: {request_id}") - print(f"Awaiting agent: {request.awaiting_agent_id}") - print(f"Prompt: {request.prompt}") - # Note: After checkpoint restore, conversation may be empty because it's not serialized - # to prevent duplication (the conversation is preserved in the coordinator's state). - # See issue #2667. - if request.conversation: - print("\nConversation so far:") - for msg in request.conversation[-3:]: - author = msg.author_name or msg.role.value - snippet = msg.text[:120] + "..." if len(msg.text) > 120 else msg.text - print(f" {author}: {snippet}") - else: - print("\n(Conversation restored from checkpoint - context preserved in workflow state)") + _print_handoff_agent_user_request(request.agent_response) print(f"{'=' * 60}\n") @@ -157,7 +159,7 @@ def _build_responses_for_requests( """Create response payloads for each pending request.""" responses: dict[str, object] = {} for request in pending_requests: - if isinstance(request.data, HandoffUserInputRequest): + if isinstance(request.data, HandoffAgentUserRequest): if user_response is None: raise ValueError("User response is required for HandoffUserInputRequest") responses[request.request_id] = user_response @@ -199,7 +201,7 @@ async def run_until_user_input_needed( elif isinstance(event, RequestInfoEvent): pending_requests.append(event) - if isinstance(event.data, HandoffUserInputRequest): + if isinstance(event.data, HandoffAgentUserRequest): _print_handoff_request(event.data, event.request_id) elif isinstance(event.data, Content) and event.data.type == "function_approval_request": _print_function_approval_request(event.data, event.request_id) @@ -256,7 +258,7 @@ async def resume_with_responses( async for event in workflow.run_stream(checkpoint_id=latest_checkpoint.checkpoint_id): # type: ignore[attr-defined] if isinstance(event, RequestInfoEvent): restored_requests.append(event) - if isinstance(event.data, HandoffUserInputRequest): + if isinstance(event.data, HandoffAgentUserRequest): _print_handoff_request(event.data, event.request_id) elif isinstance(event.data, Content) and event.data.type == "function_approval_request": _print_function_approval_request(event.data, event.request_id) @@ -289,7 +291,7 @@ async def resume_with_responses( elif isinstance(event, RequestInfoEvent): new_pending_requests.append(event) - if isinstance(event.data, HandoffUserInputRequest): + if isinstance(event.data, HandoffAgentUserRequest): _print_handoff_request(event.data, event.request_id) elif isinstance(event.data, Content) and event.data.type == "function_approval_request": _print_function_approval_request(event.data, event.request_id) @@ -302,7 +304,7 @@ async def main() -> None: Demonstrate the checkpoint-based pause/resume pattern for handoff workflows. This sample shows: - 1. Starting a workflow and getting a HandoffUserInputRequest + 1. Starting a workflow and getting a HandoffAgentUserRequest 2. Pausing (checkpoint is saved automatically) 3. Resuming from checkpoint with a user response or tool approval (two-step pattern) 4. Continuing the conversation until completion @@ -361,8 +363,10 @@ async def main() -> None: print("\n>>> Simulating process restart...\n") workflow_step, _, _, _ = create_workflow(checkpoint_storage=storage) - needs_user_input = any(isinstance(req.data, HandoffUserInputRequest) for req in pending_requests) - needs_tool_approval = any(isinstance(req.data, Content) and req.data.type == "function_approval_request" for req in pending_requests) + needs_user_input = any(isinstance(req.data, HandoffAgentUserRequest) for req in pending_requests) + needs_tool_approval = any( + isinstance(req.data, Content) and req.data.type == "function_approval_request" for req in pending_requests + ) user_response = None if needs_user_input: From 96d3f2a55e543242bbbcb850f054318484edda59 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:48:26 +0000 Subject: [PATCH 16/20] Python: Fix broken Content API imports in Python samples (#3639) * Initial plan * Fix broken import paths for Content API in all Python sample files Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com> --- .../azure_ai_with_code_interpreter_file_download.py | 6 +++--- .../azure_responses_client_image_analysis.py | 6 +++--- .../getting_started/agents/custom/custom_agent.py | 10 +++++----- .../agents/custom/custom_chat_client.py | 6 +++--- .../openai/openai_responses_client_image_analysis.py | 6 +++--- .../openai/openai_responses_client_image_generation.py | 4 ++-- ...enai_responses_client_streaming_image_generation.py | 4 ++-- .../openai_responses_client_with_code_interpreter.py | 6 +++--- .../getting_started/devui/weather_agent_azure/agent.py | 4 ++-- .../middleware/override_result_with_middleware.py | 4 ++-- .../handoff_with_code_interpreter_file.py | 4 ++-- 11 files changed, 30 insertions(+), 30 deletions(-) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_download.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_download.py index ba3f72c1ce..72e290e1b4 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_download.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_download.py @@ -8,9 +8,9 @@ from agent_framework import ( AgentResponseUpdate, ChatAgent, CitationAnnotation, + Content, HostedCodeInterpreterTool, HostedFileContent, - TextContent, tool, ) from agent_framework.azure import AzureAIProjectAgentProvider @@ -138,7 +138,7 @@ async def non_streaming_example() -> None: # AgentResponse has messages property, which contains ChatMessage objects for message in result.messages: for content in message.contents: - if isinstance(content, TextContent) and content.annotations: + if content.type == "text" and content.annotations: for annotation in content.annotations: if isinstance(annotation, CitationAnnotation) and annotation.file_id: annotations_found.append(annotation) @@ -181,7 +181,7 @@ async def streaming_example() -> None: async for update in agent.run_stream(QUERY): if isinstance(update, AgentResponseUpdate): for content in update.contents: - if isinstance(content, TextContent): + if content.type == "text": if content.text: text_chunks.append(content.text) if content.annotations: diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py index ebfb81dada..9bf05e32e0 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatMessage, TextContent, UriContent +from agent_framework import ChatMessage, Content from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential @@ -27,8 +27,8 @@ async def main(): user_message = ChatMessage( role="user", contents=[ - TextContent(text="What do you see in this image?"), - UriContent( + Content.from_text(text="What do you see in this image?"), + Content.from_uri( uri="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", media_type="image/jpeg", ), diff --git a/python/samples/getting_started/agents/custom/custom_agent.py b/python/samples/getting_started/agents/custom/custom_agent.py index 8408f88fd0..4bdc0d79e3 100644 --- a/python/samples/getting_started/agents/custom/custom_agent.py +++ b/python/samples/getting_started/agents/custom/custom_agent.py @@ -10,8 +10,8 @@ from agent_framework import ( AgentThread, BaseAgent, ChatMessage, + Content, Role, - TextContent, tool, ) @@ -78,7 +78,7 @@ class EchoAgent(BaseAgent): if not normalized_messages: response_message = ChatMessage( role=Role.ASSISTANT, - contents=[TextContent(text="Hello! I'm a custom echo agent. Send me a message and I'll echo it back.")], + contents=[Content.from_text(text="Hello! I'm a custom echo agent. Send me a message and I'll echo it back.")], ) else: # For simplicity, echo the last user message @@ -88,7 +88,7 @@ class EchoAgent(BaseAgent): else: echo_text = f"{self.echo_prefix}[Non-text message received]" - response_message = ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=echo_text)]) + response_message = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=echo_text)]) # Notify the thread of new messages if provided if thread is not None: @@ -133,7 +133,7 @@ class EchoAgent(BaseAgent): chunk_text = f" {word}" if i > 0 else word yield AgentResponseUpdate( - contents=[TextContent(text=chunk_text)], + contents=[Content.from_text(text=chunk_text)], role=Role.ASSISTANT, ) @@ -142,7 +142,7 @@ class EchoAgent(BaseAgent): # Notify the thread of the complete response if provided if thread is not None: - complete_response = ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=response_text)]) + complete_response = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)]) await self._notify_thread_of_new_messages(thread, normalized_messages, complete_response) diff --git a/python/samples/getting_started/agents/custom/custom_chat_client.py b/python/samples/getting_started/agents/custom/custom_chat_client.py index 00078d14c3..c7df328b00 100644 --- a/python/samples/getting_started/agents/custom/custom_chat_client.py +++ b/python/samples/getting_started/agents/custom/custom_chat_client.py @@ -11,8 +11,8 @@ from agent_framework import ( ChatMessage, ChatResponse, ChatResponseUpdate, + Content, Role, - TextContent, use_chat_middleware, use_function_invocation, tool, @@ -77,7 +77,7 @@ class EchoingChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]): else: response_text = f"{self.prefix} [No text message found]" - response_message = ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=response_text)]) + response_message = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)]) return ChatResponse( messages=[response_message], @@ -103,7 +103,7 @@ class EchoingChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]): # Stream character by character for char in response_text: yield ChatResponseUpdate( - contents=[TextContent(text=char)], + contents=[Content.from_text(text=char)], role=Role.ASSISTANT, response_id=f"echo-stream-resp-{random.randint(1000, 9999)}", model_id="echo-model-v1", diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_image_analysis.py b/python/samples/getting_started/agents/openai/openai_responses_client_image_analysis.py index 83908b162a..c9c56d5e48 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_image_analysis.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_image_analysis.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatMessage, TextContent, UriContent +from agent_framework import ChatMessage, Content from agent_framework.openai import OpenAIResponsesClient """ @@ -26,8 +26,8 @@ async def main(): user_message = ChatMessage( role="user", contents=[ - TextContent(text="What do you see in this image?"), - UriContent( + Content.from_text(text="What do you see in this image?"), + Content.from_uri( uri="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", media_type="image/jpeg", ), diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_image_generation.py b/python/samples/getting_started/agents/openai/openai_responses_client_image_generation.py index 39eda7fd18..9d9fcbf546 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_image_generation.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_image_generation.py @@ -3,7 +3,7 @@ import asyncio import base64 -from agent_framework import DataContent, HostedImageGenerationTool, ImageGenerationToolResultContent, UriContent +from agent_framework import Content, HostedImageGenerationTool, ImageGenerationToolResultContent from agent_framework.openai import OpenAIResponsesClient """ @@ -72,7 +72,7 @@ async def main() -> None: for content in message.contents: if isinstance(content, ImageGenerationToolResultContent) and content.outputs: for output in content.outputs: - if isinstance(output, (DataContent, UriContent)) and output.uri: + if output.type in ("data", "uri") and output.uri: show_image_info(output.uri) break diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_streaming_image_generation.py b/python/samples/getting_started/agents/openai/openai_responses_client_streaming_image_generation.py index 1f3ceae7ec..c5373b69f7 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_streaming_image_generation.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_streaming_image_generation.py @@ -4,7 +4,7 @@ import asyncio import base64 import anyio -from agent_framework import DataContent, HostedImageGenerationTool +from agent_framework import Content, HostedImageGenerationTool from agent_framework.openai import OpenAIResponsesClient """OpenAI Responses Client Streaming Image Generation Example @@ -72,7 +72,7 @@ async def main(): # Handle partial images # The final partial image IS the complete, full-quality image. Each partial # represents a progressive refinement, with the last one being the finished result. - if isinstance(content, DataContent) and content.additional_properties.get("is_partial_image"): + if content.type == "data" and content.additional_properties.get("is_partial_image"): print(f" Image {image_count} received") # Extract file extension from media_type (e.g., "image/png" -> "png") diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py index 5e8e9565ac..6988219dcc 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py @@ -6,8 +6,8 @@ from agent_framework import ( ChatAgent, CodeInterpreterToolCallContent, CodeInterpreterToolResultContent, + Content, HostedCodeInterpreterTool, - TextContent, tool, ) from agent_framework.openai import OpenAIResponsesClient @@ -41,13 +41,13 @@ async def main() -> None: if code_blocks: code_inputs = code_blocks[0].inputs or [] for content in code_inputs: - if isinstance(content, TextContent): + if content.type == "text": print(f"Generated code:\n{content.text}") break if outputs: print("Execution outputs:") for out in outputs[0].outputs or []: - if isinstance(out, TextContent): + if out.type == "text": print(out.text) diff --git a/python/samples/getting_started/devui/weather_agent_azure/agent.py b/python/samples/getting_started/devui/weather_agent_azure/agent.py index 56ba546135..cc2992d0c5 100644 --- a/python/samples/getting_started/devui/weather_agent_azure/agent.py +++ b/python/samples/getting_started/devui/weather_agent_azure/agent.py @@ -12,9 +12,9 @@ from agent_framework import ( ChatMessage, ChatResponse, ChatResponseUpdate, + Content, FunctionInvocationContext, Role, - TextContent, tool, chat_middleware, function_middleware, @@ -57,7 +57,7 @@ async def security_filter_middleware( # Streaming mode: return async generator async def blocked_stream() -> AsyncIterable[ChatResponseUpdate]: yield ChatResponseUpdate( - contents=[TextContent(text=error_message)], + contents=[Content.from_text(text=error_message)], role=Role.ASSISTANT, ) diff --git a/python/samples/getting_started/middleware/override_result_with_middleware.py b/python/samples/getting_started/middleware/override_result_with_middleware.py index e364eac279..a22b15a67b 100644 --- a/python/samples/getting_started/middleware/override_result_with_middleware.py +++ b/python/samples/getting_started/middleware/override_result_with_middleware.py @@ -10,8 +10,8 @@ from agent_framework import ( AgentResponseUpdate, AgentRunContext, ChatMessage, + Content, Role, - TextContent, tool, ) from agent_framework.azure import AzureAIAgentClient @@ -69,7 +69,7 @@ async def weather_override_middleware( # For streaming: create an async generator that yields chunks async def override_stream() -> AsyncIterable[AgentResponseUpdate]: for chunk in chunks: - yield AgentResponseUpdate(contents=[TextContent(text=chunk)]) + yield AgentResponseUpdate(contents=[Content.from_text(text=chunk)]) context.result = override_stream() else: diff --git a/python/samples/getting_started/workflows/orchestration/handoff_with_code_interpreter_file.py b/python/samples/getting_started/workflows/orchestration/handoff_with_code_interpreter_file.py index b1d6f394b7..54f7f4504c 100644 --- a/python/samples/getting_started/workflows/orchestration/handoff_with_code_interpreter_file.py +++ b/python/samples/getting_started/workflows/orchestration/handoff_with_code_interpreter_file.py @@ -32,12 +32,12 @@ from contextlib import asynccontextmanager from agent_framework import ( AgentRunUpdateEvent, ChatAgent, + Content, HandoffAgentUserRequest, HandoffBuilder, HostedCodeInterpreterTool, HostedFileContent, RequestInfoEvent, - TextContent, WorkflowEvent, WorkflowRunState, WorkflowStatusEvent, @@ -76,7 +76,7 @@ def _handle_events(events: list[WorkflowEvent]) -> tuple[list[RequestInfoEvent], if isinstance(content, HostedFileContent): file_ids.append(content.file_id) print(f"[Found HostedFileContent: file_id={content.file_id}]") - elif isinstance(content, TextContent) and content.annotations: + elif content.type == "text" and content.annotations: for annotation in content.annotations: if hasattr(annotation, "file_id") and annotation.file_id: file_ids.append(annotation.file_id) From 06d43ee1305b687374655dbcc8773902dd7256d1 Mon Sep 17 00:00:00 2001 From: Prasath Date: Wed, 4 Feb 2026 08:04:50 +0530 Subject: [PATCH 17/20] Fix: Skip model_deployment_name validation for application endpoints (#3621) Co-authored-by: Ravindu Prasath --- python/packages/azure-ai/agent_framework_azure_ai/_client.py | 3 +++ 1 file changed, 3 insertions(+) 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 58fb0e0397..202002a45f 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -421,6 +421,9 @@ class AzureAIClient(OpenAIBaseResponsesClient[TAzureAIClientOptions], Generic[TA @override def _check_model_presence(self, run_options: dict[str, Any]) -> None: + # Skip model check for application endpoints - model is pre-configured on server + if self._is_application_endpoint: + return if not run_options.get("model"): if not self.model_id: raise ValueError("model_deployment_name must be a non-empty string") From 5c6cf4fc92154a24397d5b6f7e7f6b8ed72a89d2 Mon Sep 17 00:00:00 2001 From: Dineshsuriya D <43177361+droideronline@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:04:26 +0530 Subject: [PATCH 18/20] Python: fix(claude): preserve $defs in JSON schema for nested Pydantic models (#3655) * fix(claude): preserve $defs in JSON schema for nested Pydantic models - Preserve $defs section from Pydantic JSON schema when converting FunctionTool to SDK MCP tool - This fixes tools with nested Pydantic models that use $ref references - Add test for nested type schema preservation Fixes #3654 * Adjust shared state import * Fix MCP tool kwargs serialization bug --------- Co-authored-by: Evan Mattson --- .../claude/agent_framework_claude/_agent.py | 3 +++ .../claude/tests/test_claude_agent.py | 27 +++++++++++++++++++ .../packages/core/agent_framework/_tools.py | 19 +++++++++++-- .../packages/core/tests/workflow/test_edge.py | 2 +- .../tests/workflow/test_workflow_states.py | 2 +- .../_workflows/_declarative_base.py | 2 +- 6 files changed, 50 insertions(+), 5 deletions(-) diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index 8335d2f149..f8f3796656 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -511,6 +511,9 @@ class ClaudeAgent(BaseAgent, Generic[TOptions]): "properties": schema.get("properties", {}), "required": schema.get("required", []), } + # Preserve $defs for nested type references (Pydantic uses $defs for nested models) + if "$defs" in schema: + input_schema["$defs"] = schema["$defs"] return SdkMcpTool( name=func_tool.name, diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py index 4402b611e4..15fc0b8090 100644 --- a/python/packages/claude/tests/test_claude_agent.py +++ b/python/packages/claude/tests/test_claude_agent.py @@ -499,6 +499,33 @@ class TestClaudeAgentToolConversion: assert sdk_tool.input_schema is not None assert "properties" in sdk_tool.input_schema # type: ignore[operator] + def test_function_tool_to_sdk_mcp_tool_preserves_defs_for_nested_types(self) -> None: + """Test that $defs is preserved for tools with nested Pydantic models.""" + from pydantic import BaseModel + + class Address(BaseModel): + street: str + city: str + + class Person(BaseModel): + name: str + address: Address + + @tool + def create_person(person: Person) -> str: + """Create a person with address.""" + return f"{person.name} lives at {person.address.street}, {person.address.city}" + + agent = ClaudeAgent() + sdk_tool = agent._function_tool_to_sdk_mcp_tool(create_person) # type: ignore[reportPrivateUsage] + + # Verify $defs is preserved in the schema + assert sdk_tool.input_schema is not None + assert "$defs" in sdk_tool.input_schema # type: ignore[operator] + assert "Address" in sdk_tool.input_schema["$defs"] # type: ignore[index] + # Verify the nested reference exists in properties + assert "person" in sdk_tool.input_schema["properties"] # type: ignore[index] + async def test_tool_handler_success(self) -> None: """Test tool handler executes successfully.""" diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 2ebd7b9015..d88fa4b54c 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -796,11 +796,26 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]): attributes = get_function_span_attributes(self, tool_call_id=tool_call_id) if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED: # type: ignore[name-defined] + # Filter out framework kwargs that are not JSON serializable + serializable_kwargs = { + k: v + for k, v in kwargs.items() + if k + not in { + "chat_options", + "tools", + "tool_choice", + "thread", + "conversation_id", + "options", + "response_format", + } + } attributes.update({ OtelAttr.TOOL_ARGUMENTS: arguments.model_dump_json() if arguments - else json.dumps(kwargs) - if kwargs + else json.dumps(serializable_kwargs, default=str) + if serializable_kwargs else "None" }) with get_function_span(attributes=attributes) as span: diff --git a/python/packages/core/tests/workflow/test_edge.py b/python/packages/core/tests/workflow/test_edge.py index 42e3893a73..95dc71219d 100644 --- a/python/packages/core/tests/workflow/test_edge.py +++ b/python/packages/core/tests/workflow/test_edge.py @@ -10,7 +10,6 @@ from agent_framework import ( Executor, InProcRunnerContext, Message, - SharedState, WorkflowContext, handler, ) @@ -24,6 +23,7 @@ from agent_framework._workflows._edge import ( SwitchCaseEdgeGroupDefault, ) from agent_framework._workflows._edge_runner import create_edge_runner +from agent_framework._workflows._shared_state import SharedState from agent_framework.observability import EdgeGroupDeliveryStatus # Add for test diff --git a/python/packages/core/tests/workflow/test_workflow_states.py b/python/packages/core/tests/workflow/test_workflow_states.py index 53baf86383..4aec349d15 100644 --- a/python/packages/core/tests/workflow/test_workflow_states.py +++ b/python/packages/core/tests/workflow/test_workflow_states.py @@ -8,7 +8,6 @@ from agent_framework import ( ExecutorFailedEvent, InProcRunnerContext, RequestInfoEvent, - SharedState, Workflow, WorkflowBuilder, WorkflowContext, @@ -20,6 +19,7 @@ from agent_framework import ( WorkflowStatusEvent, handler, ) +from agent_framework._workflows._shared_state import SharedState class FailingExecutor(Executor): diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py index 309a71a4b7..5fc34e1d7a 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py @@ -32,9 +32,9 @@ from typing import Any, Literal, cast from agent_framework._workflows import ( Executor, - SharedState, WorkflowContext, ) +from agent_framework._workflows._shared_state import SharedState from powerfx import Engine if sys.version_info >= (3, 11): From ef798629e5974aa35a6f3342647e058a97efb8be Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:57:08 +0900 Subject: [PATCH 19/20] Potential fix for code scanning alert no. 49: Clear-text logging of sensitive information (#3573) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../declarative/agent_framework_declarative/_models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/packages/declarative/agent_framework_declarative/_models.py b/python/packages/declarative/agent_framework_declarative/_models.py index 0132590a1c..3066848927 100644 --- a/python/packages/declarative/agent_framework_declarative/_models.py +++ b/python/packages/declarative/agent_framework_declarative/_models.py @@ -39,7 +39,7 @@ def _try_powerfx_eval(value: str | None, log_value: bool = True) -> str | None: Args: value: The value to check. - log_value: Whether to log the full value on error or just a snippet. + log_value: Whether to log additional context on error. """ if value is None: return value @@ -59,9 +59,9 @@ def _try_powerfx_eval(value: str | None, log_value: bool = True) -> str | None: return engine.eval(value[1:], symbols={"Env": dict(os.environ)}) except Exception as exc: if log_value: - logger.debug(f"PowerFx evaluation failed for value '{value}': {exc}") + logger.debug("PowerFx evaluation failed for a value: %s", exc) else: - logger.debug(f"PowerFx evaluation failed for value (first five characters shown) '{value[:5]}': {exc}") + logger.debug("PowerFx evaluation failed for a value (details redacted): %s", exc) return value From 838a7fd61d979d956f2cf2c81b75285c50e7b0b9 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Wed, 4 Feb 2026 11:13:23 +0100 Subject: [PATCH 20/20] Python: [BREAKING] Types API Review improvements (#3647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace Role and FinishReason classes with NewType + Literal - Remove EnumLike metaclass from _types.py - Replace Role class with NewType('Role', str) + RoleLiteral - Replace FinishReason class with NewType('FinishReason', str) + FinishReasonLiteral - Update all usages across codebase to use string literals - Remove .value access patterns (direct string comparison now works) - Add backward compatibility for legacy dict serialization format - Update tests to reflect new string-based types Addresses #3591, #3615 * Simplify ChatResponse and AgentResponse type hints (#3592) - Remove overloads from ChatResponse.__init__ - Remove text parameter from ChatResponse.__init__ - Remove | dict[str, Any] from finish_reason and usage_details params - Remove **kwargs from AgentResponse.__init__ - Both now accept ChatMessage | Sequence[ChatMessage] | None for messages - Update docstrings and examples to reflect changes - Fix tests that were using removed kwargs - Fix Role type hint usage in ag-ui utils * Remove text parameter from ChatResponseUpdate and AgentResponseUpdate (#3597) - Remove text parameter from ChatResponseUpdate.__init__ - Remove text parameter from AgentResponseUpdate.__init__ - Remove **kwargs from both update classes - Simplify contents parameter type to Sequence[Content] | None - Update all usages to use contents=[Content.from_text(...)] pattern - Fix imports in test files - Update docstrings and examples * Rename from_chat_response_updates to from_updates (#3593) - ChatResponse.from_chat_response_updates → ChatResponse.from_updates - ChatResponse.from_chat_response_generator → ChatResponse.from_update_generator - AgentResponse.from_agent_run_response_updates → AgentResponse.from_updates * Remove try_parse_value method from ChatResponse and AgentResponse (#3595) - Remove try_parse_value method from ChatResponse - Remove try_parse_value method from AgentResponse - Remove try_parse_value calls from from_updates and from_update_generator methods - Update samples to use try/except with response.value instead - Update tests to use response.value pattern - Users should now use response.value with try/except for safe parsing * Add agent_id to AgentResponse and clarify author_name documentation (#3596) - Add agent_id parameter to AgentResponse class - Document that author_name is on ChatMessage objects, not responses - Update ChatResponse docstring with author_name note - Update AgentResponse docstring with author_name note * Simplify ChatMessage.__init__ signature (#3618) - Make contents a positional argument accepting Sequence[Content | str] - Auto-convert strings in contents to TextContent - Remove overloads, keep text kwarg for backward compatibility with serialization - Update _parse_content_list to handle string items - Update all usages across codebase to use new format: ChatMessage("role", ["text"]) * Allow Content as input on run and get_response - Update prepare_messages and normalize_messages to accept Content - Update type signatures in _agents.py and _clients.py - Add tests for Content input handling * Fix ChatMessage usage across packages and samples Update all remaining ChatMessage(role=..., text=...) to use new ChatMessage('role', ['text']) signature. * Fix Role string usage and response format parsing - Fix redis provider: remove .value access on string literals - Fix durabletask ensure_response_format: set _response_format before accessing .value * Fix ollama .value and ai_model_id issues, handle None in content list - Fix ollama _chat_client: remove .value on string literals - Fix ollama _chat_client: rename ai_model_id to model_id - Fix _parse_content_list: skip None values gracefully * Fix A2AAgent type signature to include Content * Fix Role/FinishReason NewType dict annotations and improve test coverage to 95% * Fix mypy errors for Role/FinishReason NewType usage * Fix Role.TOOL and Role.ASSISTANT usage in _orchestrator_helpers.py * Fix Role NewType usage in durabletask _models.py --- python/CODING_STANDARD.md | 4 +- python/README.md | 4 +- .../a2a/agent_framework_a2a/_agent.py | 15 +- python/packages/a2a/tests/test_a2a_agent.py | 25 +- .../ag-ui/agent_framework_ag_ui/_client.py | 2 +- .../_event_converters.py | 22 +- .../_message_adapters.py | 25 +- .../ag-ui/agent_framework_ag_ui/_run.py | 2 +- .../ag-ui/agent_framework_ag_ui/_utils.py | 18 +- .../getting_started/client_with_agent.py | 2 +- .../packages/ag-ui/tests/test_ag_ui_client.py | 23 +- .../ag-ui/tests/test_event_converters.py | 20 +- python/packages/ag-ui/tests/test_helpers.py | 10 +- .../ag-ui/tests/test_message_adapters.py | 42 +- .../ag-ui/tests/test_message_hygiene.py | 4 +- python/packages/ag-ui/tests/test_run.py | 24 +- python/packages/ag-ui/tests/test_utils.py | 4 +- .../packages/ag-ui/tests/utils_test_ag_ui.py | 2 +- .../agent_framework_anthropic/_chat_client.py | 32 +- .../anthropic/tests/test_anthropic_client.py | 74 +- .../_search_provider.py | 12 +- .../tests/test_search_provider.py | 22 +- .../agent_framework_azure_ai/_chat_client.py | 21 +- .../agent_framework_azure_ai/_client.py | 2 +- .../tests/test_azure_ai_agent_client.py | 45 +- .../azure-ai/tests/test_azure_ai_client.py | 43 +- .../packages/azurefunctions/tests/test_app.py | 20 +- .../azurefunctions/tests/test_entities.py | 8 +- .../tests/test_orchestration.py | 8 +- .../agent_framework_bedrock/_chat_client.py | 34 +- .../bedrock/tests/test_bedrock_client.py | 8 +- .../bedrock/tests/test_bedrock_settings.py | 9 +- .../agent_framework_chatkit/_converter.py | 23 +- .../packages/chatkit/tests/test_converter.py | 14 +- .../packages/chatkit/tests/test_streaming.py | 16 +- .../claude/agent_framework_claude/_agent.py | 5 +- .../claude/tests/test_claude_agent.py | 12 +- .../agent_framework_copilotstudio/_agent.py | 3 +- .../copilotstudio/tests/test_copilot_agent.py | 8 +- python/packages/core/README.md | 4 +- .../packages/core/agent_framework/_agents.py | 19 +- .../packages/core/agent_framework/_clients.py | 19 +- python/packages/core/agent_framework/_mcp.py | 3 +- .../core/agent_framework/_middleware.py | 2 +- .../core/agent_framework/_serialization.py | 8 +- .../packages/core/agent_framework/_threads.py | 2 +- .../packages/core/agent_framework/_tools.py | 23 +- .../packages/core/agent_framework/_types.py | 801 ++++------- .../core/agent_framework/_workflows/_agent.py | 13 +- .../_workflows/_agent_executor.py | 6 +- .../_base_group_chat_orchestrator.py | 14 +- .../agent_framework/_workflows/_concurrent.py | 15 +- .../_workflows/_conversation_state.py | 14 +- .../agent_framework/_workflows/_group_chat.py | 8 +- .../agent_framework/_workflows/_handoff.py | 8 +- .../agent_framework/_workflows/_magentic.py | 39 +- .../_workflows/_message_utils.py | 6 +- .../_workflows/_orchestration_request_info.py | 4 +- .../_workflows/_orchestrator_helpers.py | 12 +- .../agent_framework/_workflows/_workflow.py | 2 +- .../core/agent_framework/observability.py | 17 +- .../openai/_assistants_client.py | 19 +- .../agent_framework/openai/_chat_client.py | 22 +- .../openai/_responses_client.py | 13 +- .../azure/test_azure_assistants_client.py | 12 +- .../tests/azure/test_azure_chat_client.py | 8 +- .../azure/test_azure_responses_client.py | 16 +- python/packages/core/tests/core/conftest.py | 17 +- .../packages/core/tests/core/test_agents.py | 45 +- .../core/test_as_tool_kwargs_propagation.py | 20 +- .../packages/core/tests/core/test_clients.py | 23 +- .../core/test_function_invocation_logic.py | 127 +- .../test_kwargs_propagation_to_ai_function.py | 11 +- python/packages/core/tests/core/test_mcp.py | 9 +- .../packages/core/tests/core/test_memory.py | 12 +- .../core/tests/core/test_middleware.py | 135 +- .../core/test_middleware_context_result.py | 37 +- .../tests/core/test_middleware_with_agent.py | 155 +- .../tests/core/test_middleware_with_chat.py | 49 +- .../core/tests/core/test_observability.py | 92 +- .../packages/core/tests/core/test_threads.py | 16 +- python/packages/core/tests/core/test_tools.py | 29 +- python/packages/core/tests/core/test_types.py | 1264 +++++++++++++++-- .../openai/test_openai_assistants_client.py | 47 +- .../tests/openai/test_openai_chat_client.py | 34 +- .../openai/test_openai_chat_client_base.py | 24 +- .../openai/test_openai_responses_client.py | 89 +- .../tests/workflow/test_agent_executor.py | 19 +- .../test_agent_executor_tool_calls.py | 15 +- .../workflow/test_agent_run_event_typing.py | 4 +- .../core/tests/workflow/test_concurrent.py | 11 +- .../core/tests/workflow/test_executor.py | 4 +- .../tests/workflow/test_full_conversation.py | 17 +- .../core/tests/workflow/test_group_chat.py | 37 +- .../core/tests/workflow/test_handoff.py | 35 +- .../core/tests/workflow/test_magentic.py | 79 +- .../test_orchestration_request_info.py | 21 +- .../core/tests/workflow/test_sequential.py | 33 +- .../core/tests/workflow/test_workflow.py | 3 +- .../tests/workflow/test_workflow_agent.py | 95 +- .../tests/workflow/test_workflow_builder.py | 3 +- .../tests/workflow/test_workflow_kwargs.py | 15 +- .../_workflows/_actions_agents.py | 24 +- .../_workflows/_executors_agents.py | 4 +- .../agent_framework_devui/_conversations.py | 6 +- .../devui/agent_framework_devui/_executor.py | 2 +- .../devui/tests/test_cleanup_hooks.py | 6 +- .../devui/tests/test_conversations.py | 14 +- python/packages/devui/tests/test_discovery.py | 4 +- python/packages/devui/tests/test_execution.py | 6 +- python/packages/devui/tests/test_helpers.py | 47 +- python/packages/devui/tests/test_mapper.py | 23 +- .../devui/tests/test_multimodal_workflow.py | 4 +- .../_durable_agent_state.py | 4 +- .../agent_framework_durabletask/_entities.py | 5 +- .../agent_framework_durabletask/_executors.py | 8 +- .../agent_framework_durabletask/_models.py | 20 +- .../_response_utils.py | 7 +- .../tests/test_durable_entities.py | 14 +- .../durabletask/tests/test_executors.py | 12 +- .../packages/durabletask/tests/test_models.py | 27 +- .../packages/durabletask/tests/test_shim.py | 6 +- .../agent_framework_github_copilot/_agent.py | 5 +- .../tests/test_github_copilot_agent.py | 7 +- .../_message_utils.py | 46 +- .../_sliding_window.py | 4 +- .../tau2/agent_framework_lab_tau2/runner.py | 5 +- .../lab/tau2/tests/test_message_utils.py | 64 +- .../lab/tau2/tests/test_sliding_window.py | 47 +- .../lab/tau2/tests/test_tau2_utils.py | 32 +- .../mem0/agent_framework_mem0/_provider.py | 6 +- .../mem0/tests/test_mem0_context_provider.py | 42 +- .../agent_framework_ollama/_chat_client.py | 17 +- python/packages/purview/README.md | 2 +- .../agent_framework_purview/_middleware.py | 12 +- .../purview/tests/test_chat_middleware.py | 54 +- .../packages/purview/tests/test_middleware.py | 54 +- .../packages/purview/tests/test_processor.py | 48 +- .../_chat_message_store.py | 2 +- .../redis/agent_framework_redis/_provider.py | 12 +- .../tests/test_redis_chat_message_store.py | 30 +- .../redis/tests/test_redis_provider.py | 36 +- .../01_round_robin_group_chat.py | 1 - .../orchestrations/03_swarm.py | 3 +- .../samples/demos/chatkit-integration/app.py | 10 +- .../agent_with_text_search_rag/main.py | 4 +- .../demos/m365-agent/m365_agent_demo/app.py | 4 +- .../demos/workflow_evaluation/_tools.py | 47 +- .../workflow_evaluation/create_workflow.py | 10 +- .../workflow_evaluation/run_evaluation.py | 91 +- .../agents/anthropic/anthropic_basic.py | 3 +- .../agents/azure_ai/azure_ai_basic.py | 3 +- .../azure_ai/azure_ai_provider_methods.py | 3 +- .../azure_ai/azure_ai_use_latest_version.py | 3 +- ...i_with_code_interpreter_file_generation.py | 1 - .../azure_ai_with_existing_conversation.py | 3 +- .../azure_ai_with_explicit_settings.py | 3 +- .../azure_ai/azure_ai_with_hosted_mcp.py | 4 +- .../azure_ai/azure_ai_with_response_format.py | 5 +- .../agents/azure_ai_agent/azure_ai_basic.py | 3 +- .../azure_ai_provider_methods.py | 3 +- ...i_with_code_interpreter_file_generation.py | 1 - .../azure_ai_with_existing_thread.py | 3 +- .../azure_ai_with_explicit_settings.py | 3 +- .../azure_ai_with_function_tools.py | 4 +- .../azure_ai_with_multiple_tools.py | 2 +- .../azure_ai_with_response_format.py | 10 +- .../azure_ai_agent/azure_ai_with_thread.py | 4 +- .../azure_openai/azure_assistants_basic.py | 3 +- ...zure_assistants_with_existing_assistant.py | 4 +- ...azure_assistants_with_explicit_settings.py | 3 +- .../azure_assistants_with_function_tools.py | 5 +- .../azure_assistants_with_thread.py | 4 +- .../azure_openai/azure_chat_client_basic.py | 3 +- ...zure_chat_client_with_explicit_settings.py | 3 +- .../azure_chat_client_with_function_tools.py | 5 +- .../azure_chat_client_with_thread.py | 4 +- .../azure_responses_client_basic.py | 3 +- ...responses_client_with_explicit_settings.py | 3 +- ...re_responses_client_with_function_tools.py | 5 +- .../azure_responses_client_with_hosted_mcp.py | 6 +- .../azure_responses_client_with_thread.py | 4 +- .../agents/custom/custom_agent.py | 12 +- .../agents/custom/custom_chat_client.py | 8 +- .../agents/ollama/ollama_agent_basic.py | 3 +- .../agents/ollama/ollama_chat_client.py | 3 +- .../agents/ollama/ollama_chat_multimodal.py | 4 +- .../ollama/ollama_with_openai_chat_client.py | 3 +- .../agents/openai/openai_assistants_basic.py | 3 +- .../openai_assistants_provider_methods.py | 3 +- ...enai_assistants_with_existing_assistant.py | 3 +- ...penai_assistants_with_explicit_settings.py | 3 +- .../openai_assistants_with_function_tools.py | 4 +- .../openai_assistants_with_response_format.py | 10 +- .../openai/openai_assistants_with_thread.py | 4 +- .../agents/openai/openai_chat_client_basic.py | 3 +- ...enai_chat_client_with_explicit_settings.py | 3 +- .../openai_chat_client_with_function_tools.py | 5 +- .../openai/openai_chat_client_with_thread.py | 4 +- .../openai/openai_responses_client_basic.py | 4 +- ..._responses_client_with_code_interpreter.py | 1 - ...responses_client_with_explicit_settings.py | 3 +- ...ai_responses_client_with_function_tools.py | 5 +- ...openai_responses_client_with_hosted_mcp.py | 6 +- .../openai_responses_client_with_thread.py | 4 +- .../02_multi_agent/function_app.py | 4 +- .../redis_stream_response_handler.py | 2 +- .../03_reliable_streaming/tools.py | 11 +- .../function_app.py | 14 +- .../function_app.py | 13 +- .../chat_client/azure_ai_chat_client.py | 3 +- .../chat_client/azure_assistants_client.py | 3 +- .../chat_client/azure_chat_client.py | 3 +- .../chat_client/azure_responses_client.py | 16 +- .../chat_client/openai_assistants_client.py | 3 +- .../chat_client/openai_chat_client.py | 3 +- .../chat_client/openai_responses_client.py | 3 +- .../context_providers/mem0/mem0_basic.py | 3 +- .../context_providers/mem0/mem0_oss.py | 3 +- .../context_providers/mem0/mem0_threads.py | 3 +- .../context_providers/redis/redis_basics.py | 12 +- .../simple_context_provider.py | 7 +- .../azure_openai_responses_agent.py | 7 +- .../declarative/openai_responses_agent.py | 7 +- .../devui/fanout_workflow/workflow.py | 1 - .../devui/foundry_agent/agent.py | 5 +- .../getting_started/devui/in_memory_mode.py | 4 +- .../devui/spam_workflow/workflow.py | 1 - .../devui/weather_agent_azure/agent.py | 9 +- .../durabletask/01_single_agent/client.py | 24 +- .../durabletask/01_single_agent/sample.py | 14 +- .../durabletask/01_single_agent/worker.py | 24 +- .../durabletask/02_multi_agent/client.py | 32 +- .../durabletask/02_multi_agent/sample.py | 13 +- .../durabletask/02_multi_agent/worker.py | 26 +- .../03_single_agent_streaming/client.py | 45 +- .../redis_stream_response_handler.py | 2 +- .../03_single_agent_streaming/sample.py | 14 +- .../03_single_agent_streaming/tools.py | 11 +- .../03_single_agent_streaming/worker.py | 23 +- .../client.py | 24 +- .../sample.py | 15 +- .../worker.py | 50 +- .../client.py | 22 +- .../sample.py | 13 +- .../worker.py | 58 +- .../client.py | 34 +- .../sample.py | 21 +- .../worker.py | 72 +- .../client.py | 105 +- .../sample.py | 17 +- .../worker.py | 109 +- .../self_reflection/self_reflection.py | 132 +- .../mcp/agent_as_mcp_server.py | 4 +- .../agent_and_run_level_middleware.py | 2 +- .../middleware/chat_middleware.py | 11 +- .../middleware/class_based_middleware.py | 5 +- .../middleware/decorator_middleware.py | 2 +- .../exception_handling_with_middleware.py | 4 +- .../middleware/function_based_middleware.py | 2 +- .../middleware/middleware_termination.py | 5 +- .../override_result_with_middleware.py | 5 +- .../middleware/runtime_context_delegation.py | 2 +- .../middleware/shared_state_middleware.py | 4 +- .../middleware/thread_behavior_middleware.py | 2 +- .../samples/getting_started/minimal_sample.py | 3 +- .../multimodal_input/azure_chat_multimodal.py | 4 +- .../azure_responses_multimodal.py | 6 +- .../openai_chat_multimodal.py | 8 +- .../advanced_manual_setup_console_output.py | 3 +- .../observability/advanced_zero_code.py | 3 +- .../observability/agent_observability.py | 4 +- .../agent_with_foundry_tracing.py | 4 +- .../azure_ai_agent_observability.py | 4 +- ...onfigure_otel_providers_with_parameters.py | 2 +- .../observability/workflow_observability.py | 1 - .../purview_agent/sample_purview_agent.py | 18 +- .../function_invocation_configuration.py | 3 +- .../function_tool_recover_from_failures.py | 4 +- .../tools/function_tool_with_approval.py | 8 +- ...function_tool_with_approval_and_threads.py | 4 +- .../_start-here/step1_executors_and_edges.py | 1 - .../workflows/_start-here/step3_streaming.py | 3 +- .../_start-here/step4_using_factories.py | 1 - .../azure_chat_agents_function_bridge.py | 6 +- ...re_chat_agents_tool_calls_with_feedback.py | 9 +- .../agents/custom_agent_executors.py | 3 +- .../agents/handoff_workflow_as_agent.py | 7 +- .../agents/magentic_workflow_as_agent.py | 1 - .../agents/mixed_agents_and_executors.py | 1 - .../agents/sequential_workflow_as_agent.py | 4 +- .../workflow_as_agent_human_in_the_loop.py | 4 +- .../workflow_as_agent_reflection_pattern.py | 14 +- .../agents/workflow_as_agent_with_thread.py | 8 +- .../checkpoint_with_human_in_the_loop.py | 6 +- .../checkpoint/checkpoint_with_resume.py | 1 - ...ff_with_tool_approval_checkpoint_resume.py | 4 +- .../checkpoint/sub_workflow_checkpoint.py | 1 - .../workflow_as_agent_checkpoint.py | 3 +- .../composition/sub_workflow_basics.py | 3 +- .../sub_workflow_parallel_requests.py | 1 - .../sub_workflow_request_interception.py | 1 - .../workflows/control-flow/edge_condition.py | 8 +- .../multi_selection_edge_group.py | 8 +- .../control-flow/sequential_executors.py | 1 - .../workflows/control-flow/simple_loop.py | 4 +- .../control-flow/switch_case_edge_group.py | 8 +- .../customer_support/ticketing_plugin.py | 2 +- .../declarative/function_tools/main.py | 6 +- .../agents_with_approval_requests.py | 2 +- .../concurrent_request_info.py | 10 +- .../group_chat_request_info.py | 3 +- .../guessing_game_with_human_input.py | 8 +- .../sequential_request_info.py | 5 +- .../observability/executor_io_observation.py | 1 - .../concurrent_custom_agent_executors.py | 1 - .../concurrent_custom_aggregator.py | 6 +- .../concurrent_participant_factory.py | 6 +- .../orchestration/group_chat_agent_manager.py | 4 +- .../group_chat_philosophical_debate.py | 4 +- .../group_chat_simple_selector.py | 1 - .../orchestration/handoff_autonomous.py | 5 +- .../handoff_participant_factory.py | 6 +- .../workflows/orchestration/handoff_simple.py | 6 +- .../handoff_with_code_interpreter_file.py | 3 +- .../workflows/orchestration/magentic.py | 1 - .../orchestration/magentic_checkpoint.py | 1 - .../magentic_human_plan_review.py | 1 - .../orchestration/sequential_agents.py | 4 +- .../sequential_custom_executors.py | 12 +- .../sequential_participant_factory.py | 4 +- .../parallelism/fan_out_fan_in_edges.py | 6 +- .../map_reduce_and_visualization.py | 3 +- .../shared_states_with_agents.py | 6 +- .../concurrent_builder_tool_approval.py | 2 +- .../sequential_builder_tool_approval.py | 2 +- .../concurrent_with_visualization.py | 4 +- .../orchestrations/handoff.py | 3 +- .../orchestrations/sequential.py | 4 +- .../processes/nested_process.py | 1 - .../getting_started/test_agent_samples.py | 6 +- 341 files changed, 3766 insertions(+), 3228 deletions(-) diff --git a/python/CODING_STANDARD.md b/python/CODING_STANDARD.md index 1b7b2726b8..d5f9d6f150 100644 --- a/python/CODING_STANDARD.md +++ b/python/CODING_STANDARD.md @@ -55,8 +55,8 @@ Prefer attributes over inheritance when parameters are mostly the same: # ✅ Preferred - using attributes from agent_framework import ChatMessage -user_msg = ChatMessage(role="user", content="Hello, world!") -asst_msg = ChatMessage(role="assistant", content="Hello, world!") +user_msg = ChatMessage("user", ["Hello, world!"]) +asst_msg = ChatMessage("assistant", ["Hello, world!"]) # ❌ Not preferred - unnecessary inheritance from agent_framework import UserMessage, AssistantMessage diff --git a/python/README.md b/python/README.md index 06eca19999..74d7052c12 100644 --- a/python/README.md +++ b/python/README.md @@ -113,8 +113,8 @@ async def main(): client = OpenAIChatClient() messages = [ - ChatMessage(role="system", text="You are a helpful assistant."), - ChatMessage(role="user", text="Write a haiku about Agent Framework.") + ChatMessage("system", ["You are a helpful assistant."]), + ChatMessage("user", ["Write a haiku about Agent Framework."]) ] response = await client.get_response(messages) diff --git a/python/packages/a2a/agent_framework_a2a/_agent.py b/python/packages/a2a/agent_framework_a2a/_agent.py index 00e045fba6..4dd89c6f02 100644 --- a/python/packages/a2a/agent_framework_a2a/_agent.py +++ b/python/packages/a2a/agent_framework_a2a/_agent.py @@ -32,7 +32,6 @@ from agent_framework import ( BaseAgent, ChatMessage, Content, - Role, normalize_messages, prepend_agent_framework_to_user_agent, ) @@ -187,7 +186,7 @@ class A2AAgent(BaseAgent): async def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -210,11 +209,11 @@ class A2AAgent(BaseAgent): """ # Collect all updates and use framework to consolidate updates into response updates = [update async for update in self.run_stream(messages, thread=thread, **kwargs)] - return AgentResponse.from_agent_run_response_updates(updates) + return AgentResponse.from_updates(updates) async def run_stream( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -245,7 +244,7 @@ class A2AAgent(BaseAgent): contents = self._parse_contents_from_a2a(item.parts) yield AgentResponseUpdate( contents=contents, - role=Role.ASSISTANT if item.role == A2ARole.agent else Role.USER, + role="assistant" if item.role == A2ARole.agent else "user", response_id=str(getattr(item, "message_id", uuid.uuid4())), raw_representation=item, ) @@ -269,7 +268,7 @@ class A2AAgent(BaseAgent): # Empty task yield AgentResponseUpdate( contents=[], - role=Role.ASSISTANT, + role="assistant", response_id=task.id, raw_representation=task, ) @@ -421,7 +420,7 @@ class A2AAgent(BaseAgent): contents = self._parse_contents_from_a2a(history_item.parts) messages.append( ChatMessage( - role=Role.ASSISTANT if history_item.role == A2ARole.agent else Role.USER, + role="assistant" if history_item.role == A2ARole.agent else "user", contents=contents, raw_representation=history_item, ) @@ -433,7 +432,7 @@ class A2AAgent(BaseAgent): """Parse A2A Artifact into ChatMessage using part contents.""" contents = self._parse_contents_from_a2a(artifact.parts) return ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=contents, raw_representation=artifact, ) diff --git a/python/packages/a2a/tests/test_a2a_agent.py b/python/packages/a2a/tests/test_a2a_agent.py index eca97b2ac6..cbbb16fd63 100644 --- a/python/packages/a2a/tests/test_a2a_agent.py +++ b/python/packages/a2a/tests/test_a2a_agent.py @@ -25,7 +25,6 @@ from agent_framework import ( AgentResponseUpdate, ChatMessage, Content, - Role, ) from agent_framework.a2a import A2AAgent from pytest import fixture, raises @@ -129,7 +128,7 @@ async def test_run_with_message_response(a2a_agent: A2AAgent, mock_a2a_client: M assert isinstance(response, AgentResponse) assert len(response.messages) == 1 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert response.messages[0].text == "Hello from agent!" assert response.response_id == "msg-123" assert mock_a2a_client.call_count == 1 @@ -144,7 +143,7 @@ async def test_run_with_task_response_single_artifact(a2a_agent: A2AAgent, mock_ assert isinstance(response, AgentResponse) assert len(response.messages) == 1 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert response.messages[0].text == "Generated report content" assert response.response_id == "task-456" assert mock_a2a_client.call_count == 1 @@ -170,7 +169,7 @@ async def test_run_with_task_response_multiple_artifacts(a2a_agent: A2AAgent, mo # All should be assistant messages for message in response.messages: - assert message.role == Role.ASSISTANT + assert message.role == "assistant" assert response.response_id == "task-789" @@ -233,7 +232,7 @@ def test_parse_messages_from_task_with_artifacts(a2a_agent: A2AAgent) -> None: assert len(result) == 2 assert result[0].text == "Content 1" assert result[1].text == "Content 2" - assert all(msg.role == Role.ASSISTANT for msg in result) + assert all(msg.role == "assistant" for msg in result) def test_parse_message_from_artifact(a2a_agent: A2AAgent) -> None: @@ -252,7 +251,7 @@ def test_parse_message_from_artifact(a2a_agent: A2AAgent) -> None: result = a2a_agent._parse_message_from_artifact(artifact) assert isinstance(result, ChatMessage) - assert result.role == Role.ASSISTANT + assert result.role == "assistant" assert result.text == "Artifact content" assert result.raw_representation == artifact @@ -296,7 +295,7 @@ def test_prepare_message_for_a2a_with_error_content(a2a_agent: A2AAgent) -> None # Create ChatMessage with ErrorContent error_content = Content.from_error(message="Test error message") - message = ChatMessage(role=Role.USER, contents=[error_content]) + message = ChatMessage("user", [error_content]) # Convert to A2A message a2a_message = a2a_agent._prepare_message_for_a2a(message) @@ -311,7 +310,7 @@ def test_prepare_message_for_a2a_with_uri_content(a2a_agent: A2AAgent) -> None: # Create ChatMessage with UriContent uri_content = Content.from_uri(uri="http://example.com/file.pdf", media_type="application/pdf") - message = ChatMessage(role=Role.USER, contents=[uri_content]) + message = ChatMessage("user", [uri_content]) # Convert to A2A message a2a_message = a2a_agent._prepare_message_for_a2a(message) @@ -327,7 +326,7 @@ def test_prepare_message_for_a2a_with_data_content(a2a_agent: A2AAgent) -> None: # Create ChatMessage with DataContent (base64 data URI) data_content = Content.from_uri(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain") - message = ChatMessage(role=Role.USER, contents=[data_content]) + message = ChatMessage("user", [data_content]) # Convert to A2A message a2a_message = a2a_agent._prepare_message_for_a2a(message) @@ -341,7 +340,7 @@ def test_prepare_message_for_a2a_with_data_content(a2a_agent: A2AAgent) -> None: def test_prepare_message_for_a2a_empty_contents_raises_error(a2a_agent: A2AAgent) -> None: """Test _prepare_message_for_a2a with empty contents raises ValueError.""" # Create ChatMessage with no contents - message = ChatMessage(role=Role.USER, contents=[]) + message = ChatMessage("user", []) # Should raise ValueError for empty contents with raises(ValueError, match="ChatMessage.contents is empty"): @@ -360,7 +359,7 @@ async def test_run_stream_with_message_response(a2a_agent: A2AAgent, mock_a2a_cl # Verify streaming response assert len(updates) == 1 assert isinstance(updates[0], AgentResponseUpdate) - assert updates[0].role == Role.ASSISTANT + assert updates[0].role == "assistant" assert len(updates[0].contents) == 1 content = updates[0].contents[0] @@ -408,7 +407,7 @@ def test_prepare_message_for_a2a_with_multiple_contents() -> None: # Create message with multiple content types message = ChatMessage( - role=Role.USER, + role="user", contents=[ Content.from_text(text="Here's the analysis:"), Content.from_data(data=b"binary data", media_type="application/octet-stream"), @@ -465,7 +464,7 @@ def test_prepare_message_for_a2a_with_hosted_file() -> None: # Create message with hosted file content message = ChatMessage( - role=Role.USER, + role="user", contents=[Content.from_hosted_file(file_id="hosted://storage/document.pdf")], ) diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_client.py b/python/packages/ag-ui/agent_framework_ag_ui/_client.py index 74bb50e306..340d2c125f 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_client.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_client.py @@ -334,7 +334,7 @@ class AGUIChatClient(BaseChatClient[TAGUIChatOptions], Generic[TAGUIChatOptions] Returns: ChatResponse object """ - return await ChatResponse.from_chat_response_generator( + return await ChatResponse.from_update_generator( self._inner_get_streaming_response( messages=messages, options=options, diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_event_converters.py b/python/packages/ag-ui/agent_framework_ag_ui/_event_converters.py index bd2d989f2a..7b7e99e8d4 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_event_converters.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_event_converters.py @@ -7,8 +7,6 @@ from typing import Any from agent_framework import ( ChatResponseUpdate, Content, - FinishReason, - Role, ) @@ -86,7 +84,7 @@ class AGUIEventConverter: self.run_id = event.get("runId") return ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=[], additional_properties={ "thread_id": self.thread_id, @@ -98,7 +96,7 @@ class AGUIEventConverter: """Handle TEXT_MESSAGE_START event.""" self.current_message_id = event.get("messageId") return ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", message_id=self.current_message_id, contents=[], ) @@ -112,7 +110,7 @@ class AGUIEventConverter: self.current_message_id = message_id return ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", message_id=self.current_message_id, contents=[Content.from_text(text=delta)], ) @@ -128,7 +126,7 @@ class AGUIEventConverter: self.accumulated_tool_args = "" return ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id=self.current_tool_call_id or "", @@ -144,7 +142,7 @@ class AGUIEventConverter: self.accumulated_tool_args += delta return ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id=self.current_tool_call_id or "", @@ -165,7 +163,7 @@ class AGUIEventConverter: result = event.get("result") if event.get("result") is not None else event.get("content") return ChatResponseUpdate( - role=Role.TOOL, + role="tool", contents=[ Content.from_function_result( call_id=tool_call_id, @@ -177,8 +175,8 @@ class AGUIEventConverter: def _handle_run_finished(self, event: dict[str, Any]) -> ChatResponseUpdate: """Handle RUN_FINISHED event.""" return ChatResponseUpdate( - role=Role.ASSISTANT, - finish_reason=FinishReason.STOP, + role="assistant", + finish_reason="stop", contents=[], additional_properties={ "thread_id": self.thread_id, @@ -191,8 +189,8 @@ class AGUIEventConverter: error_message = event.get("message", "Unknown error") return ChatResponseUpdate( - role=Role.ASSISTANT, - finish_reason=FinishReason.CONTENT_FILTER, + role="assistant", + finish_reason="content_filter", contents=[ Content.from_error( message=error_message, diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py b/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py index f8f1623a30..dfa64e9bdb 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py @@ -9,7 +9,6 @@ from typing import Any, cast from agent_framework import ( ChatMessage, Content, - Role, prepare_function_call_results, ) @@ -269,7 +268,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha def _find_matching_func_call(call_id: str) -> Content | None: for prev_msg in result: - role_val = prev_msg.role.value if hasattr(prev_msg.role, "value") else str(prev_msg.role) + role_val = prev_msg.role if hasattr(prev_msg.role, "value") else str(prev_msg.role) if role_val != "assistant": continue for content in prev_msg.contents or []: @@ -287,7 +286,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha return str(explicit_call_id) for prev_msg in result: - role_val = prev_msg.role.value if hasattr(prev_msg.role, "value") else str(prev_msg.role) + role_val = prev_msg.role if hasattr(prev_msg.role, "value") else str(prev_msg.role) if role_val != "assistant": continue direct_call = None @@ -396,7 +395,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha m for m in result if not ( - (m.role.value if hasattr(m.role, "value") else str(m.role)) == "tool" + (m.role if hasattr(m.role, "value") else str(m.role)) == "tool" and any( c.type == "function_result" and c.call_id == approval_call_id for c in (m.contents or []) @@ -473,14 +472,14 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha additional_properties={"ag_ui_state_args": state_args} if state_args else None, ) chat_msg = ChatMessage( - role=Role.USER, + role="user", contents=[approval_response], ) else: # No matching function call found - this is likely a confirm_changes approval # Keep the old behavior for backwards compatibility chat_msg = ChatMessage( - role=Role.USER, + role="user", contents=[Content.from_text(text=approval_payload_text)], additional_properties={"is_tool_result": True, "tool_call_id": str(tool_call_id or "")}, ) @@ -500,7 +499,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha else: func_result = str(result_content) chat_msg = ChatMessage( - role=Role.TOOL, + role="tool", contents=[Content.from_function_result(call_id=str(tool_call_id), result=func_result)], ) if "id" in msg: @@ -516,7 +515,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha result_content = msg.get("result", msg.get("content", "")) chat_msg = ChatMessage( - role=Role.TOOL, + role="tool", contents=[Content.from_function_result(call_id=str(tool_call_id), result=result_content)], ) if "id" in msg: @@ -554,7 +553,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha arguments=arguments, ) ) - chat_msg = ChatMessage(role=Role.ASSISTANT, contents=contents) + chat_msg = ChatMessage("assistant", contents) if "id" in msg: chat_msg.message_id = msg["id"] result.append(chat_msg) @@ -562,7 +561,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha # No special handling required for assistant/plain messages here - role = AGUI_TO_FRAMEWORK_ROLE.get(role_str, Role.USER) + role = AGUI_TO_FRAMEWORK_ROLE.get(role_str, "user") # Check if this message contains function approvals if "function_approvals" in msg and msg["function_approvals"]: @@ -584,14 +583,14 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha ) approval_contents.append(approval_response) - chat_msg = ChatMessage(role=role, contents=approval_contents) # type: ignore[arg-type] + chat_msg = ChatMessage(role, approval_contents) # type: ignore[arg-type] else: # Regular text message content = msg.get("content", "") if isinstance(content, str): - chat_msg = ChatMessage(role=role, contents=[Content.from_text(text=content)]) + chat_msg = ChatMessage(role, [Content.from_text(text=content)]) else: - chat_msg = ChatMessage(role=role, contents=[Content.from_text(text=str(content))]) + chat_msg = ChatMessage(role, [Content.from_text(text=str(content))]) if "id" in msg: chat_msg.message_id = msg["id"] diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_run.py b/python/packages/ag-ui/agent_framework_ag_ui/_run.py index d1229620a7..7cd9e0c686 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_run.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_run.py @@ -862,7 +862,7 @@ async def run_agent_stream( from pydantic import BaseModel logger.info(f"Processing structured output, update count: {len(all_updates)}") - final_response = AgentResponse.from_agent_run_response_updates(all_updates, output_format_type=response_format) + final_response = AgentResponse.from_updates(all_updates, output_format_type=response_format) if final_response.value and isinstance(final_response.value, BaseModel): response_dict = final_response.value.model_dump(mode="json", exclude_none=True) diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_utils.py b/python/packages/ag-ui/agent_framework_ag_ui/_utils.py index f7f01261f5..bb33c3279e 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_utils.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_utils.py @@ -10,19 +10,19 @@ from dataclasses import asdict, is_dataclass from datetime import date, datetime from typing import Any -from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool, Role, ToolProtocol +from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool, ToolProtocol # Role mapping constants -AGUI_TO_FRAMEWORK_ROLE: dict[str, Role] = { - "user": Role.USER, - "assistant": Role.ASSISTANT, - "system": Role.SYSTEM, +AGUI_TO_FRAMEWORK_ROLE: dict[str, str] = { + "user": "user", + "assistant": "assistant", + "system": "system", } -FRAMEWORK_TO_AGUI_ROLE: dict[Role, str] = { - Role.USER: "user", - Role.ASSISTANT: "assistant", - Role.SYSTEM: "system", +FRAMEWORK_TO_AGUI_ROLE: dict[str, str] = { + "user": "user", + "assistant": "assistant", + "system": "system", } ALLOWED_AGUI_ROLES: set[str] = {"user", "assistant", "system", "tool"} diff --git a/python/packages/ag-ui/getting_started/client_with_agent.py b/python/packages/ag-ui/getting_started/client_with_agent.py index be23404583..1a17a8e618 100644 --- a/python/packages/ag-ui/getting_started/client_with_agent.py +++ b/python/packages/ag-ui/getting_started/client_with_agent.py @@ -171,7 +171,7 @@ async def main(): messages = await thread.message_store.list_messages() print(f"\n[THREAD STATE] {len(messages)} messages in thread's message_store") for i, msg in enumerate(messages[-6:], 1): # Show last 6 - role = msg.role.value if hasattr(msg.role, "value") else str(msg.role) + role = msg.role if hasattr(msg.role, "value") else str(msg.role) text_preview = _preview_for_message(msg) print(f" {i}. [{role}]: {text_preview}") diff --git a/python/packages/ag-ui/tests/test_ag_ui_client.py b/python/packages/ag-ui/tests/test_ag_ui_client.py index af9c7fb916..5f4ad1794b 100644 --- a/python/packages/ag-ui/tests/test_ag_ui_client.py +++ b/python/packages/ag-ui/tests/test_ag_ui_client.py @@ -12,7 +12,6 @@ from agent_framework import ( ChatResponse, ChatResponseUpdate, Content, - Role, tool, ) from pytest import MonkeyPatch @@ -76,8 +75,8 @@ class TestAGUIChatClient: """Test state extraction when no state is present.""" client = TestableAGUIChatClient(endpoint="http://localhost:8888/") messages = [ - ChatMessage(role="user", text="Hello"), - ChatMessage(role="assistant", text="Hi there"), + ChatMessage("user", ["Hello"]), + ChatMessage("assistant", ["Hi there"]), ] result_messages, state = client.extract_state_from_messages(messages) @@ -96,7 +95,7 @@ class TestAGUIChatClient: state_b64 = base64.b64encode(state_json.encode("utf-8")).decode("utf-8") messages = [ - ChatMessage(role="user", text="Hello"), + ChatMessage("user", ["Hello"]), ChatMessage( role="user", contents=[Content.from_uri(uri=f"data:application/json;base64,{state_b64}")], @@ -134,8 +133,8 @@ class TestAGUIChatClient: """Test message conversion to AG-UI format.""" client = TestableAGUIChatClient(endpoint="http://localhost:8888/") messages = [ - ChatMessage(role=Role.USER, text="What is the weather?"), - ChatMessage(role=Role.ASSISTANT, text="Let me check.", message_id="msg_123"), + ChatMessage("user", ["What is the weather?"]), + ChatMessage("assistant", ["Let me check."], message_id="msg_123"), ] agui_messages = client.convert_messages_to_agui_format(messages) @@ -182,7 +181,7 @@ class TestAGUIChatClient: client = TestableAGUIChatClient(endpoint="http://localhost:8888/") monkeypatch.setattr(client.http_service, "post_run", mock_post_run) - messages = [ChatMessage(role="user", text="Test message")] + messages = [ChatMessage("user", ["Test message"])] chat_options = ChatOptions() updates: list[ChatResponseUpdate] = [] @@ -215,7 +214,7 @@ class TestAGUIChatClient: client = TestableAGUIChatClient(endpoint="http://localhost:8888/") monkeypatch.setattr(client.http_service, "post_run", mock_post_run) - messages = [ChatMessage(role="user", text="Test message")] + messages = [ChatMessage("user", ["Test message"])] chat_options = {} response = await client.inner_get_response(messages=messages, options=chat_options) @@ -258,7 +257,7 @@ class TestAGUIChatClient: client = TestableAGUIChatClient(endpoint="http://localhost:8888/") monkeypatch.setattr(client.http_service, "post_run", mock_post_run) - messages = [ChatMessage(role="user", text="Test with tools")] + messages = [ChatMessage("user", ["Test with tools"])] chat_options = ChatOptions(tools=[test_tool]) response = await client.inner_get_response(messages=messages, options=chat_options) @@ -282,7 +281,7 @@ class TestAGUIChatClient: client = TestableAGUIChatClient(endpoint="http://localhost:8888/") monkeypatch.setattr(client.http_service, "post_run", mock_post_run) - messages = [ChatMessage(role="user", text="Test server tool execution")] + messages = [ChatMessage("user", ["Test server tool execution"])] updates: list[ChatResponseUpdate] = [] async for update in client.get_streaming_response(messages): @@ -324,7 +323,7 @@ class TestAGUIChatClient: client = TestableAGUIChatClient(endpoint="http://localhost:8888/") monkeypatch.setattr(client.http_service, "post_run", mock_post_run) - messages = [ChatMessage(role="user", text="Test server tool execution")] + messages = [ChatMessage("user", ["Test server tool execution"])] async for _ in client.get_streaming_response(messages, options={"tool_choice": "auto", "tools": [client_tool]}): pass @@ -338,7 +337,7 @@ class TestAGUIChatClient: state_b64 = base64.b64encode(state_json.encode("utf-8")).decode("utf-8") messages = [ - ChatMessage(role="user", text="Hello"), + ChatMessage("user", ["Hello"]), ChatMessage( role="user", contents=[Content.from_uri(uri=f"data:application/json;base64,{state_b64}")], diff --git a/python/packages/ag-ui/tests/test_event_converters.py b/python/packages/ag-ui/tests/test_event_converters.py index ff4d2ddc91..f26013a3fe 100644 --- a/python/packages/ag-ui/tests/test_event_converters.py +++ b/python/packages/ag-ui/tests/test_event_converters.py @@ -2,8 +2,6 @@ """Tests for AG-UI event converter.""" -from agent_framework import FinishReason, Role - from agent_framework_ag_ui._event_converters import AGUIEventConverter @@ -22,7 +20,7 @@ class TestAGUIEventConverter: update = converter.convert_event(event) assert update is not None - assert update.role == Role.ASSISTANT + assert update.role == "assistant" assert update.additional_properties["thread_id"] == "thread_123" assert update.additional_properties["run_id"] == "run_456" assert converter.thread_id == "thread_123" @@ -39,7 +37,7 @@ class TestAGUIEventConverter: update = converter.convert_event(event) assert update is not None - assert update.role == Role.ASSISTANT + assert update.role == "assistant" assert update.message_id == "msg_789" assert converter.current_message_id == "msg_789" @@ -55,7 +53,7 @@ class TestAGUIEventConverter: update = converter.convert_event(event) assert update is not None - assert update.role == Role.ASSISTANT + assert update.role == "assistant" assert update.message_id == "msg_1" assert len(update.contents) == 1 assert update.contents[0].text == "Hello" @@ -101,7 +99,7 @@ class TestAGUIEventConverter: update = converter.convert_event(event) assert update is not None - assert update.role == Role.ASSISTANT + assert update.role == "assistant" assert len(update.contents) == 1 assert update.contents[0].call_id == "call_123" assert update.contents[0].name == "get_weather" @@ -184,7 +182,7 @@ class TestAGUIEventConverter: update = converter.convert_event(event) assert update is not None - assert update.role == Role.TOOL + assert update.role == "tool" assert len(update.contents) == 1 assert update.contents[0].call_id == "call_123" assert update.contents[0].result == {"temperature": 22, "condition": "sunny"} @@ -204,8 +202,8 @@ class TestAGUIEventConverter: update = converter.convert_event(event) assert update is not None - assert update.role == Role.ASSISTANT - assert update.finish_reason == FinishReason.STOP + assert update.role == "assistant" + assert update.finish_reason == "stop" assert update.additional_properties["thread_id"] == "thread_123" assert update.additional_properties["run_id"] == "run_456" @@ -223,8 +221,8 @@ class TestAGUIEventConverter: update = converter.convert_event(event) assert update is not None - assert update.role == Role.ASSISTANT - assert update.finish_reason == FinishReason.CONTENT_FILTER + assert update.role == "assistant" + assert update.finish_reason == "content_filter" assert len(update.contents) == 1 assert update.contents[0].message == "Connection timeout" assert update.contents[0].error_code == "RUN_ERROR" diff --git a/python/packages/ag-ui/tests/test_helpers.py b/python/packages/ag-ui/tests/test_helpers.py index b4a7e9f047..2fdd1d6771 100644 --- a/python/packages/ag-ui/tests/test_helpers.py +++ b/python/packages/ag-ui/tests/test_helpers.py @@ -29,8 +29,8 @@ class TestPendingToolCallIds: def test_no_tool_calls(self): """Returns empty set when no tool calls in messages.""" messages = [ - ChatMessage(role="user", contents=[Content.from_text("Hello")]), - ChatMessage(role="assistant", contents=[Content.from_text("Hi there")]), + ChatMessage("user", [Content.from_text("Hello")]), + ChatMessage("assistant", [Content.from_text("Hi there")]), ] result = pending_tool_call_ids(messages) assert result == set() @@ -114,7 +114,7 @@ class TestIsStateContextMessage: def test_empty_contents(self): """Returns False for message with empty contents.""" - message = ChatMessage(role="system", contents=[]) + message = ChatMessage("system", []) assert is_state_context_message(message) is False @@ -342,7 +342,7 @@ class TestLatestApprovalResponse: def test_no_approval_response(self): """Returns None when no approval response in last message.""" messages = [ - ChatMessage(role="assistant", contents=[Content.from_text("Hello")]), + ChatMessage("assistant", [Content.from_text("Hello")]), ] result = latest_approval_response(messages) assert result is None @@ -357,7 +357,7 @@ class TestLatestApprovalResponse: function_call=fc, ) messages = [ - ChatMessage(role="user", contents=[approval_content]), + ChatMessage("user", [approval_content]), ] result = latest_approval_response(messages) assert result is approval_content diff --git a/python/packages/ag-ui/tests/test_message_adapters.py b/python/packages/ag-ui/tests/test_message_adapters.py index 4f6c3f1d42..85fe778e09 100644 --- a/python/packages/ag-ui/tests/test_message_adapters.py +++ b/python/packages/ag-ui/tests/test_message_adapters.py @@ -5,7 +5,7 @@ import json import pytest -from agent_framework import ChatMessage, Content, Role +from agent_framework import ChatMessage, Content from agent_framework_ag_ui._message_adapters import ( agent_framework_messages_to_agui, @@ -24,7 +24,7 @@ def sample_agui_message(): @pytest.fixture def sample_agent_framework_message(): """Create a sample Agent Framework message.""" - return ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")], message_id="msg-123") + return ChatMessage("user", [Content.from_text(text="Hello")], message_id="msg-123") def test_agui_to_agent_framework_basic(sample_agui_message): @@ -32,7 +32,7 @@ def test_agui_to_agent_framework_basic(sample_agui_message): messages = agui_messages_to_agent_framework([sample_agui_message]) assert len(messages) == 1 - assert messages[0].role == Role.USER + assert messages[0].role == "user" assert messages[0].message_id == "msg-123" @@ -86,7 +86,7 @@ def test_agui_tool_result_to_agent_framework(): assert len(messages) == 1 message = messages[0] - assert message.role == Role.USER + assert message.role == "user" assert len(message.contents) == 1 assert message.contents[0].type == "text" @@ -328,9 +328,9 @@ def test_agui_multiple_messages_to_agent_framework(): messages = agui_messages_to_agent_framework(messages_input) assert len(messages) == 3 - assert messages[0].role == Role.USER - assert messages[1].role == Role.ASSISTANT - assert messages[2].role == Role.USER + assert messages[0].role == "user" + assert messages[1].role == "assistant" + assert messages[2].role == "user" def test_agui_empty_messages(): @@ -366,7 +366,7 @@ def test_agui_function_approvals(): assert len(messages) == 1 msg = messages[0] - assert msg.role == Role.USER + assert msg.role == "user" assert len(msg.contents) == 2 assert msg.contents[0].type == "function_approval_response" @@ -385,7 +385,7 @@ def test_agui_system_role(): messages = agui_messages_to_agent_framework([{"role": "system", "content": "System prompt"}]) assert len(messages) == 1 - assert messages[0].role == Role.SYSTEM + assert messages[0].role == "system" def test_agui_non_string_content(): @@ -425,7 +425,7 @@ def test_agui_with_tool_calls_to_agent_framework(): assert len(messages) == 1 msg = messages[0] - assert msg.role == Role.ASSISTANT + assert msg.role == "assistant" assert msg.message_id == "msg-789" # First content is text, second is the function call assert msg.contents[0].type == "text" @@ -439,7 +439,7 @@ def test_agui_with_tool_calls_to_agent_framework(): def test_agent_framework_to_agui_with_tool_calls(): """Test converting Agent Framework message with tool calls to AG-UI.""" msg = ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_text(text="Calling tool"), Content.from_function_call(call_id="call-123", name="search", arguments={"query": "test"}), @@ -464,7 +464,7 @@ def test_agent_framework_to_agui_with_tool_calls(): def test_agent_framework_to_agui_multiple_text_contents(): """Test concatenating multiple text contents.""" msg = ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(text="Part 1 "), Content.from_text(text="Part 2")], ) @@ -476,7 +476,7 @@ def test_agent_framework_to_agui_multiple_text_contents(): def test_agent_framework_to_agui_no_message_id(): """Test message without message_id - should auto-generate ID.""" - msg = ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")]) + msg = ChatMessage("user", [Content.from_text(text="Hello")]) messages = agent_framework_messages_to_agui([msg]) @@ -488,7 +488,7 @@ def test_agent_framework_to_agui_no_message_id(): def test_agent_framework_to_agui_system_role(): """Test system role conversion.""" - msg = ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="System")]) + msg = ChatMessage("system", [Content.from_text(text="System")]) messages = agent_framework_messages_to_agui([msg]) @@ -534,7 +534,7 @@ def test_extract_text_from_custom_contents(): def test_agent_framework_to_agui_function_result_dict(): """Test converting FunctionResultContent with dict result to AG-UI.""" msg = ChatMessage( - role=Role.TOOL, + role="tool", contents=[Content.from_function_result(call_id="call-123", result={"key": "value", "count": 42})], message_id="msg-789", ) @@ -551,7 +551,7 @@ def test_agent_framework_to_agui_function_result_dict(): def test_agent_framework_to_agui_function_result_none(): """Test converting FunctionResultContent with None result to AG-UI.""" msg = ChatMessage( - role=Role.TOOL, + role="tool", contents=[Content.from_function_result(call_id="call-123", result=None)], message_id="msg-789", ) @@ -567,7 +567,7 @@ def test_agent_framework_to_agui_function_result_none(): def test_agent_framework_to_agui_function_result_string(): """Test converting FunctionResultContent with string result to AG-UI.""" msg = ChatMessage( - role=Role.TOOL, + role="tool", contents=[Content.from_function_result(call_id="call-123", result="plain text result")], message_id="msg-789", ) @@ -582,7 +582,7 @@ def test_agent_framework_to_agui_function_result_string(): def test_agent_framework_to_agui_function_result_empty_list(): """Test converting FunctionResultContent with empty list result to AG-UI.""" msg = ChatMessage( - role=Role.TOOL, + role="tool", contents=[Content.from_function_result(call_id="call-123", result=[])], message_id="msg-789", ) @@ -604,7 +604,7 @@ def test_agent_framework_to_agui_function_result_single_text_content(): text: str msg = ChatMessage( - role=Role.TOOL, + role="tool", contents=[Content.from_function_result(call_id="call-123", result=[MockTextContent("Hello from MCP!")])], message_id="msg-789", ) @@ -626,7 +626,7 @@ def test_agent_framework_to_agui_function_result_multiple_text_contents(): text: str msg = ChatMessage( - role=Role.TOOL, + role="tool", contents=[ Content.from_function_result( call_id="call-123", @@ -723,7 +723,7 @@ def test_agui_to_agent_framework_tool_result(): assert len(result) == 2 # Second message should be tool result tool_msg = result[1] - assert tool_msg.role == Role.TOOL + assert tool_msg.role == "tool" assert tool_msg.contents[0].type == "function_result" assert tool_msg.contents[0].result == "Sunny" diff --git a/python/packages/ag-ui/tests/test_message_hygiene.py b/python/packages/ag-ui/tests/test_message_hygiene.py index ecc01de3cb..03c8a1b9b3 100644 --- a/python/packages/ag-ui/tests/test_message_hygiene.py +++ b/python/packages/ag-ui/tests/test_message_hygiene.py @@ -25,9 +25,7 @@ def test_sanitize_tool_history_injects_confirm_changes_result() -> None: sanitized = _sanitize_tool_history(messages) - tool_messages = [ - msg for msg in sanitized if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "tool" - ] + tool_messages = [msg for msg in sanitized if (msg.role if hasattr(msg.role, "value") else str(msg.role)) == "tool"] assert len(tool_messages) == 1 assert str(tool_messages[0].contents[0].call_id) == "call_confirm_123" assert tool_messages[0].contents[0].result == "Confirmed" diff --git a/python/packages/ag-ui/tests/test_run.py b/python/packages/ag-ui/tests/test_run.py index a415000692..7fb7055ae0 100644 --- a/python/packages/ag-ui/tests/test_run.py +++ b/python/packages/ag-ui/tests/test_run.py @@ -188,7 +188,6 @@ class TestCreateStateContextMessage: def test_creates_message(self): """Creates state context message.""" - from agent_framework import Role state = {"document": "Hello world"} schema = {"properties": {"document": {"type": "string"}}} @@ -196,7 +195,7 @@ class TestCreateStateContextMessage: result = _create_state_context_message(state, schema) assert result is not None - assert result.role == Role.SYSTEM + assert result.role == "system" assert len(result.contents) == 1 assert "Hello world" in result.contents[0].text assert "Current state" in result.contents[0].text @@ -207,7 +206,7 @@ class TestInjectStateContext: def test_no_state_message(self): """Returns original messages when no state context needed.""" - messages = [ChatMessage(role="user", contents=[Content.from_text("Hello")])] + messages = [ChatMessage("user", [Content.from_text("Hello")])] result = _inject_state_context(messages, {}, {}) assert result == messages @@ -219,8 +218,8 @@ class TestInjectStateContext: def test_last_message_not_user(self): """Returns original messages when last message is not from user.""" messages = [ - ChatMessage(role="user", contents=[Content.from_text("Hello")]), - ChatMessage(role="assistant", contents=[Content.from_text("Hi")]), + ChatMessage("user", [Content.from_text("Hello")]), + ChatMessage("assistant", [Content.from_text("Hi")]), ] state = {"key": "value"} schema = {"properties": {"key": {"type": "string"}}} @@ -230,11 +229,10 @@ class TestInjectStateContext: def test_injects_before_last_user_message(self): """Injects state context before last user message.""" - from agent_framework import Role messages = [ - ChatMessage(role="system", contents=[Content.from_text("You are helpful")]), - ChatMessage(role="user", contents=[Content.from_text("Hello")]), + ChatMessage("system", [Content.from_text("You are helpful")]), + ChatMessage("user", [Content.from_text("Hello")]), ] state = {"document": "content"} schema = {"properties": {"document": {"type": "string"}}} @@ -243,13 +241,13 @@ class TestInjectStateContext: assert len(result) == 3 # System message first - assert result[0].role == Role.SYSTEM + assert result[0].role == "system" assert "helpful" in result[0].contents[0].text # State context second - assert result[1].role == Role.SYSTEM + assert result[1].role == "system" assert "Current state" in result[1].contents[0].text # User message last - assert result[2].role == Role.USER + assert result[2].role == "user" assert "Hello" in result[2].contents[0].text @@ -357,7 +355,7 @@ def test_extract_approved_state_updates_no_handler(): """Test _extract_approved_state_updates returns empty with no handler.""" from agent_framework_ag_ui._run import _extract_approved_state_updates - messages = [ChatMessage(role="user", contents=[Content.from_text("Hello")])] + messages = [ChatMessage("user", [Content.from_text("Hello")])] result = _extract_approved_state_updates(messages, None) assert result == {} @@ -368,6 +366,6 @@ def test_extract_approved_state_updates_no_approval(): from agent_framework_ag_ui._run import _extract_approved_state_updates handler = PredictiveStateHandler(predict_state_config={"doc": {"tool": "write", "tool_argument": "content"}}) - messages = [ChatMessage(role="user", contents=[Content.from_text("Hello")])] + messages = [ChatMessage("user", [Content.from_text("Hello")])] result = _extract_approved_state_updates(messages, handler) assert result == {} diff --git a/python/packages/ag-ui/tests/test_utils.py b/python/packages/ag-ui/tests/test_utils.py index 7f1de812c4..41b8e3665b 100644 --- a/python/packages/ag-ui/tests/test_utils.py +++ b/python/packages/ag-ui/tests/test_utils.py @@ -404,11 +404,11 @@ def test_safe_json_parse_with_none(): def test_get_role_value_with_enum(): """Test get_role_value with enum role.""" - from agent_framework import ChatMessage, Content, Role + from agent_framework import ChatMessage, Content from agent_framework_ag_ui._utils import get_role_value - message = ChatMessage(role=Role.USER, contents=[Content.from_text("test")]) + message = ChatMessage("user", [Content.from_text("test")]) result = get_role_value(message) assert result == "user" diff --git a/python/packages/ag-ui/tests/utils_test_ag_ui.py b/python/packages/ag-ui/tests/utils_test_ag_ui.py index 5c2415583c..9ac9b04df4 100644 --- a/python/packages/ag-ui/tests/utils_test_ag_ui.py +++ b/python/packages/ag-ui/tests/utils_test_ag_ui.py @@ -56,7 +56,7 @@ class StreamingChatClientStub(BaseChatClient[TOptions_co], Generic[TOptions_co]) contents.extend(update.contents) return ChatResponse( - messages=[ChatMessage(role="assistant", contents=contents)], + messages=[ChatMessage("assistant", contents)], response_id="stub-response", ) diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 630b92ca02..901a42122f 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -13,12 +13,10 @@ from agent_framework import ( ChatResponse, ChatResponseUpdate, Content, - FinishReason, FunctionTool, HostedCodeInterpreterTool, HostedMCPTool, HostedWebSearchTool, - Role, TextSpanRegion, UsageDetails, get_logger, @@ -172,20 +170,20 @@ OPTION_TRANSLATIONS: dict[str, str] = { # region Role and Finish Reason Maps -ROLE_MAP: dict[Role, str] = { - Role.USER: "user", - Role.ASSISTANT: "assistant", - Role.SYSTEM: "user", - Role.TOOL: "user", +ROLE_MAP: dict[str, str] = { + "user": "user", + "assistant": "assistant", + "system": "user", + "tool": "user", } -FINISH_REASON_MAP: dict[str, FinishReason] = { - "stop_sequence": FinishReason.STOP, - "max_tokens": FinishReason.LENGTH, - "tool_use": FinishReason.TOOL_CALLS, - "end_turn": FinishReason.STOP, - "refusal": FinishReason.CONTENT_FILTER, - "pause_turn": FinishReason.STOP, +FINISH_REASON_MAP: dict[str, str] = { + "stop_sequence": "stop", + "max_tokens": "length", + "tool_use": "tool_calls", + "end_turn": "stop", + "refusal": "content_filter", + "pause_turn": "stop", } @@ -415,7 +413,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio run_options["messages"] = self._prepare_messages_for_anthropic(messages) # system message - first system message is passed as instructions - if messages and isinstance(messages[0], ChatMessage) and messages[0].role == Role.SYSTEM: + if messages and isinstance(messages[0], ChatMessage) and messages[0].role == "system": run_options["system"] = messages[0].text # betas @@ -502,7 +500,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio as Anthropic expects system instructions as a separate parameter. """ # first system message is passed as instructions - if messages and isinstance(messages[0], ChatMessage) and messages[0].role == Role.SYSTEM: + if messages and isinstance(messages[0], ChatMessage) and messages[0].role == "system": return [self._prepare_message_for_anthropic(msg) for msg in messages[1:]] return [self._prepare_message_for_anthropic(msg) for msg in messages] @@ -673,7 +671,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio response_id=message.id, messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=self._parse_contents_from_anthropic(message.content), raw_representation=message, ) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 6b06843b73..516f644ea7 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -11,11 +11,9 @@ from agent_framework import ( ChatOptions, ChatResponseUpdate, Content, - FinishReason, HostedCodeInterpreterTool, HostedMCPTool, HostedWebSearchTool, - Role, tool, ) from agent_framework.exceptions import ServiceInitializationError @@ -150,7 +148,7 @@ def test_anthropic_client_service_url(mock_anthropic_client: MagicMock) -> None: def test_prepare_message_for_anthropic_text(mock_anthropic_client: MagicMock) -> None: """Test converting text message to Anthropic format.""" chat_client = create_test_anthropic_client(mock_anthropic_client) - message = ChatMessage(role=Role.USER, text="Hello, world!") + message = ChatMessage("user", ["Hello, world!"]) result = chat_client._prepare_message_for_anthropic(message) @@ -164,7 +162,7 @@ def test_prepare_message_for_anthropic_function_call(mock_anthropic_client: Magi """Test converting function call message to Anthropic format.""" chat_client = create_test_anthropic_client(mock_anthropic_client) message = ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="call_123", @@ -188,7 +186,7 @@ def test_prepare_message_for_anthropic_function_result(mock_anthropic_client: Ma """Test converting function result message to Anthropic format.""" chat_client = create_test_anthropic_client(mock_anthropic_client) message = ChatMessage( - role=Role.TOOL, + role="tool", contents=[ Content.from_function_result( call_id="call_123", @@ -213,7 +211,7 @@ def test_prepare_message_for_anthropic_text_reasoning(mock_anthropic_client: Mag """Test converting text reasoning message to Anthropic format.""" chat_client = create_test_anthropic_client(mock_anthropic_client) message = ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text_reasoning(text="Let me think about this...")], ) @@ -229,8 +227,8 @@ def test_prepare_messages_for_anthropic_with_system(mock_anthropic_client: Magic """Test converting messages list with system message.""" chat_client = create_test_anthropic_client(mock_anthropic_client) messages = [ - ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant."), - ChatMessage(role=Role.USER, text="Hello!"), + ChatMessage("system", ["You are a helpful assistant."]), + ChatMessage("user", ["Hello!"]), ] result = chat_client._prepare_messages_for_anthropic(messages) @@ -245,8 +243,8 @@ def test_prepare_messages_for_anthropic_without_system(mock_anthropic_client: Ma """Test converting messages list without system message.""" chat_client = create_test_anthropic_client(mock_anthropic_client) messages = [ - ChatMessage(role=Role.USER, text="Hello!"), - ChatMessage(role=Role.ASSISTANT, text="Hi there!"), + ChatMessage("user", ["Hello!"]), + ChatMessage("assistant", ["Hi there!"]), ] result = chat_client._prepare_messages_for_anthropic(messages) @@ -374,7 +372,7 @@ async def test_prepare_options_basic(mock_anthropic_client: MagicMock) -> None: """Test _prepare_options with basic ChatOptions.""" chat_client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options = ChatOptions(max_tokens=100, temperature=0.7) run_options = chat_client._prepare_options(messages, chat_options) @@ -390,8 +388,8 @@ async def test_prepare_options_with_system_message(mock_anthropic_client: MagicM chat_client = create_test_anthropic_client(mock_anthropic_client) messages = [ - ChatMessage(role=Role.SYSTEM, text="You are helpful."), - ChatMessage(role=Role.USER, text="Hello"), + ChatMessage("system", ["You are helpful."]), + ChatMessage("user", ["Hello"]), ] chat_options = ChatOptions() @@ -405,7 +403,7 @@ async def test_prepare_options_with_tool_choice_auto(mock_anthropic_client: Magi """Test _prepare_options with auto tool choice.""" chat_client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options = ChatOptions(tool_choice="auto") run_options = chat_client._prepare_options(messages, chat_options) @@ -417,7 +415,7 @@ async def test_prepare_options_with_tool_choice_required(mock_anthropic_client: """Test _prepare_options with required tool choice.""" chat_client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] # For required with specific function, need to pass as dict chat_options = ChatOptions(tool_choice={"mode": "required", "required_function_name": "get_weather"}) @@ -431,7 +429,7 @@ async def test_prepare_options_with_tool_choice_none(mock_anthropic_client: Magi """Test _prepare_options with none tool choice.""" chat_client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options = ChatOptions(tool_choice="none") run_options = chat_client._prepare_options(messages, chat_options) @@ -448,7 +446,7 @@ async def test_prepare_options_with_tools(mock_anthropic_client: MagicMock) -> N """Get weather for a location.""" return f"Weather for {location}" - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options = ChatOptions(tools=[get_weather]) run_options = chat_client._prepare_options(messages, chat_options) @@ -461,7 +459,7 @@ async def test_prepare_options_with_stop_sequences(mock_anthropic_client: MagicM """Test _prepare_options with stop sequences.""" chat_client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options = ChatOptions(stop=["STOP", "END"]) run_options = chat_client._prepare_options(messages, chat_options) @@ -473,7 +471,7 @@ async def test_prepare_options_with_top_p(mock_anthropic_client: MagicMock) -> N """Test _prepare_options with top_p.""" chat_client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options = ChatOptions(top_p=0.9) run_options = chat_client._prepare_options(messages, chat_options) @@ -500,11 +498,11 @@ def test_process_message_basic(mock_anthropic_client: MagicMock) -> None: assert response.response_id == "msg_123" assert response.model_id == "claude-3-5-sonnet-20241022" assert len(response.messages) == 1 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert len(response.messages[0].contents) == 1 assert response.messages[0].contents[0].type == "text" assert response.messages[0].contents[0].text == "Hello there!" - assert response.finish_reason == FinishReason.STOP + assert response.finish_reason == "stop" assert response.usage_details is not None assert response.usage_details["input_token_count"] == 10 assert response.usage_details["output_token_count"] == 5 @@ -534,7 +532,7 @@ def test_process_message_with_tool_use(mock_anthropic_client: MagicMock) -> None assert response.messages[0].contents[0].type == "function_call" assert response.messages[0].contents[0].call_id == "call_123" assert response.messages[0].contents[0].name == "get_weather" - assert response.finish_reason == FinishReason.TOOL_CALLS + assert response.finish_reason == "tool_calls" def test_parse_usage_from_anthropic_basic(mock_anthropic_client: MagicMock) -> None: @@ -668,7 +666,7 @@ async def test_inner_get_response(mock_anthropic_client: MagicMock) -> None: mock_anthropic_client.beta.messages.create.return_value = mock_message - messages = [ChatMessage(role=Role.USER, text="Hi")] + messages = [ChatMessage("user", ["Hi"])] chat_options = ChatOptions(max_tokens=10) response = await chat_client._inner_get_response( # type: ignore[attr-defined] @@ -692,7 +690,7 @@ async def test_inner_get_streaming_response(mock_anthropic_client: MagicMock) -> mock_anthropic_client.beta.messages.create.return_value = mock_stream() - messages = [ChatMessage(role=Role.USER, text="Hi")] + messages = [ChatMessage("user", ["Hi"])] chat_options = ChatOptions(max_tokens=10) chunks: list[ChatResponseUpdate] = [] @@ -723,13 +721,13 @@ async def test_anthropic_client_integration_basic_chat() -> None: """Integration test for basic chat completion.""" client = AnthropicClient() - messages = [ChatMessage(role=Role.USER, text="Say 'Hello, World!' and nothing else.")] + messages = [ChatMessage("user", ["Say 'Hello, World!' and nothing else."])] response = await client.get_response(messages=messages, options={"max_tokens": 50}) assert response is not None assert len(response.messages) > 0 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert len(response.messages[0].text) > 0 assert response.usage_details is not None @@ -740,7 +738,7 @@ async def test_anthropic_client_integration_streaming_chat() -> None: """Integration test for streaming chat completion.""" client = AnthropicClient() - messages = [ChatMessage(role=Role.USER, text="Count from 1 to 5.")] + messages = [ChatMessage("user", ["Count from 1 to 5."])] chunks = [] async for chunk in client.get_streaming_response(messages=messages, options={"max_tokens": 50}): @@ -756,7 +754,7 @@ async def test_anthropic_client_integration_function_calling() -> None: """Integration test for function calling.""" client = AnthropicClient() - messages = [ChatMessage(role=Role.USER, text="What's the weather in San Francisco?")] + messages = [ChatMessage("user", ["What's the weather in San Francisco?"])] tools = [get_weather] response = await client.get_response( @@ -776,7 +774,7 @@ async def test_anthropic_client_integration_hosted_tools() -> None: """Integration test for hosted tools.""" client = AnthropicClient() - messages = [ChatMessage(role=Role.USER, text="What tools do you have available?")] + messages = [ChatMessage("user", ["What tools do you have available?"])] tools = [ HostedWebSearchTool(), HostedCodeInterpreterTool(), @@ -803,8 +801,8 @@ async def test_anthropic_client_integration_with_system_message() -> None: client = AnthropicClient() messages = [ - ChatMessage(role=Role.SYSTEM, text="You are a pirate. Always respond like a pirate."), - ChatMessage(role=Role.USER, text="Hello!"), + ChatMessage("system", ["You are a pirate. Always respond like a pirate."]), + ChatMessage("user", ["Hello!"]), ] response = await client.get_response(messages=messages, options={"max_tokens": 50}) @@ -819,7 +817,7 @@ async def test_anthropic_client_integration_temperature_control() -> None: """Integration test with temperature control.""" client = AnthropicClient() - messages = [ChatMessage(role=Role.USER, text="Say hello.")] + messages = [ChatMessage("user", ["Say hello."])] response = await client.get_response( messages=messages, @@ -837,11 +835,11 @@ async def test_anthropic_client_integration_ordering() -> None: client = AnthropicClient() messages = [ - ChatMessage(role=Role.USER, text="Say hello."), - ChatMessage(role=Role.USER, text="Then say goodbye."), - ChatMessage(role=Role.ASSISTANT, text="Thank you for chatting!"), - ChatMessage(role=Role.ASSISTANT, text="Let me know if I can help."), - ChatMessage(role=Role.USER, text="Just testing things."), + ChatMessage("user", ["Say hello."]), + ChatMessage("user", ["Then say goodbye."]), + ChatMessage("assistant", ["Thank you for chatting!"]), + ChatMessage("assistant", ["Let me know if I can help."]), + ChatMessage("user", ["Just testing things."]), ] response = await client.get_response(messages=messages) @@ -863,7 +861,7 @@ async def test_anthropic_client_integration_images() -> None: messages = [ ChatMessage( - role=Role.USER, + role="user", contents=[ Content.from_text(text="Describe this image"), Content.from_data(media_type="image/jpeg", data=image_bytes), diff --git a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py index ac81a3c50b..e11d3e8793 100644 --- a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py +++ b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py @@ -5,7 +5,7 @@ import sys from collections.abc import Awaitable, Callable, MutableSequence from typing import TYPE_CHECKING, Any, ClassVar, Literal -from agent_framework import AGENT_FRAMEWORK_USER_AGENT, ChatMessage, Context, ContextProvider, Role +from agent_framework import AGENT_FRAMEWORK_USER_AGENT, ChatMessage, Context, ContextProvider from agent_framework._logging import get_logger from agent_framework._pydantic import AFBaseSettings from agent_framework.exceptions import ServiceInitializationError @@ -525,9 +525,7 @@ class AzureAISearchContextProvider(ContextProvider): messages_list = [messages] if isinstance(messages, ChatMessage) else list(messages) filtered_messages = [ - msg - for msg in messages_list - if msg and msg.text and msg.text.strip() and msg.role in [Role.USER, Role.ASSISTANT] + msg for msg in messages_list if msg and msg.text and msg.text.strip() and msg.role in ["user", "assistant"] ] if not filtered_messages: @@ -548,8 +546,8 @@ class AzureAISearchContextProvider(ContextProvider): return Context() # Create context messages: first message with prompt, then one message per result part - context_messages = [ChatMessage(role=Role.USER, text=self.context_prompt)] - context_messages.extend([ChatMessage(role=Role.USER, text=part) for part in search_result_parts]) + context_messages = [ChatMessage("user", [self.context_prompt])] + context_messages.extend([ChatMessage("user", [part]) for part in search_result_parts]) return Context(messages=context_messages) @@ -921,7 +919,7 @@ class AzureAISearchContextProvider(ContextProvider): # Medium/low reasoning uses messages with conversation history kb_messages = [ KnowledgeBaseMessage( - role=msg.role.value if hasattr(msg.role, "value") else str(msg.role), + role=msg.role if hasattr(msg.role, "value") else str(msg.role), content=[KnowledgeBaseMessageTextContent(text=msg.text)], ) for msg in messages diff --git a/python/packages/azure-ai-search/tests/test_search_provider.py b/python/packages/azure-ai-search/tests/test_search_provider.py index 66ead79a6b..d348f3ef79 100644 --- a/python/packages/azure-ai-search/tests/test_search_provider.py +++ b/python/packages/azure-ai-search/tests/test_search_provider.py @@ -5,7 +5,7 @@ import os from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import ChatMessage, Context, Role +from agent_framework import ChatMessage, Context from agent_framework.azure import AzureAISearchContextProvider, AzureAISearchSettings from agent_framework.exceptions import ServiceInitializationError from azure.core.credentials import AzureKeyCredential @@ -39,7 +39,7 @@ def mock_index_client() -> AsyncMock: def sample_messages() -> list[ChatMessage]: """Create sample chat messages for testing.""" return [ - ChatMessage(role=Role.USER, text="What is in the documents?"), + ChatMessage("user", ["What is in the documents?"]), ] @@ -318,7 +318,7 @@ class TestSemanticSearch: ) # Empty message - context = await provider.invoking([ChatMessage(role=Role.USER, text="")]) + context = await provider.invoking([ChatMessage("user", [""])]) assert isinstance(context, Context) assert len(context.messages) == 0 @@ -520,10 +520,10 @@ class TestMessageFiltering: # Mix of message types messages = [ - ChatMessage(role=Role.SYSTEM, text="System message"), - ChatMessage(role=Role.USER, text="User message"), - ChatMessage(role=Role.ASSISTANT, text="Assistant message"), - ChatMessage(role=Role.TOOL, text="Tool message"), + ChatMessage("system", ["System message"]), + ChatMessage("user", ["User message"]), + ChatMessage("assistant", ["Assistant message"]), + ChatMessage("tool", ["Tool message"]), ] context = await provider.invoking(messages) @@ -548,9 +548,9 @@ class TestMessageFiltering: # Messages with empty/whitespace text messages = [ - ChatMessage(role=Role.USER, text=""), - ChatMessage(role=Role.USER, text=" "), - ChatMessage(role=Role.USER, text=None), + ChatMessage("user", [""]), + ChatMessage("user", [" "]), + ChatMessage("user", [None]), ] context = await provider.invoking(messages) @@ -581,7 +581,7 @@ class TestCitations: mode="semantic", ) - context = await provider.invoking([ChatMessage(role=Role.USER, text="test query")]) + context = await provider.invoking([ChatMessage("user", ["test query"])]) # Check that citation is included assert isinstance(context, Context) 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 540aacbca2..e2c1c79bdb 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 @@ -26,7 +26,6 @@ from agent_framework import ( HostedMCPTool, HostedWebSearchTool, Middleware, - Role, TextSpanRegion, ToolProtocol, UsageDetails, @@ -353,7 +352,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA options: dict[str, Any], **kwargs: Any, ) -> ChatResponse: - return await ChatResponse.from_chat_response_generator( + return await ChatResponse.from_update_generator( updates=self._inner_get_streaming_response(messages=messages, options=options, **kwargs), output_format_type=options.get("response_format"), ) @@ -638,7 +637,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA match event_data: case MessageDeltaChunk(): # only one event_type: AgentStreamEvent.THREAD_MESSAGE_DELTA - role = Role.USER if event_data.delta.role == MessageRole.USER else Role.ASSISTANT + role = "user" if event_data.delta.role == "user" else "assistant" # Extract URL citations from the delta chunk url_citations = self._extract_url_citations(event_data, azure_search_tool_calls) @@ -688,7 +687,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA ) if function_call_contents: yield ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=function_call_contents, conversation_id=thread_id, message_id=response_id, @@ -704,7 +703,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA message_id=response_id, raw_representation=event_data, response_id=response_id, - role=Role.ASSISTANT, + role="assistant", model_id=event_data.model, ) @@ -733,7 +732,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA ) ) yield ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=[usage_content], conversation_id=thread_id, message_id=response_id, @@ -747,7 +746,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA message_id=response_id, raw_representation=event_data, response_id=response_id, - role=Role.ASSISTANT, + role="assistant", ) case RunStepDeltaChunk(): # type: ignore if ( @@ -776,7 +775,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA Content.from_hosted_file(file_id=output.image.file_id) ) yield ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=code_contents, conversation_id=thread_id, message_id=response_id, @@ -795,7 +794,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA message_id=response_id, raw_representation=event_data, # type: ignore response_id=response_id, - role=Role.ASSISTANT, + role="assistant", ) except Exception as ex: logger.error(f"Error processing stream: {ex}") @@ -1077,7 +1076,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA additional_messages: list[ThreadMessageOptions] | None = None for chat_message in messages: - if chat_message.role.value in ["system", "developer"]: + if chat_message.role in ["system", "developer"]: for text_content in [content for content in chat_message.contents if content.type == "text"]: instructions.append(text_content.text) # type: ignore[arg-type] continue @@ -1107,7 +1106,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA additional_messages = [] additional_messages.append( ThreadMessageOptions( - role=MessageRole.AGENT if chat_message.role == Role.ASSISTANT else MessageRole.USER, + role=MessageRole.AGENT if chat_message.role == "assistant" else MessageRole.USER, content=message_contents, ) ) 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 202002a45f..15bcd7cfc9 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -482,7 +482,7 @@ class AzureAIClient(OpenAIBaseResponsesClient[TAzureAIClientOptions], Generic[TA # System/developer messages are turned into instructions, since there is no such message roles in Azure AI. for message in messages: - if message.role.value in ["system", "developer"]: + if message.role in ["system", "developer"]: for text_content in [content for content in message.contents if content.type == "text"]: instructions_list.append(text_content.text) # type: ignore[arg-type] else: 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 4366ea8141..76c1c75252 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 @@ -22,7 +22,6 @@ from agent_framework import ( HostedFileSearchTool, HostedMCPTool, HostedWebSearchTool, - Role, tool, ) from agent_framework._serialization import SerializationMixin @@ -309,7 +308,7 @@ async def test_azure_ai_chat_client_thread_management_through_public_api(mock_ag mock_stream.__aenter__ = AsyncMock(return_value=empty_async_iter()) mock_stream.__aexit__ = AsyncMock(return_value=None) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] # Call without existing thread - should create new one response = chat_client.get_streaming_response(messages) @@ -336,7 +335,7 @@ async def test_azure_ai_chat_client_prepare_options_basic(mock_agents_client: Ma """Test _prepare_options with basic ChatOptions.""" chat_client = create_test_azure_ai_chat_client(mock_agents_client) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options: ChatOptions = {"max_tokens": 100, "temperature": 0.7} run_options, tool_results = await chat_client._prepare_options(messages, chat_options) # type: ignore @@ -349,7 +348,7 @@ async def test_azure_ai_chat_client_prepare_options_no_chat_options(mock_agents_ """Test _prepare_options with default ChatOptions.""" chat_client = create_test_azure_ai_chat_client(mock_agents_client) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] run_options, tool_results = await chat_client._prepare_options(messages, {}) # type: ignore @@ -366,7 +365,7 @@ async def test_azure_ai_chat_client_prepare_options_with_image_content(mock_agen mock_agents_client.get_agent = AsyncMock(return_value=None) image_content = Content.from_uri(uri="https://example.com/image.jpg", media_type="image/jpeg") - messages = [ChatMessage(role=Role.USER, contents=[image_content])] + messages = [ChatMessage("user", [image_content])] run_options, _ = await chat_client._prepare_options(messages, {}) # type: ignore @@ -455,8 +454,8 @@ async def test_azure_ai_chat_client_prepare_options_with_messages(mock_agents_cl # Test with system message (becomes instruction) messages = [ - ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant"), - ChatMessage(role=Role.USER, text="Hello"), + ChatMessage("system", ["You are a helpful assistant"]), + ChatMessage("user", ["Hello"]), ] run_options, _ = await chat_client._prepare_options(messages, {}) # type: ignore @@ -478,7 +477,7 @@ async def test_azure_ai_chat_client_prepare_options_with_instructions_from_optio chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") mock_agents_client.get_agent = AsyncMock(return_value=None) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options: ChatOptions = { "instructions": "You are a thoughtful reviewer. Give brief feedback.", } @@ -501,8 +500,8 @@ async def test_azure_ai_chat_client_prepare_options_merges_instructions_from_mes mock_agents_client.get_agent = AsyncMock(return_value=None) messages = [ - ChatMessage(role=Role.SYSTEM, text="Context: You are reviewing marketing copy."), - ChatMessage(role=Role.USER, text="Review this tagline"), + ChatMessage("system", ["Context: You are reviewing marketing copy."]), + ChatMessage("user", ["Review this tagline"]), ] chat_options: ChatOptions = { "instructions": "Be concise and constructive in your feedback.", @@ -520,17 +519,17 @@ async def test_azure_ai_chat_client_prepare_options_merges_instructions_from_mes async def test_azure_ai_chat_client_inner_get_response(mock_agents_client: MagicMock) -> None: """Test _inner_get_response method.""" chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options: ChatOptions = {} async def mock_streaming_response(): - yield ChatResponseUpdate(role=Role.ASSISTANT, text="Hello back") + yield ChatResponseUpdate(role="assistant", text="Hello back") with ( patch.object(chat_client, "_inner_get_streaming_response", return_value=mock_streaming_response()), - patch("agent_framework.ChatResponse.from_chat_response_generator") as mock_from_generator, + patch("agent_framework.ChatResponse.from_update_generator") as mock_from_generator, ): - mock_response = ChatResponse(role=Role.ASSISTANT, text="Hello back") + mock_response = ChatResponse(messages=ChatMessage("assistant", ["Hello back"])) mock_from_generator.return_value = mock_response result = await chat_client._inner_get_response(messages=messages, options=chat_options) # type: ignore @@ -673,7 +672,7 @@ async def test_azure_ai_chat_client_prepare_options_tool_choice_required_specifi dict_tool = {"type": "function", "function": {"name": "test_function"}} chat_options = {"tools": [dict_tool], "tool_choice": required_tool_mode} - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] run_options, _ = await chat_client._prepare_options(messages, chat_options) # type: ignore @@ -718,7 +717,7 @@ async def test_azure_ai_chat_client_prepare_options_mcp_never_require(mock_agent mcp_tool = HostedMCPTool(name="Test MCP Tool", url="https://example.com/mcp", approval_mode="never_require") - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options: ChatOptions = {"tools": [mcp_tool], "tool_choice": "auto"} with patch("agent_framework_azure_ai._shared.McpTool") as mock_mcp_tool_class: @@ -750,7 +749,7 @@ async def test_azure_ai_chat_client_prepare_options_mcp_with_headers(mock_agents name="Test MCP Tool", url="https://example.com/mcp", headers=headers, approval_mode="never_require" ) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] chat_options: ChatOptions = {"tools": [mcp_tool], "tool_choice": "auto"} with patch("agent_framework_azure_ai._shared.McpTool") as mock_mcp_tool_class: @@ -1409,7 +1408,7 @@ async def test_azure_ai_chat_client_get_response() -> None: "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(ChatMessage("user", ["What's the weather like today?"])) # Test that the agents_client can be used to get a response response = await azure_ai_chat_client.get_response(messages=messages) @@ -1427,7 +1426,7 @@ async def test_azure_ai_chat_client_get_response_tools() -> None: assert isinstance(azure_ai_chat_client, ChatClientProtocol) messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages.append(ChatMessage("user", ["What's the weather like in Seattle?"])) # Test that the agents_client can be used to get a response response = await azure_ai_chat_client.get_response( @@ -1455,7 +1454,7 @@ async def test_azure_ai_chat_client_streaming() -> None: "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(ChatMessage("user", ["What's the weather like today?"])) # Test that the agents_client can be used to get a response response = azure_ai_chat_client.get_streaming_response(messages=messages) @@ -1479,7 +1478,7 @@ async def test_azure_ai_chat_client_streaming_tools() -> None: assert isinstance(azure_ai_chat_client, ChatClientProtocol) messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages.append(ChatMessage("user", ["What's the weather like in Seattle?"])) # Test that the agents_client can be used to get a response response = azure_ai_chat_client.get_streaming_response( @@ -2098,7 +2097,7 @@ def test_azure_ai_chat_client_prepare_messages_with_function_result( chat_client = create_test_azure_ai_chat_client(mock_agents_client) function_result = Content.from_function_result(call_id='["run_123", "call_456"]', result="test result") - messages = [ChatMessage(role=Role.USER, contents=[function_result])] + messages = [ChatMessage("user", [function_result])] additional_messages, instructions, required_action_results = chat_client._prepare_messages(messages) # type: ignore @@ -2118,7 +2117,7 @@ def test_azure_ai_chat_client_prepare_messages_with_raw_content_block( # Create content with raw_representation that is a MessageInputContentBlock raw_block = MessageInputTextBlock(text="Raw block text") custom_content = Content(type="custom", raw_representation=raw_block) - messages = [ChatMessage(role=Role.USER, contents=[custom_content])] + messages = [ChatMessage("user", [custom_content])] additional_messages, instructions, required_action_results = chat_client._prepare_messages(messages) # type: ignore 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 694bcb6604..8563d78cbf 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -22,7 +22,6 @@ from agent_framework import ( HostedFileSearchTool, HostedMCPTool, HostedWebSearchTool, - Role, tool, ) from agent_framework.exceptions import ServiceInitializationError @@ -299,16 +298,16 @@ async def test_prepare_messages_for_azure_ai_with_system_messages( client = create_test_azure_ai_client(mock_project_client) messages = [ - ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="You are a helpful assistant.")]), - ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="System response")]), + ChatMessage("system", [Content.from_text(text="You are a helpful assistant.")]), + ChatMessage("user", [Content.from_text(text="Hello")]), + ChatMessage("assistant", [Content.from_text(text="System response")]), ] result_messages, instructions = client._prepare_messages_for_azure_ai(messages) # type: ignore assert len(result_messages) == 2 - assert result_messages[0].role == Role.USER - assert result_messages[1].role == Role.ASSISTANT + assert result_messages[0].role == "user" + assert result_messages[1].role == "assistant" assert instructions == "You are a helpful assistant." @@ -319,8 +318,8 @@ async def test_prepare_messages_for_azure_ai_no_system_messages( client = create_test_azure_ai_client(mock_project_client) messages = [ - ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Hi there!")]), + ChatMessage("user", [Content.from_text(text="Hello")]), + ChatMessage("assistant", [Content.from_text(text="Hi there!")]), ] result_messages, instructions = client._prepare_messages_for_azure_ai(messages) # type: ignore @@ -420,7 +419,7 @@ async def test_prepare_options_basic(mock_project_client: MagicMock) -> None: """Test prepare_options basic functionality.""" client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0") - messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])] + messages = [ChatMessage("user", [Content.from_text(text="Hello")])] with ( patch.object(client.__class__.__bases__[0], "_prepare_options", return_value={"model": "test-model"}), @@ -454,7 +453,7 @@ async def test_prepare_options_with_application_endpoint( agent_version="1", ) - messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])] + messages = [ChatMessage("user", [Content.from_text(text="Hello")])] with ( patch.object(client.__class__.__bases__[0], "_prepare_options", return_value={"model": "test-model"}), @@ -493,7 +492,7 @@ async def test_prepare_options_with_application_project_client( agent_version="1", ) - messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])] + messages = [ChatMessage("user", [Content.from_text(text="Hello")])] with ( patch.object(client.__class__.__bases__[0], "_prepare_options", return_value={"model": "test-model"}), @@ -969,7 +968,7 @@ async def test_prepare_options_excludes_response_format( """Test that prepare_options excludes response_format, text, and text_format from final run options.""" client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0") - messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])] + messages = [ChatMessage("user", [Content.from_text(text="Hello")])] chat_options: ChatOptions = {} with ( @@ -1355,10 +1354,10 @@ async def test_integration_options( # Prepare test message if option_name.startswith("tool_choice"): # Use weather-related prompt for tool tests - messages = [ChatMessage(role="user", text="What is the weather in Seattle?")] + messages = [ChatMessage("user", ["What is the weather in Seattle?"])] else: # Generic prompt for simple options - messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")] + messages = [ChatMessage("user", ["Say 'Hello World' briefly."])] # Build options dict options: dict[str, Any] = {option_name: option_value, "tools": [get_weather]} @@ -1372,7 +1371,7 @@ async def test_integration_options( ) output_format = option_value if option_name == "response_format" else None - response = await ChatResponse.from_chat_response_generator(response_gen, output_format_type=output_format) + response = await ChatResponse.from_update_generator(response_gen, output_format_type=output_format) else: # Test non-streaming mode response = await client.get_response( @@ -1458,11 +1457,11 @@ async def test_integration_agent_options( # Prepare test message if option_name.startswith("response_format"): # Use prompt that works well with structured output - messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")] - messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) + messages = [ChatMessage("user", ["The weather in Seattle is sunny"])] + messages.append(ChatMessage("user", ["What is the weather in Seattle?"])) else: # Generic prompt for simple options - messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")] + messages = [ChatMessage("user", ["Say 'Hello World' briefly."])] # Build options dict options = {option_name: option_value} @@ -1475,9 +1474,7 @@ async def test_integration_agent_options( ) output_format = option_value if option_name.startswith("response_format") else None - response = await ChatResponse.from_chat_response_generator( - response_gen, output_format_type=output_format - ) + response = await ChatResponse.from_update_generator(response_gen, output_format_type=output_format) else: # Test non-streaming mode response = await client.get_response( @@ -1519,7 +1516,7 @@ async def test_integration_web_search() -> None: }, } if streaming: - response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content)) + response = await ChatResponse.from_update_generator(client.get_streaming_response(**content)) else: response = await client.get_response(**content) @@ -1544,7 +1541,7 @@ async def test_integration_web_search() -> None: }, } if streaming: - response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content)) + response = await ChatResponse.from_update_generator(client.get_streaming_response(**content)) else: response = await client.get_response(**content) assert response.text is not None diff --git a/python/packages/azurefunctions/tests/test_app.py b/python/packages/azurefunctions/tests/test_app.py index f8b414fc34..d33ca1f99c 100644 --- a/python/packages/azurefunctions/tests/test_app.py +++ b/python/packages/azurefunctions/tests/test_app.py @@ -355,9 +355,7 @@ class TestAgentEntityOperations: async def test_entity_run_agent_operation(self) -> None: """Test that entity can run agent operation.""" mock_agent = Mock() - mock_agent.run = AsyncMock( - return_value=AgentResponse(messages=[ChatMessage(role="assistant", text="Test response")]) - ) + mock_agent.run = AsyncMock(return_value=AgentResponse(messages=[ChatMessage("assistant", ["Test response"])])) entity = AgentEntity(mock_agent, state_provider=_InMemoryStateProvider(thread_id="test-conv-123")) @@ -373,9 +371,7 @@ class TestAgentEntityOperations: async def test_entity_stores_conversation_history(self) -> None: """Test that the entity stores conversation history.""" mock_agent = Mock() - mock_agent.run = AsyncMock( - return_value=AgentResponse(messages=[ChatMessage(role="assistant", text="Response 1")]) - ) + mock_agent.run = AsyncMock(return_value=AgentResponse(messages=[ChatMessage("assistant", ["Response 1"])])) entity = AgentEntity(mock_agent, state_provider=_InMemoryStateProvider(thread_id="conv-1")) @@ -407,9 +403,7 @@ class TestAgentEntityOperations: async def test_entity_increments_message_count(self) -> None: """Test that the entity increments the message count.""" mock_agent = Mock() - mock_agent.run = AsyncMock( - return_value=AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) - ) + mock_agent.run = AsyncMock(return_value=AgentResponse(messages=[ChatMessage("assistant", ["Response"])])) entity = AgentEntity(mock_agent, state_provider=_InMemoryStateProvider(thread_id="conv-1")) @@ -448,9 +442,7 @@ class TestAgentEntityFactory: def test_entity_function_handles_run_operation(self) -> None: """Test that the entity function handles the run operation.""" mock_agent = Mock() - mock_agent.run = AsyncMock( - return_value=AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) - ) + mock_agent.run = AsyncMock(return_value=AgentResponse(messages=[ChatMessage("assistant", ["Response"])])) entity_function = create_agent_entity(mock_agent) @@ -475,9 +467,7 @@ class TestAgentEntityFactory: def test_entity_function_handles_run_agent_operation(self) -> None: """Test that the entity function handles the deprecated run_agent operation for backward compatibility.""" mock_agent = Mock() - mock_agent.run = AsyncMock( - return_value=AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) - ) + mock_agent.run = AsyncMock(return_value=AgentResponse(messages=[ChatMessage("assistant", ["Response"])])) entity_function = create_agent_entity(mock_agent) diff --git a/python/packages/azurefunctions/tests/test_entities.py b/python/packages/azurefunctions/tests/test_entities.py index 555b588887..909dedd6f8 100644 --- a/python/packages/azurefunctions/tests/test_entities.py +++ b/python/packages/azurefunctions/tests/test_entities.py @@ -10,7 +10,7 @@ from typing import Any, TypeVar from unittest.mock import AsyncMock, Mock import pytest -from agent_framework import AgentResponse, ChatMessage, Role +from agent_framework import AgentResponse, ChatMessage from agent_framework_azurefunctions._entities import create_agent_entity @@ -19,11 +19,7 @@ TFunc = TypeVar("TFunc", bound=Callable[..., Any]) def _agent_response(text: str | None) -> AgentResponse: """Create an AgentResponse with a single assistant message.""" - message = ( - ChatMessage(role=Role.ASSISTANT, text=text) - if text is not None - else ChatMessage(role=Role.ASSISTANT, contents=[]) - ) + message = ChatMessage("assistant", [text]) if text is not None else ChatMessage("assistant", []) return AgentResponse(messages=[message]) diff --git a/python/packages/azurefunctions/tests/test_orchestration.py b/python/packages/azurefunctions/tests/test_orchestration.py index 2b9a4126d4..1f8a029dba 100644 --- a/python/packages/azurefunctions/tests/test_orchestration.py +++ b/python/packages/azurefunctions/tests/test_orchestration.py @@ -6,7 +6,7 @@ from typing import Any from unittest.mock import Mock import pytest -from agent_framework import AgentResponse, ChatMessage, Role +from agent_framework import AgentResponse, ChatMessage from agent_framework_durabletask import DurableAIAgent from azure.durable_functions.models.Task import TaskBase, TaskState @@ -136,7 +136,7 @@ class TestAgentResponseHelpers: # Simulate successful entity task completion entity_task.state = TaskState.SUCCEEDED - entity_task.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Test response")]).to_dict() + entity_task.result = AgentResponse(messages=[ChatMessage("assistant", ["Test response"])]).to_dict() # Clear pending_tasks to simulate that parent has processed the child task.pending_tasks.clear() @@ -178,7 +178,7 @@ class TestAgentResponseHelpers: # Simulate successful entity task with JSON response entity_task.state = TaskState.SUCCEEDED - entity_task.result = AgentResponse(messages=[ChatMessage(role="assistant", text='{"answer": "42"}')]).to_dict() + entity_task.result = AgentResponse(messages=[ChatMessage("assistant", ['{"answer": "42"}'])]).to_dict() # Clear pending_tasks to simulate that parent has processed the child task.pending_tasks.clear() @@ -254,7 +254,7 @@ class TestAzureFunctionsFireAndForget: response = result.result assert isinstance(response, AgentResponse) assert len(response.messages) == 1 - assert response.messages[0].role == Role.SYSTEM + assert response.messages[0].role == "system" # Check message contains key information message_text = response.messages[0].text assert "accepted" in message_text.lower() diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index d7e0754c2b..bc67bc7908 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -16,9 +16,7 @@ from agent_framework import ( ChatResponse, ChatResponseUpdate, Content, - FinishReason, FunctionTool, - Role, ToolProtocol, UsageDetails, get_logger, @@ -185,20 +183,20 @@ TBedrockChatOptions = TypeVar("TBedrockChatOptions", bound=TypedDict, default="B # endregion -ROLE_MAP: dict[Role, str] = { - Role.USER: "user", - Role.ASSISTANT: "assistant", - Role.SYSTEM: "user", - Role.TOOL: "user", +ROLE_MAP: dict[str, str] = { + "user": "user", + "assistant": "assistant", + "system": "user", + "tool": "user", } -FINISH_REASON_MAP: dict[str, FinishReason] = { - "end_turn": FinishReason.STOP, - "stop_sequence": FinishReason.STOP, - "max_tokens": FinishReason.LENGTH, - "length": FinishReason.LENGTH, - "content_filtered": FinishReason.CONTENT_FILTER, - "tool_use": FinishReason.TOOL_CALLS, +FINISH_REASON_MAP: dict[str, str] = { + "end_turn": "stop", + "stop_sequence": "stop", + "max_tokens": "length", + "length": "length", + "content_filtered": "content_filter", + "tool_use": "tool_calls", } @@ -397,7 +395,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha conversation: list[dict[str, Any]] = [] pending_tool_use_ids: deque[str] = deque() for message in messages: - if message.role == Role.SYSTEM: + if message.role == "system": text_value = message.text if text_value: prompts.append({"text": text_value}) @@ -414,7 +412,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha for block in content_blocks if isinstance(block, MutableMapping) and "toolUse" in block ) - elif message.role == Role.TOOL: + elif message.role == "tool": content_blocks = self._align_tool_results_with_pending(content_blocks, pending_tool_use_ids) pending_tool_use_ids.clear() if not content_blocks: @@ -574,7 +572,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha message = output.get("message", {}) content_blocks = message.get("content", []) or [] contents = self._parse_message_contents(content_blocks) - chat_message = ChatMessage(role=Role.ASSISTANT, contents=contents, raw_representation=message) + chat_message = ChatMessage("assistant", contents, raw_representation=message) usage_details = self._parse_usage(response.get("usage") or output.get("usage")) finish_reason = self._map_finish_reason(output.get("completionReason") or response.get("stopReason")) response_id = response.get("responseId") or message.get("id") @@ -642,7 +640,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha logger.debug("Ignoring unsupported Bedrock content block: %s", block) return contents - def _map_finish_reason(self, reason: str | None) -> FinishReason | None: + def _map_finish_reason(self, reason: str | None) -> str | None: if not reason: return None return FINISH_REASON_MAP.get(reason.lower()) diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py index 704eb2138a..7addad3b73 100644 --- a/python/packages/bedrock/tests/test_bedrock_client.py +++ b/python/packages/bedrock/tests/test_bedrock_client.py @@ -6,7 +6,7 @@ import asyncio from typing import Any import pytest -from agent_framework import ChatMessage, Content, Role +from agent_framework import ChatMessage, Content from agent_framework.exceptions import ServiceInitializationError from agent_framework_bedrock import BedrockChatClient @@ -42,8 +42,8 @@ def test_get_response_invokes_bedrock_runtime() -> None: ) messages = [ - ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="You are concise.")]), - ChatMessage(role=Role.USER, contents=[Content.from_text(text="hello")]), + ChatMessage("system", [Content.from_text(text="You are concise.")]), + ChatMessage("user", [Content.from_text(text="hello")]), ] response = asyncio.run(client.get_response(messages=messages, options={"max_tokens": 32})) @@ -63,7 +63,7 @@ def test_build_request_requires_non_system_messages() -> None: client=_StubBedrockRuntime(), ) - messages = [ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="Only system text")])] + messages = [ChatMessage("system", [Content.from_text(text="Only system text")])] with pytest.raises(ServiceInitializationError): client._prepare_options(messages, {}) diff --git a/python/packages/bedrock/tests/test_bedrock_settings.py b/python/packages/bedrock/tests/test_bedrock_settings.py index d98cf00817..124892e51d 100644 --- a/python/packages/bedrock/tests/test_bedrock_settings.py +++ b/python/packages/bedrock/tests/test_bedrock_settings.py @@ -10,7 +10,6 @@ from agent_framework import ( ChatOptions, Content, FunctionTool, - Role, ) from pydantic import BaseModel @@ -47,7 +46,7 @@ def test_build_request_includes_tool_config() -> None: "tools": [tool], "tool_choice": {"mode": "required", "required_function_name": "get_weather"}, } - messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="hi")])] + messages = [ChatMessage("user", [Content.from_text(text="hi")])] request = client._prepare_options(messages, options) @@ -59,15 +58,15 @@ def test_build_request_serializes_tool_history() -> None: client = _build_client() options: ChatOptions = {} messages = [ - ChatMessage(role=Role.USER, contents=[Content.from_text(text="how's weather?")]), + ChatMessage("user", [Content.from_text(text="how's weather?")]), ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call(call_id="call-1", name="get_weather", arguments='{"location": "SEA"}') ], ), ChatMessage( - role=Role.TOOL, + role="tool", contents=[Content.from_function_result(call_id="call-1", result={"answer": "72F"})], ), ] diff --git a/python/packages/chatkit/agent_framework_chatkit/_converter.py b/python/packages/chatkit/agent_framework_chatkit/_converter.py index 894d54831d..457cfc5e1e 100644 --- a/python/packages/chatkit/agent_framework_chatkit/_converter.py +++ b/python/packages/chatkit/agent_framework_chatkit/_converter.py @@ -9,7 +9,6 @@ from collections.abc import Awaitable, Callable, Sequence from agent_framework import ( ChatMessage, Content, - Role, ) from chatkit.types import ( AssistantMessageItem, @@ -101,21 +100,21 @@ class ThreadItemConverter: # If only text and no attachments, use text parameter for simplicity if text_content.strip() and not data_contents: - user_message = ChatMessage(role=Role.USER, text=text_content.strip()) + user_message = ChatMessage("user", [text_content.strip()]) else: # Build contents list with both text and attachments contents: list[Content] = [] if text_content.strip(): contents.append(Content.from_text(text=text_content.strip())) contents.extend(data_contents) - user_message = ChatMessage(role=Role.USER, contents=contents) + user_message = ChatMessage("user", contents) # Handle quoted text if this is the last message messages = [user_message] if item.quoted_text and is_last_message: quoted_context = ChatMessage( - role=Role.USER, - text=f"The user is referring to this in particular:\n{item.quoted_text}", + "user", + [f"The user is referring to this in particular:\n{item.quoted_text}"], ) # Prepend quoted context before the main message messages.insert(0, quoted_context) @@ -214,7 +213,7 @@ class ThreadItemConverter: message = converter.hidden_context_to_input(hidden_item) # Returns: ChatMessage(role=SYSTEM, text="User's email: ...") """ - return ChatMessage(role=Role.SYSTEM, text=f"{item.content}") + return ChatMessage("system", [f"{item.content}"]) def tag_to_message_content(self, tag: UserMessageTagContent) -> Content: """Convert a ChatKit tag (@-mention) to Agent Framework content. @@ -293,7 +292,7 @@ class ThreadItemConverter: f"A message was displayed to the user that the following task was performed:\n\n{task_text}\n" ) - return ChatMessage(role=Role.USER, text=text) + return ChatMessage("user", [text]) def workflow_to_input(self, item: WorkflowItem) -> ChatMessage | list[ChatMessage] | None: """Convert a ChatKit WorkflowItem to Agent Framework ChatMessage(s). @@ -348,7 +347,7 @@ class ThreadItemConverter: f"\n{task_text}\n" ) - messages.append(ChatMessage(role=Role.USER, text=text)) + messages.append(ChatMessage("user", [text])) return messages if messages else None @@ -390,7 +389,7 @@ class ThreadItemConverter: try: widget_json = item.widget.model_dump_json(exclude_unset=True, exclude_none=True) text = f"The following graphical UI widget (id: {item.id}) was displayed to the user:{widget_json}" - return ChatMessage(role=Role.USER, text=text) + return ChatMessage("user", [text]) except Exception: # If JSON serialization fails, skip the widget return None @@ -416,7 +415,7 @@ class ThreadItemConverter: if not text_parts: return None - return ChatMessage(role=Role.ASSISTANT, text="".join(text_parts)) + return ChatMessage("assistant", ["".join(text_parts)]) async def client_tool_call_to_input(self, item: ClientToolCallItem) -> ChatMessage | list[ChatMessage] | None: """Convert a ChatKit ClientToolCallItem to Agent Framework ChatMessage(s). @@ -442,7 +441,7 @@ class ThreadItemConverter: # Create function call message function_call_msg = ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id=item.call_id, @@ -454,7 +453,7 @@ class ThreadItemConverter: # Create function result message function_result_msg = ChatMessage( - role=Role.TOOL, + role="tool", contents=[ Content.from_function_result( call_id=item.call_id, diff --git a/python/packages/chatkit/tests/test_converter.py b/python/packages/chatkit/tests/test_converter.py index b75139bf58..71400527aa 100644 --- a/python/packages/chatkit/tests/test_converter.py +++ b/python/packages/chatkit/tests/test_converter.py @@ -5,7 +5,7 @@ from unittest.mock import Mock import pytest -from agent_framework import ChatMessage, Role +from agent_framework import ChatMessage from chatkit.types import UserMessageTextContent from agent_framework_chatkit import ThreadItemConverter, simple_to_agent_input @@ -44,7 +44,7 @@ class TestThreadItemConverter: assert len(result) == 1 assert isinstance(result[0], ChatMessage) - assert result[0].role == Role.USER + assert result[0].role == "user" assert result[0].text == "Hello, how can you help me?" async def test_to_agent_input_empty_text(self, converter): @@ -117,7 +117,7 @@ class TestThreadItemConverter: result = converter.hidden_context_to_input(hidden_item) assert isinstance(result, ChatMessage) - assert result.role == Role.SYSTEM + assert result.role == "system" assert result.text == "This is hidden context information" def test_tag_to_message_content(self, converter): @@ -234,7 +234,7 @@ class TestThreadItemConverter: assert len(result) == 1 message = result[0] - assert message.role == Role.USER + assert message.role == "user" assert len(message.contents) == 2 # First content should be text @@ -303,7 +303,7 @@ class TestThreadItemConverter: result = converter.task_to_input(task_item) assert isinstance(result, ChatMessage) - assert result.role == Role.USER + assert result.role == "user" assert "Analysis: Analyzed the data" in result.text assert "" in result.text @@ -385,7 +385,7 @@ class TestThreadItemConverter: result = converter.widget_to_input(widget_item) assert isinstance(result, ChatMessage) - assert result.role == Role.USER + assert result.role == "user" assert "widget_1" in result.text assert "graphical UI widget" in result.text @@ -418,5 +418,5 @@ class TestSimpleToAgentInput: assert len(result) == 1 assert isinstance(result[0], ChatMessage) - assert result[0].role == Role.USER + assert result[0].role == "user" assert result[0].text == "Test message" diff --git a/python/packages/chatkit/tests/test_streaming.py b/python/packages/chatkit/tests/test_streaming.py index ff552d79e8..c26a9cb7ac 100644 --- a/python/packages/chatkit/tests/test_streaming.py +++ b/python/packages/chatkit/tests/test_streaming.py @@ -4,7 +4,7 @@ from unittest.mock import Mock -from agent_framework import AgentResponseUpdate, Content, Role +from agent_framework import AgentResponseUpdate, Content from chatkit.types import ( ThreadItemAddedEvent, ThreadItemDoneEvent, @@ -34,7 +34,7 @@ class TestStreamAgentResponse: """Test streaming single text update.""" async def single_update_stream(): - yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello world")]) + yield AgentResponseUpdate(role="assistant", contents=[Content.from_text(text="Hello world")]) events = [] async for event in stream_agent_response(single_update_stream(), thread_id="test_thread"): @@ -59,8 +59,8 @@ class TestStreamAgentResponse: """Test streaming multiple text updates.""" async def multiple_updates_stream(): - yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello ")]) - yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="world!")]) + yield AgentResponseUpdate(role="assistant", contents=[Content.from_text(text="Hello ")]) + yield AgentResponseUpdate(role="assistant", contents=[Content.from_text(text="world!")]) events = [] async for event in stream_agent_response(multiple_updates_stream(), thread_id="test_thread"): @@ -91,7 +91,7 @@ class TestStreamAgentResponse: return f"custom_{item_type}_123" async def single_update_stream(): - yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="Test")]) + yield AgentResponseUpdate(role="assistant", contents=[Content.from_text(text="Test")]) events = [] async for event in stream_agent_response( @@ -107,8 +107,8 @@ class TestStreamAgentResponse: """Test streaming updates with empty content.""" async def empty_content_stream(): - yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[]) - yield AgentResponseUpdate(role=Role.ASSISTANT, contents=None) + yield AgentResponseUpdate(role="assistant", contents=[]) + yield AgentResponseUpdate(role="assistant", contents=None) events = [] async for event in stream_agent_response(empty_content_stream(), thread_id="test_thread"): @@ -131,7 +131,7 @@ class TestStreamAgentResponse: non_text_content.text = None async def non_text_stream(): - yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[non_text_content]) + yield AgentResponseUpdate(role="assistant", contents=[non_text_content]) events = [] async for event in stream_agent_response(non_text_stream(), thread_id="test_thread"): diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index f8f3796656..f4439df851 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -16,7 +16,6 @@ from agent_framework import ( Content, ContextProvider, FunctionTool, - Role, ToolProtocol, get_logger, normalize_messages, @@ -628,7 +627,7 @@ class ClaudeAgent(BaseAgent, Generic[TOptions]): text = delta.get("text", "") if text: yield AgentResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(text=text, raw_representation=message)], raw_representation=message, ) @@ -636,7 +635,7 @@ class ClaudeAgent(BaseAgent, Generic[TOptions]): thinking = delta.get("thinking", "") if thinking: yield AgentResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text_reasoning(text=thinking, raw_representation=message)], raw_representation=message, ) diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py index 15fc0b8090..d54489cd0d 100644 --- a/python/packages/claude/tests/test_claude_agent.py +++ b/python/packages/claude/tests/test_claude_agent.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import AgentResponseUpdate, AgentThread, ChatMessage, Content, Role, tool +from agent_framework import AgentResponseUpdate, AgentThread, ChatMessage, Content, tool from agent_framework_claude import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings from agent_framework_claude._agent import TOOLS_MCP_SERVER_NAME @@ -375,7 +375,7 @@ class TestClaudeAgentRunStream: updates.append(update) # StreamEvent yields text deltas assert len(updates) == 2 - assert updates[0].role == Role.ASSISTANT + assert updates[0].role == "assistant" assert updates[0].text == "Streaming " assert updates[1].text == "response" @@ -632,7 +632,7 @@ class TestFormatPrompt: """Test formatting user message.""" agent = ClaudeAgent() msg = ChatMessage( - role=Role.USER, + role="user", contents=[Content.from_text(text="Hello")], ) result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage] @@ -642,9 +642,9 @@ class TestFormatPrompt: """Test formatting multiple messages.""" agent = ClaudeAgent() messages = [ - ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hi")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello!")]), - ChatMessage(role=Role.USER, contents=[Content.from_text(text="How are you?")]), + ChatMessage("user", [Content.from_text(text="Hi")]), + ChatMessage("assistant", [Content.from_text(text="Hello!")]), + ChatMessage("user", [Content.from_text(text="How are you?")]), ] result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage] assert "Hi" in result diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py index 98d5a2b475..6d764bf68a 100644 --- a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py @@ -12,7 +12,6 @@ from agent_framework import ( ChatMessage, Content, ContextProvider, - Role, normalize_messages, ) from agent_framework._pydantic import AFBaseSettings @@ -331,7 +330,7 @@ class CopilotStudioAgent(BaseAgent): (activity.type == "message" and not streaming) or (activity.type == "typing" and streaming) ): yield ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(activity.text)], author_name=activity.from_property.name if activity.from_property else None, message_id=activity.id, diff --git a/python/packages/copilotstudio/tests/test_copilot_agent.py b/python/packages/copilotstudio/tests/test_copilot_agent.py index c4e2ff3e08..4f3edbbbfd 100644 --- a/python/packages/copilotstudio/tests/test_copilot_agent.py +++ b/python/packages/copilotstudio/tests/test_copilot_agent.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import MagicMock, patch import pytest -from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, ChatMessage, Content, Role +from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, ChatMessage, Content from agent_framework.exceptions import ServiceException, ServiceInitializationError from microsoft_agents.copilotstudio.client import CopilotClient @@ -131,7 +131,7 @@ class TestCopilotStudioAgent: content = response.messages[0].contents[0] assert content.type == "text" assert content.text == "Test response" - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" async def test_run_with_chat_message(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None: """Test run method with ChatMessage.""" @@ -143,7 +143,7 @@ class TestCopilotStudioAgent: mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity]) mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity]) - chat_message = ChatMessage(role=Role.USER, contents=[Content.from_text("test message")]) + chat_message = ChatMessage("user", [Content.from_text("test message")]) response = await agent.run(chat_message) assert isinstance(response, AgentResponse) @@ -151,7 +151,7 @@ class TestCopilotStudioAgent: content = response.messages[0].contents[0] assert content.type == "text" assert content.text == "Test response" - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" async def test_run_with_thread(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None: """Test run method with existing thread.""" diff --git a/python/packages/core/README.md b/python/packages/core/README.md index 4113eca061..30ff1b7aa4 100644 --- a/python/packages/core/README.md +++ b/python/packages/core/README.md @@ -96,8 +96,8 @@ async def main(): client = OpenAIChatClient() messages = [ - ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant."), - ChatMessage(role=Role.USER, text="Write a haiku about Agent Framework.") + ChatMessage("system", ["You are a helpful assistant."]), + ChatMessage("user", ["Write a haiku about Agent Framework."]) ] response = await client.get_response(messages) diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index 4dc6df2eac..5c36d937fa 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -38,6 +38,7 @@ from ._types import ( ChatMessage, ChatResponse, ChatResponseUpdate, + Content, normalize_messages, ) from .exceptions import AgentExecutionException, AgentInitializationError @@ -209,7 +210,7 @@ class AgentProtocol(Protocol): async def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -240,7 +241,7 @@ class AgentProtocol(Protocol): def run_stream( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -490,7 +491,7 @@ class BaseAgent(SerializationMixin): stream_callback(update) # Create final text from accumulated updates - return AgentResponse.from_agent_run_response_updates(response_updates).text + return AgentResponse.from_updates(response_updates).text agent_tool: FunctionTool[BaseModel, str] = FunctionTool( name=tool_name, @@ -755,7 +756,7 @@ class ChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc] @overload async def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, *, thread: AgentThread | None = None, tools: ToolProtocol @@ -770,7 +771,7 @@ class ChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc] @overload async def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, *, thread: AgentThread | None = None, tools: ToolProtocol @@ -784,7 +785,7 @@ class ChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc] async def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, *, thread: AgentThread | None = None, tools: ToolProtocol @@ -927,7 +928,7 @@ class ChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc] async def run_stream( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, *, thread: AgentThread | None = None, tools: ToolProtocol @@ -1043,9 +1044,7 @@ class ChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc] raw_representation=update, ) - response = ChatResponse.from_chat_response_updates( - response_updates, output_format_type=co.get("response_format") - ) + response = ChatResponse.from_updates(response_updates, output_format_type=co.get("response_format")) await self._update_thread_with_type_and_conversation_id(thread, response.conversation_id) await self._notify_thread_of_new_messages( diff --git a/python/packages/core/agent_framework/_clients.py b/python/packages/core/agent_framework/_clients.py index 68d9d0312f..60fe7698ea 100644 --- a/python/packages/core/agent_framework/_clients.py +++ b/python/packages/core/agent_framework/_clients.py @@ -45,6 +45,7 @@ from ._types import ( ChatMessage, ChatResponse, ChatResponseUpdate, + Content, prepare_messages, validate_chat_options, ) @@ -129,7 +130,7 @@ class ChatClientProtocol(Protocol[TOptions_contra]): # @overload async def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage], *, options: "ChatOptions[TResponseModelT]", **kwargs: Any, @@ -138,7 +139,7 @@ class ChatClientProtocol(Protocol[TOptions_contra]): # @overload async def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage], *, options: TOptions_contra | None = None, **kwargs: Any, @@ -160,7 +161,7 @@ class ChatClientProtocol(Protocol[TOptions_contra]): # def get_streaming_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage], *, options: TOptions_contra | None = None, **kwargs: Any, @@ -219,9 +220,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[TOptions_co]): class CustomChatClient(BaseChatClient): async def _inner_get_response(self, *, messages, options, **kwargs): # Your custom implementation - return ChatResponse( - messages=[ChatMessage(role="assistant", text="Hello!")], response_id="custom-response" - ) + return ChatResponse(messages=[ChatMessage("assistant", ["Hello!"])], response_id="custom-response") async def _inner_get_streaming_response(self, *, messages, options, **kwargs): # Your custom streaming implementation @@ -341,7 +340,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[TOptions_co]): @overload async def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage], *, options: "ChatOptions[TResponseModelT]", **kwargs: Any, @@ -350,7 +349,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[TOptions_co]): @overload async def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage], *, options: TOptions_co | None = None, **kwargs: Any, @@ -358,7 +357,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[TOptions_co]): async def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage], *, options: TOptions_co | "ChatOptions[Any]" | None = None, **kwargs: Any, @@ -381,7 +380,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[TOptions_co]): async def get_streaming_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage], *, options: TOptions_co | None = None, **kwargs: Any, diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index 9410a6698b..578fb606e1 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -32,7 +32,6 @@ from ._tools import ( from ._types import ( ChatMessage, Content, - Role, ) from .exceptions import ToolException, ToolExecutionException @@ -71,7 +70,7 @@ def _parse_message_from_mcp( ) -> ChatMessage: """Parse an MCP container type into an Agent Framework type.""" return ChatMessage( - role=Role(value=mcp_type.role), + role=mcp_type.role, contents=_parse_content_from_mcp(mcp_type.content), raw_representation=mcp_type, ) diff --git a/python/packages/core/agent_framework/_middleware.py b/python/packages/core/agent_framework/_middleware.py index c41c2e7b5b..4cd136a230 100644 --- a/python/packages/core/agent_framework/_middleware.py +++ b/python/packages/core/agent_framework/_middleware.py @@ -429,7 +429,7 @@ class ChatMiddleware(ABC): # Add system prompt to messages from agent_framework import ChatMessage - context.messages.insert(0, ChatMessage(role="system", content=self.system_prompt)) + context.messages.insert(0, ChatMessage("system", [self.system_prompt])) # Continue execution await next(context) diff --git a/python/packages/core/agent_framework/_serialization.py b/python/packages/core/agent_framework/_serialization.py index e4866c12d6..01161435ec 100644 --- a/python/packages/core/agent_framework/_serialization.py +++ b/python/packages/core/agent_framework/_serialization.py @@ -38,7 +38,7 @@ class SerializationProtocol(Protocol): # ChatMessage implements SerializationProtocol via SerializationMixin - user_msg = ChatMessage(role="user", text="What's the weather like today?") + user_msg = ChatMessage("user", ["What's the weather like today?"]) # Serialize to dictionary - automatic type identification and nested serialization msg_dict = user_msg.to_dict() @@ -53,7 +53,7 @@ class SerializationProtocol(Protocol): # Deserialize back to ChatMessage instance - automatic type reconstruction restored_msg = ChatMessage.from_dict(msg_dict) print(restored_msg.text) # "What's the weather like today?" - print(restored_msg.role.value) # "user" + print(restored_msg.role) # "user" # Verify protocol compliance (useful for type checking and validation) assert isinstance(user_msg, SerializationProtocol) @@ -175,8 +175,8 @@ class SerializationMixin: # ChatMessageStoreState handles nested ChatMessage serialization store_state = ChatMessageStoreState( messages=[ - ChatMessage(role="user", text="Hello agent"), - ChatMessage(role="assistant", text="Hi! How can I help?"), + ChatMessage("user", ["Hello agent"]), + ChatMessage("assistant", ["Hi! How can I help?"]), ] ) diff --git a/python/packages/core/agent_framework/_threads.py b/python/packages/core/agent_framework/_threads.py index e44c362324..a9d53c9890 100644 --- a/python/packages/core/agent_framework/_threads.py +++ b/python/packages/core/agent_framework/_threads.py @@ -202,7 +202,7 @@ class ChatMessageStore: store = ChatMessageStore() # Add messages - message = ChatMessage(role="user", content="Hello") + message = ChatMessage("user", ["Hello"]) await store.add_messages([message]) # Retrieve messages diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index d88fa4b54c..56594ecec2 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -1831,7 +1831,6 @@ def _replace_approval_contents_with_results( """Replace approval request/response contents with function call/result contents in-place.""" from ._types import ( Content, - Role, ) result_idx = 0 @@ -1861,7 +1860,7 @@ def _replace_approval_contents_with_results( if result_idx < len(approved_function_results): msg.contents[content_idx] = approved_function_results[result_idx] result_idx += 1 - msg.role = Role.TOOL + msg.role = "tool" else: # Create a "not approved" result for rejected calls # Use function_call.call_id (the function's ID), not content.id (approval's ID) @@ -1869,7 +1868,7 @@ def _replace_approval_contents_with_results( call_id=content.function_call.call_id, # type: ignore[union-attr, arg-type] result="Error: Tool call invocation was rejected by user.", ) - msg.role = Role.TOOL + msg.role = "tool" # Remove approval requests that were duplicates (in reverse order to preserve indices) for idx in reversed(contents_to_remove): @@ -1988,13 +1987,12 @@ def _handle_function_calls_response( if any(fccr.type == "function_approval_request" for fccr in function_call_results): # Add approval requests to the existing assistant message (with tool_calls) # instead of creating a separate tool message - from ._types import Role - if response.messages and response.messages[0].role == Role.ASSISTANT: + if response.messages and response.messages[0].role == "assistant": response.messages[0].contents.extend(function_call_results) else: # Fallback: create new assistant message (shouldn't normally happen) - result_message = ChatMessage(role="assistant", contents=function_call_results) + result_message = ChatMessage("assistant", function_call_results) response.messages.append(result_message) return response if any(fccr.type == "function_call" for fccr in function_call_results): @@ -2005,7 +2003,7 @@ def _handle_function_calls_response( # This allows middleware to short-circuit the tool loop without another LLM call if should_terminate: # Add tool results to response and return immediately without calling LLM again - result_message = ChatMessage(role="tool", contents=function_call_results) + result_message = ChatMessage("tool", function_call_results) response.messages.append(result_message) if fcc_messages: for msg in reversed(fcc_messages): @@ -2026,7 +2024,7 @@ def _handle_function_calls_response( errors_in_a_row = 0 # add a single ChatMessage to the response with the results - result_message = ChatMessage(role="tool", contents=function_call_results) + result_message = ChatMessage("tool", function_call_results) response.messages.append(result_message) # response should contain 2 messages after this, # one with function call contents @@ -2162,7 +2160,7 @@ def _handle_function_calls_streaming_response( # Depending on the prompt, the message may contain both function call # content and others - response: "ChatResponse" = ChatResponse.from_chat_response_updates(all_updates) + response: "ChatResponse" = ChatResponse.from_updates(all_updates) # get the function calls (excluding ones that already have results) function_results = {it.call_id for it in response.messages[0].contents if it.type == "function_result"} function_calls = [ @@ -2206,15 +2204,14 @@ def _handle_function_calls_streaming_response( if any(fccr.type == "function_approval_request" for fccr in function_call_results): # Add approval requests to the existing assistant message (with tool_calls) # instead of creating a separate tool message - from ._types import Role - if response.messages and response.messages[0].role == Role.ASSISTANT: + if response.messages and response.messages[0].role == "assistant": response.messages[0].contents.extend(function_call_results) # Yield the approval requests as part of the assistant message yield ChatResponseUpdate(contents=function_call_results, role="assistant") else: # Fallback: create new assistant message (shouldn't normally happen) - result_message = ChatMessage(role="assistant", contents=function_call_results) + result_message = ChatMessage("assistant", function_call_results) yield ChatResponseUpdate(contents=function_call_results, role="assistant") response.messages.append(result_message) return @@ -2243,7 +2240,7 @@ def _handle_function_calls_streaming_response( errors_in_a_row = 0 # add a single ChatMessage to the response with the results - result_message = ChatMessage(role="tool", contents=function_call_results) + result_message = ChatMessage("tool", function_call_results) yield ChatResponseUpdate(contents=function_call_results, role="tool") response.messages.append(result_message) # response should contain 2 messages after this, diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 9c49d25845..826394b11c 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -8,13 +8,12 @@ from collections.abc import ( Callable, Mapping, MutableMapping, - MutableSequence, Sequence, ) from copy import deepcopy -from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Literal, cast, overload +from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Literal, NewType, cast, overload -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel from ._logging import get_logger from ._serialization import SerializationMixin @@ -40,7 +39,9 @@ __all__ = [ "ChatResponseUpdate", "Content", "FinishReason", + "FinishReasonLiteral", "Role", + "RoleLiteral", "TextSpanRegion", "ToolMode", "UsageDetails", @@ -62,42 +63,25 @@ logger = get_logger("agent_framework") # region Content Parsing Utilities -class EnumLike(type): - """Generic metaclass for creating enum-like classes with predefined constants. - - This metaclass automatically creates class-level constants based on a _constants - class attribute. Each constant is defined as a tuple of (name, *args) where - name is the constant name and args are the constructor arguments. - """ - - def __new__(mcs, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> "EnumLike": - cls = super().__new__(mcs, name, bases, namespace) - - # Create constants if _constants is defined - if (const := getattr(cls, "_constants", None)) and isinstance(const, dict): - for const_name, const_args in const.items(): - if isinstance(const_args, (list, tuple)): - setattr(cls, const_name, cls(*const_args)) - else: - setattr(cls, const_name, cls(const_args)) - - return cls - - def _parse_content_list(contents_data: Sequence[Any]) -> list["Content"]: - """Parse a list of content data dictionaries into appropriate Content objects. + """Parse a list of content data into appropriate Content objects. Args: - contents_data: List of content data (dicts or already constructed objects) + contents_data: List of content data (strings, dicts, or already constructed objects) Returns: List of Content objects with unknown types logged and ignored """ contents: list["Content"] = [] for content_data in contents_data: + if content_data is None: + continue if isinstance(content_data, Content): contents.append(content_data) continue + if isinstance(content_data, str): + contents.append(Content.from_text(text=content_data)) + continue try: contents.append(Content.from_dict(content_data)) except ContentError as exc: @@ -1420,140 +1404,56 @@ def prepare_function_call_results(content: "Content | Any | list[Content | Any]" # region Chat Response constants +RoleLiteral = Literal["system", "user", "assistant", "tool"] +"""Literal type for known role values. Accepts any string for extensibility.""" -class Role(SerializationMixin, metaclass=EnumLike): - """Describes the intended purpose of a message within a chat interaction. +Role = NewType("Role", str) +"""Type for chat message roles. Use string values directly (e.g., "user", "assistant"). - Attributes: - value: The string representation of the role. +Known values: "system", "user", "assistant", "tool" - Properties: - SYSTEM: The role that instructs or sets the behavior of the AI system. - USER: The role that provides user input for chat interactions. - ASSISTANT: The role that provides responses to system-instructed, user-prompted input. - TOOL: The role that provides additional information and references in response to tool use requests. +Examples: + .. code-block:: python - Examples: - .. code-block:: python + from agent_framework import ChatMessage - from agent_framework import Role + # Use string values directly + user_msg = ChatMessage("user", ["Hello"]) + assistant_msg = ChatMessage("assistant", ["Hi there!"]) - # Use predefined role constants - system_role = Role.SYSTEM - user_role = Role.USER - assistant_role = Role.ASSISTANT - tool_role = Role.TOOL + # Custom roles are also supported + custom_msg = ChatMessage("custom", ["Custom role message"]) - # Create custom role - custom_role = Role(value="custom") + # Compare roles directly as strings + if user_msg.role == "user": + print("This is a user message") +""" - # Compare roles - print(system_role == Role.SYSTEM) # True - print(system_role.value) # "system" - """ +FinishReasonLiteral = Literal["stop", "length", "tool_calls", "content_filter"] +"""Literal type for known finish reason values. Accepts any string for extensibility.""" - # Constants configuration for EnumLike metaclass - _constants: ClassVar[dict[str, str]] = { - "SYSTEM": "system", - "USER": "user", - "ASSISTANT": "assistant", - "TOOL": "tool", - } +FinishReason = NewType("FinishReason", str) +"""Type for chat response finish reasons. Use string values directly. - # Type annotations for constants - SYSTEM: "Role" - USER: "Role" - ASSISTANT: "Role" - TOOL: "Role" +Known values: + - "stop": Normal completion + - "length": Max tokens reached + - "tool_calls": Tool calls triggered + - "content_filter": Content filter triggered - def __init__(self, value: str) -> None: - """Initialize Role with a value. +Examples: + .. code-block:: python - Args: - value: The string representation of the role. - """ - self.value = value + from agent_framework import ChatResponse - def __str__(self) -> str: - """Returns the string representation of the role.""" - return self.value + response = ChatResponse(messages=[...], finish_reason="stop") - def __repr__(self) -> str: - """Returns the string representation of the role.""" - return f"Role(value={self.value!r})" - - def __eq__(self, other: object) -> bool: - """Check if two Role instances are equal.""" - if not isinstance(other, Role): - return False - return self.value == other.value - - def __hash__(self) -> int: - """Return hash of the Role for use in sets and dicts.""" - return hash(self.value) - - -class FinishReason(SerializationMixin, metaclass=EnumLike): - """Represents the reason a chat response completed. - - Attributes: - value: The string representation of the finish reason. - - Examples: - .. code-block:: python - - from agent_framework import FinishReason - - # Use predefined finish reason constants - stop_reason = FinishReason.STOP # Normal completion - length_reason = FinishReason.LENGTH # Max tokens reached - tool_calls_reason = FinishReason.TOOL_CALLS # Tool calls triggered - filter_reason = FinishReason.CONTENT_FILTER # Content filter triggered - - # Check finish reason - if stop_reason == FinishReason.STOP: - print("Response completed normally") - """ - - # Constants configuration for EnumLike metaclass - _constants: ClassVar[dict[str, str]] = { - "CONTENT_FILTER": "content_filter", - "LENGTH": "length", - "STOP": "stop", - "TOOL_CALLS": "tool_calls", - } - - # Type annotations for constants - CONTENT_FILTER: "FinishReason" - LENGTH: "FinishReason" - STOP: "FinishReason" - TOOL_CALLS: "FinishReason" - - def __init__(self, value: str) -> None: - """Initialize FinishReason with a value. - - Args: - value: The string representation of the finish reason. - """ - self.value = value - - def __eq__(self, other: object) -> bool: - """Check if two FinishReason instances are equal.""" - if not isinstance(other, FinishReason): - return False - return self.value == other.value - - def __hash__(self) -> int: - """Return hash of the FinishReason for use in sets and dicts.""" - return hash(self.value) - - def __str__(self) -> str: - """Returns the string representation of the finish reason.""" - return self.value - - def __repr__(self) -> str: - """Returns the string representation of the finish reason.""" - return f"FinishReason(value={self.value!r})" + # Check finish reason directly as string + if response.finish_reason == "stop": + print("Response completed normally") + elif response.finish_reason == "tool_calls": + print("Tool calls need to be processed") +""" # region ChatMessage @@ -1574,138 +1474,82 @@ class ChatMessage(SerializationMixin): Examples: .. code-block:: python - from agent_framework import ChatMessage, TextContent + from agent_framework import ChatMessage, Content - # Create a message with text - user_msg = ChatMessage(role="user", text="What's the weather?") + # Create a message with text content + user_msg = ChatMessage("user", ["What's the weather?"]) print(user_msg.text) # "What's the weather?" - # Create a message with role string - system_msg = ChatMessage(role="system", text="You are a helpful assistant.") + # Create a system message + system_msg = ChatMessage("system", ["You are a helpful assistant."]) - # Create a message with contents + # Create a message with mixed content types assistant_msg = ChatMessage( - role="assistant", - contents=[Content.from_text(text="The weather is sunny!")], + "assistant", + ["The weather is sunny!", Content.from_image_uri("https://...")], ) print(assistant_msg.text) # "The weather is sunny!" # Serialization - to_dict and from_dict msg_dict = user_msg.to_dict() - # {'type': 'chat_message', 'role': {'type': 'role', 'value': 'user'}, + # {'type': 'chat_message', 'role': 'user', # 'contents': [{'type': 'text', 'text': "What's the weather?"}], 'additional_properties': {}} restored_msg = ChatMessage.from_dict(msg_dict) print(restored_msg.text) # "What's the weather?" # Serialization - to_json and from_json msg_json = user_msg.to_json() - # '{"type": "chat_message", "role": {"type": "role", "value": "user"}, "contents": [...], ...}' + # '{"type": "chat_message", "role": "user", "contents": [...], ...}' restored_from_json = ChatMessage.from_json(msg_json) - print(restored_from_json.role.value) # "user" + print(restored_from_json.role) # "user" """ DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"} - @overload def __init__( self, - role: Role | Literal["system", "user", "assistant", "tool"], - *, - text: str, - author_name: str | None = None, - message_id: str | None = None, - additional_properties: MutableMapping[str, Any] | None = None, - raw_representation: Any | None = None, - **kwargs: Any, - ) -> None: - """Initializes a ChatMessage with a role and text content. - - Args: - role: The role of the author of the message. - - Keyword Args: - text: The text content of the message. - author_name: Optional name of the author of the message. - message_id: Optional ID of the chat message. - additional_properties: Optional additional properties associated with the chat message. - Additional properties are used within Agent Framework, they are not sent to services. - raw_representation: Optional raw representation of the chat message. - **kwargs: Additional keyword arguments. - """ - - @overload - def __init__( - self, - role: Role | Literal["system", "user", "assistant", "tool"], - *, - contents: "Sequence[Content | Mapping[str, Any]]", - author_name: str | None = None, - message_id: str | None = None, - additional_properties: MutableMapping[str, Any] | None = None, - raw_representation: Any | None = None, - **kwargs: Any, - ) -> None: - """Initializes a ChatMessage with a role and optional contents. - - Args: - role: The role of the author of the message. - - Keyword Args: - contents: Optional list of BaseContent items to include in the message. - author_name: Optional name of the author of the message. - message_id: Optional ID of the chat message. - additional_properties: Optional additional properties associated with the chat message. - Additional properties are used within Agent Framework, they are not sent to services. - raw_representation: Optional raw representation of the chat message. - **kwargs: Additional keyword arguments. - """ - - def __init__( - self, - role: Role | Literal["system", "user", "assistant", "tool"] | dict[str, Any], + role: RoleLiteral | str, + contents: "Sequence[Content | str | Mapping[str, Any]] | None" = None, *, text: str | None = None, - contents: "Sequence[Content | Mapping[str, Any]] | None" = None, author_name: str | None = None, message_id: str | None = None, additional_properties: MutableMapping[str, Any] | None = None, raw_representation: Any | None = None, - **kwargs: Any, ) -> None: """Initialize ChatMessage. Args: - role: The role of the author of the message (Role, string, or dict). + role: The role of the author of the message (e.g., "user", "assistant", "system", "tool"). + contents: A sequence of content items. Can be Content objects, strings (auto-converted + to TextContent), or dicts (parsed via Content.from_dict). Defaults to empty list. Keyword Args: - text: Optional text content of the message. - contents: Optional list of BaseContent items or dicts to include in the message. + text: Deprecated. Text content of the message. Use contents instead. + This parameter is kept for backward compatibility with serialization. author_name: Optional name of the author of the message. message_id: Optional ID of the chat message. additional_properties: Optional additional properties associated with the chat message. Additional properties are used within Agent Framework, they are not sent to services. raw_representation: Optional raw representation of the chat message. - kwargs: will be combined with additional_properties if provided. """ - # Handle role conversion - if isinstance(role, dict): - role = Role.from_dict(role) - elif isinstance(role, str): - role = Role(value=role) + # Handle role conversion from legacy dict format + if isinstance(role, dict) and "value" in role: + role = role["value"] # Handle contents conversion parsed_contents = [] if contents is None else _parse_content_list(contents) + # Handle text for backward compatibility (from serialization) if text is not None: parsed_contents.append(Content.from_text(text=text)) - self.role = role + self.role: str = role self.contents = parsed_contents self.author_name = author_name self.message_id = message_id self.additional_properties = additional_properties or {} - self.additional_properties.update(kwargs or {}) self.raw_representation = raw_representation @property @@ -1719,12 +1563,17 @@ class ChatMessage(SerializationMixin): def prepare_messages( - messages: str | ChatMessage | Sequence[str | ChatMessage], system_instructions: str | Sequence[str] | None = None + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage], + system_instructions: str | Sequence[str] | None = None, ) -> list[ChatMessage]: """Convert various message input formats into a list of ChatMessage objects. Args: - messages: The input messages in various supported formats. + messages: The input messages in various supported formats. Can be: + - A string (converted to a user message) + - A Content object (wrapped in a user ChatMessage) + - A ChatMessage object + - A sequence containing any mix of the above system_instructions: The system instructions. They will be inserted to the start of the messages list. Returns: @@ -1733,43 +1582,66 @@ def prepare_messages( if system_instructions is not None: if isinstance(system_instructions, str): system_instructions = [system_instructions] - system_instruction_messages = [ChatMessage(role="system", text=instr) for instr in system_instructions] + system_instruction_messages = [ChatMessage("system", [instr]) for instr in system_instructions] else: system_instruction_messages = [] if isinstance(messages, str): - return [*system_instruction_messages, ChatMessage(role="user", text=messages)] + return [*system_instruction_messages, ChatMessage("user", [messages])] + if isinstance(messages, Content): + return [*system_instruction_messages, ChatMessage("user", [messages])] if isinstance(messages, ChatMessage): return [*system_instruction_messages, messages] return_messages: list[ChatMessage] = system_instruction_messages for msg in messages: - if isinstance(msg, str): - msg = ChatMessage(role="user", text=msg) + if isinstance(msg, (str, Content)): + msg = ChatMessage("user", [msg]) return_messages.append(msg) return return_messages def normalize_messages( - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, ) -> list[ChatMessage]: - """Normalize message inputs to a list of ChatMessage objects.""" + """Normalize message inputs to a list of ChatMessage objects. + + Args: + messages: The input messages in various supported formats. Can be: + - None (returns empty list) + - A string (converted to a user message) + - A Content object (wrapped in a user ChatMessage) + - A ChatMessage object + - A sequence containing any mix of the above + + Returns: + A list of ChatMessage objects. + """ if messages is None: return [] if isinstance(messages, str): - return [ChatMessage(role=Role.USER, text=messages)] + return [ChatMessage("user", [messages])] + + if isinstance(messages, Content): + return [ChatMessage("user", [messages])] if isinstance(messages, ChatMessage): return [messages] - return [ChatMessage(role=Role.USER, text=msg) if isinstance(msg, str) else msg for msg in messages] + result: list[ChatMessage] = [] + for msg in messages: + if isinstance(msg, (str, Content)): + result.append(ChatMessage("user", [msg])) + else: + result.append(msg) + return result def prepend_instructions_to_messages( messages: list[ChatMessage], instructions: str | Sequence[str] | None, - role: Role | Literal["system", "user", "assistant"] = "system", + role: RoleLiteral | str = "system", ) -> list[ChatMessage]: """Prepend instructions to a list of messages with a specified role. @@ -1790,7 +1662,7 @@ def prepend_instructions_to_messages( from agent_framework import prepend_instructions_to_messages, ChatMessage - messages = [ChatMessage(role="user", text="Hello")] + messages = [ChatMessage("user", ["Hello"])] instructions = "You are a helpful assistant" # Prepend as system message (default) @@ -1805,7 +1677,7 @@ def prepend_instructions_to_messages( if isinstance(instructions, str): instructions = [instructions] - instruction_messages = [ChatMessage(role=role, text=instr) for instr in instructions] + instruction_messages = [ChatMessage(role, [instr]) for instr in instructions] return [*instruction_messages, *messages] @@ -1829,7 +1701,7 @@ def _process_update( is_new_message = True if is_new_message: - message = ChatMessage(role=Role.ASSISTANT, contents=[]) + message = ChatMessage("assistant", []) response.messages.append(message) else: message = response.messages[-1] @@ -1937,31 +1809,32 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): additional_properties: Any additional properties associated with the chat response. raw_representation: The raw representation of the chat response from an underlying implementation. + Note: + The `author_name` attribute is available on the `ChatMessage` objects inside `messages`, + not on the `ChatResponse` itself. Use `response.messages[0].author_name` to access + the author name of individual messages. + Examples: .. code-block:: python from agent_framework import ChatResponse, ChatMessage - # Create a simple text response - response = ChatResponse(text="Hello, how can I help you?") - print(response.text) # "Hello, how can I help you?" - # Create a response with messages - msg = ChatMessage(role="assistant", text="The weather is sunny.") + msg = ChatMessage("assistant", ["The weather is sunny."]) response = ChatResponse( messages=[msg], finish_reason="stop", model_id="gpt-4", ) + print(response.text) # "The weather is sunny." # Combine streaming updates updates = [...] # List of ChatResponseUpdate objects - response = ChatResponse.from_chat_response_updates(updates) + response = ChatResponse.from_updates(updates) # Serialization - to_dict and from_dict response_dict = response.to_dict() - # {'type': 'chat_response', 'messages': [...], 'model_id': 'gpt-4', - # 'finish_reason': {'type': 'finish_reason', 'value': 'stop'}} + # {'type': 'chat_response', 'messages': [...], 'model_id': 'gpt-4', 'finish_reason': 'stop'} restored_response = ChatResponse.from_dict(response_dict) print(restored_response.model_id) # "gpt-4" @@ -1974,154 +1847,66 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation", "additional_properties"} - @overload def __init__( self, *, - messages: ChatMessage | MutableSequence[ChatMessage], + messages: ChatMessage | Sequence[ChatMessage] | None = None, response_id: str | None = None, conversation_id: str | None = None, model_id: str | None = None, created_at: CreatedAtT | None = None, - finish_reason: FinishReason | None = None, + finish_reason: FinishReasonLiteral | str | None = None, usage_details: UsageDetails | None = None, value: TResponseModel | None = None, response_format: type[BaseModel] | None = None, additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, - **kwargs: Any, ) -> None: """Initializes a ChatResponse with the provided parameters. Keyword Args: - messages: A single ChatMessage or a sequence of ChatMessage objects to include in the response. + messages: A single ChatMessage or sequence of ChatMessage 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. - finish_reason: Optional reason for the chat response. - usage_details: Optional usage details for the chat response. - value: Optional value of the structured output. - response_format: Optional response format for the chat response. - messages: List of ChatMessage objects to include in the response. - additional_properties: Optional additional properties associated with the chat response. - raw_representation: Optional raw representation of the chat response from an underlying implementation. - **kwargs: Any additional keyword arguments. - """ - - @overload - def __init__( - self, - *, - text: Content | str, - response_id: str | None = None, - conversation_id: str | None = None, - model_id: str | None = None, - created_at: CreatedAtT | None = None, - finish_reason: FinishReason | None = None, - usage_details: UsageDetails | None = None, - value: TResponseModel | None = None, - response_format: type[BaseModel] | None = None, - additional_properties: dict[str, Any] | None = None, - raw_representation: Any | None = None, - **kwargs: Any, - ) -> None: - """Initializes a ChatResponse with the provided parameters. - - Keyword Args: - text: The text content to include in the response. If provided, it will be added as a ChatMessage. - 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. - finish_reason: Optional reason for the chat response. + 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. response_format: Optional response format for the chat response. additional_properties: Optional additional properties associated with the chat response. raw_representation: Optional raw representation of the chat response from an underlying implementation. - **kwargs: Any additional keyword arguments. - """ - - def __init__( - self, - *, - messages: ChatMessage | MutableSequence[ChatMessage] | list[dict[str, Any]] | None = None, - text: Content | str | None = None, - response_id: str | None = None, - conversation_id: str | None = None, - model_id: str | None = None, - created_at: CreatedAtT | None = None, - finish_reason: FinishReason | dict[str, Any] | None = None, - usage_details: UsageDetails | dict[str, Any] | None = None, - value: TResponseModel | None = None, - response_format: type[BaseModel] | None = None, - additional_properties: dict[str, Any] | None = None, - raw_representation: Any | None = None, - **kwargs: Any, - ) -> None: - """Initializes a ChatResponse with the provided parameters. - - Keyword Args: - messages: A single ChatMessage or a sequence of ChatMessage objects to include in the response. - text: The text content to include in the response. If provided, it will be added as a ChatMessage. - 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. - finish_reason: Optional reason for the chat response. - usage_details: Optional usage details for the chat response. - value: Optional value of the structured output. - response_format: Optional response format for the chat response. - additional_properties: Optional additional properties associated with the chat response. - raw_representation: Optional raw representation of the chat response from an underlying implementation. - **kwargs: Any additional keyword arguments. - """ - # Handle messages conversion if messages is None: - messages = [] - elif not isinstance(messages, MutableSequence): - messages = [messages] + self.messages: list[ChatMessage] = [] + elif isinstance(messages, ChatMessage): + self.messages = [messages] else: - # Convert any dicts in messages list to ChatMessage objects - converted_messages: list[ChatMessage] = [] + # Handle both ChatMessage objects and dicts (for from_dict support) + processed_messages: list[ChatMessage] = [] for msg in messages: - if isinstance(msg, dict): - converted_messages.append(ChatMessage.from_dict(msg)) + if isinstance(msg, ChatMessage): + processed_messages.append(msg) + elif isinstance(msg, dict): + processed_messages.append(ChatMessage.from_dict(msg)) else: - converted_messages.append(msg) - messages = converted_messages - - if text is not None: - if isinstance(text, str): - text = Content.from_text(text=text) - messages.append(ChatMessage(role=Role.ASSISTANT, contents=[text])) - - # Handle finish_reason conversion - if isinstance(finish_reason, dict): - finish_reason = FinishReason.from_dict(finish_reason) - - # Handle usage_details - UsageDetails is now a TypedDict, so dict is already the right type - # No conversion needed - - self.messages = list(messages) + processed_messages.append(msg) + self.messages = processed_messages self.response_id = response_id self.conversation_id = conversation_id self.model_id = model_id self.created_at = created_at - self.finish_reason = finish_reason + self.finish_reason: str | None = finish_reason self.usage_details = usage_details self._value: TResponseModel | None = value self._response_format: type[BaseModel] | None = response_format self._value_parsed: bool = value is not None self.additional_properties = additional_properties or {} - self.additional_properties.update(kwargs or {}) self.raw_representation: Any | list[Any] | None = raw_representation @overload @classmethod - def from_chat_response_updates( + def from_updates( cls: type["ChatResponse[Any]"], updates: Sequence["ChatResponseUpdate"], *, @@ -2130,7 +1915,7 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): @overload @classmethod - def from_chat_response_updates( + def from_updates( cls: type["ChatResponse[Any]"], updates: Sequence["ChatResponseUpdate"], *, @@ -2138,7 +1923,7 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): ) -> "ChatResponse[Any]": ... @classmethod - def from_chat_response_updates( + def from_updates( cls: type[TChatResponse], updates: Sequence["ChatResponseUpdate"], *, @@ -2153,12 +1938,12 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): # Create some response updates updates = [ - ChatResponseUpdate(role="assistant", text="Hello"), - ChatResponseUpdate(text=" How can I help you?"), + ChatResponseUpdate(contents=[Content.from_text(text="Hello")], role="assistant"), + ChatResponseUpdate(contents=[Content.from_text(text=" How can I help you?")]), ] # Combine updates into a single ChatResponse - response = ChatResponse.from_chat_response_updates(updates) + response = ChatResponse.from_updates(updates) print(response.text) # "Hello How can I help you?" Args: @@ -2167,17 +1952,16 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): Keyword Args: output_format_type: Optional Pydantic model type to parse the response text into structured data. """ - msg = cls(messages=[]) + response_format = output_format_type if isinstance(output_format_type, type) else None + msg = cls(messages=[], response_format=response_format) for update in updates: _process_update(msg, update) _finalize_response(msg) - if output_format_type: - msg.try_parse_value(output_format_type) return msg @overload @classmethod - async def from_chat_response_generator( + async def from_update_generator( cls: type["ChatResponse[Any]"], updates: AsyncIterable["ChatResponseUpdate"], *, @@ -2186,7 +1970,7 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): @overload @classmethod - async def from_chat_response_generator( + async def from_update_generator( cls: type["ChatResponse[Any]"], updates: AsyncIterable["ChatResponseUpdate"], *, @@ -2194,7 +1978,7 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): ) -> "ChatResponse[Any]": ... @classmethod - async def from_chat_response_generator( + async def from_update_generator( cls: type[TChatResponse], updates: AsyncIterable["ChatResponseUpdate"], *, @@ -2208,7 +1992,7 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): from agent_framework import ChatResponse, ChatResponseUpdate, ChatClient client = ChatClient() # should be a concrete implementation - response = await ChatResponse.from_chat_response_generator( + response = await ChatResponse.from_update_generator( client.get_streaming_response("Hello, how are you?") ) print(response.text) @@ -2224,8 +2008,6 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): async for update in updates: _process_update(msg, update) _finalize_response(msg) - if response_format and issubclass(response_format, BaseModel): - msg.try_parse_value(response_format) return msg @property @@ -2257,47 +2039,6 @@ class ChatResponse(SerializationMixin, Generic[TResponseModel]): def __str__(self) -> str: return self.text - @overload - def try_parse_value(self, output_format_type: type[TResponseModelT]) -> TResponseModelT | None: ... - - @overload - def try_parse_value(self, output_format_type: None = None) -> TResponseModel | None: ... - - def try_parse_value(self, output_format_type: type[BaseModel] | None = None) -> BaseModel | None: - """Try to parse the text into a typed value. - - This is the safe alternative to accessing the value property directly. - Returns the parsed value on success, or None on failure. - - Args: - output_format_type: The Pydantic model type to parse into. - If None, uses the response_format from initialization. - - Returns: - The parsed value as the specified type, or None if parsing fails. - """ - format_type = output_format_type or self._response_format - if format_type is None or not (isinstance(format_type, type) and issubclass(format_type, BaseModel)): - return None - - # Cache the result unless a different schema than the configured response_format is requested. - # This prevents calls with a different schema from polluting the cached value. - use_cache = ( - self._response_format is None or output_format_type is None or output_format_type is self._response_format - ) - - if use_cache and self._value_parsed and self._value is not None: - return self._value # type: ignore[return-value, no-any-return] - try: - parsed_value = format_type.model_validate_json(self.text) # type: ignore[reportUnknownMemberType] - if use_cache: - self._value = cast(TResponseModel, parsed_value) - self._value_parsed = True - return parsed_value # type: ignore[return-value] - except ValidationError as ex: - logger.warning("Failed to parse value from chat response text: %s", ex) - return None - # region ChatResponseUpdate @@ -2308,7 +2049,10 @@ class ChatResponseUpdate(SerializationMixin): Attributes: contents: The chat response update content items. role: The role of the author of the response update. - author_name: The name of the author of the response update. + author_name: The name of the author of the response update. This is primarily used in + multi-agent scenarios to identify which agent or participant generated the response. + When updates are combined into a `ChatResponse`, the `author_name` is propagated + to the resulting `ChatMessage` objects. response_id: The ID of the response of which this update is a part. message_id: The ID of the message of which this update is a part. conversation_id: An identifier for the state of the conversation of which this update is a part. @@ -2321,9 +2065,9 @@ class ChatResponseUpdate(SerializationMixin): Examples: .. code-block:: python - from agent_framework import ChatResponseUpdate, TextContent + from agent_framework import ChatResponseUpdate, Content - # Create a response update + # Create a response update with text content update = ChatResponseUpdate( contents=[Content.from_text(text="Hello")], role="assistant", @@ -2331,13 +2075,10 @@ class ChatResponseUpdate(SerializationMixin): ) print(update.text) # "Hello" - # Create update with text shorthand - update = ChatResponseUpdate(text="World!", role="assistant") - # Serialization - to_dict and from_dict update_dict = update.to_dict() # {'type': 'chat_response_update', 'contents': [{'type': 'text', 'text': 'Hello'}], - # 'role': {'type': 'role', 'value': 'assistant'}, 'message_id': 'msg_123'} + # 'role': 'assistant', 'message_id': 'msg_123'} restored_update = ChatResponseUpdate.from_dict(update_dict) print(restored_update.text) # "Hello" @@ -2354,26 +2095,23 @@ class ChatResponseUpdate(SerializationMixin): def __init__( self, *, - contents: Sequence[Content | dict[str, Any]] | None = None, - text: Content | str | None = None, - role: Role | Literal["system", "user", "assistant", "tool"] | dict[str, Any] | None = None, + contents: Sequence[Content] | None = None, + role: RoleLiteral | str | None = None, author_name: str | None = None, response_id: str | None = None, message_id: str | None = None, conversation_id: str | None = None, model_id: str | None = None, created_at: CreatedAtT | None = None, - finish_reason: FinishReason | dict[str, Any] | None = None, + finish_reason: FinishReasonLiteral | str | None = None, additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, - **kwargs: Any, ) -> None: """Initializes a ChatResponseUpdate with the provided parameters. Keyword Args: - contents: Optional list of BaseContent items or dicts to include in the update. - text: Optional text content to include in the update. - role: Optional role of the author of the response update (Role, string, or dict + contents: Optional list of Content items to include in the update. + role: Optional role of the author of the response update (e.g., "user", "assistant"). author_name: Optional name of the author of the response update. 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. @@ -2384,36 +2122,36 @@ class ChatResponseUpdate(SerializationMixin): additional_properties: Optional additional properties associated with the chat response update. raw_representation: Optional raw representation of the chat response update from an underlying implementation. - **kwargs: Any additional keyword arguments. """ - # Handle contents conversion - contents = [] if contents is None else _parse_content_list(contents) + # Handle contents - support dict conversion for from_dict + if contents is None: + self.contents: list[Content] = [] + else: + processed_contents: list[Content] = [] + for c in contents: + if isinstance(c, Content): + processed_contents.append(c) + elif isinstance(c, dict): + processed_contents.append(Content.from_dict(c)) + else: + processed_contents.append(c) + self.contents = processed_contents - if text is not None: - if isinstance(text, str): - text = Content.from_text(text=text) - contents.append(text) + # Handle legacy dict formats for role and finish_reason + if isinstance(role, dict) and "value" in role: + role = role["value"] + if isinstance(finish_reason, dict) and "value" in finish_reason: + finish_reason = finish_reason["value"] - # Handle role conversion - if isinstance(role, dict): - role = Role.from_dict(role) - elif isinstance(role, str): - role = Role(value=role) - - # Handle finish_reason conversion - if isinstance(finish_reason, dict): - finish_reason = FinishReason.from_dict(finish_reason) - - self.contents = list(contents) - self.role = role + self.role: str | None = role self.author_name = author_name self.response_id = response_id self.message_id = message_id self.conversation_id = conversation_id self.model_id = model_id self.created_at = created_at - self.finish_reason = finish_reason + self.finish_reason: str | None = finish_reason self.additional_properties = additional_properties self.raw_representation = raw_representation @@ -2436,13 +2174,18 @@ class AgentResponse(SerializationMixin, Generic[TResponseModel]): A typical response will contain a single message, but may contain multiple messages in scenarios involving function calls, RAG retrievals, or complex logic. + Note: + The `author_name` attribute is available on the `ChatMessage` objects inside `messages`, + not on the `AgentResponse` itself. Use `response.messages[0].author_name` to access + the author name of individual messages. + Examples: .. code-block:: python from agent_framework import AgentResponse, ChatMessage # Create agent response - msg = ChatMessage(role="assistant", text="Task completed successfully.") + msg = ChatMessage("assistant", ["Task completed successfully."]) response = AgentResponse(messages=[msg], response_id="run_123") print(response.text) # "Task completed successfully." @@ -2452,7 +2195,7 @@ class AgentResponse(SerializationMixin, Generic[TResponseModel]): # Combine streaming updates updates = [...] # List of AgentResponseUpdate objects - response = AgentResponse.from_agent_run_response_updates(updates) + response = AgentResponse.from_updates(updates) # Serialization - to_dict and from_dict response_dict = response.to_dict() @@ -2473,60 +2216,53 @@ class AgentResponse(SerializationMixin, Generic[TResponseModel]): def __init__( self, *, - messages: ChatMessage - | list[ChatMessage] - | MutableMapping[str, Any] - | list[MutableMapping[str, Any]] - | None = None, + messages: ChatMessage | Sequence[ChatMessage] | None = None, response_id: str | None = None, + agent_id: str | None = None, created_at: CreatedAtT | None = None, - usage_details: UsageDetails | MutableMapping[str, Any] | None = None, + usage_details: UsageDetails | None = None, value: TResponseModel | None = None, response_format: type[BaseModel] | None = None, raw_representation: Any | None = None, additional_properties: dict[str, Any] | None = None, - **kwargs: Any, ) -> None: """Initialize an AgentResponse. Keyword Args: - messages: The list of chat messages in the response. + messages: A single ChatMessage or sequence of ChatMessage objects to include in the response. response_id: The ID of the chat response. + agent_id: The identifier of the agent that produced this response. Useful in multi-agent + scenarios to track which agent generated the response. created_at: A timestamp for the chat response. usage_details: The usage details for the chat response. value: The structured output of the agent run response, if applicable. response_format: Optional response format for the agent response. additional_properties: Any additional properties associated with the chat response. raw_representation: The raw representation of the chat response from an underlying implementation. - **kwargs: Additional properties to set on the response. """ - processed_messages: list[ChatMessage] = [] - if messages is not None: - if isinstance(messages, ChatMessage): - processed_messages.append(messages) - elif isinstance(messages, list): - for message_data in messages: - if isinstance(message_data, ChatMessage): - processed_messages.append(message_data) - elif isinstance(message_data, MutableMapping): - processed_messages.append(ChatMessage.from_dict(message_data)) - else: - logger.warning(f"Unknown message content: {message_data}") - elif isinstance(messages, MutableMapping): - processed_messages.append(ChatMessage.from_dict(messages)) - - # Convert usage_details from dict if needed (for SerializationMixin support) - # UsageDetails is now a TypedDict, so dict is already the right type - - self.messages = processed_messages + if messages is None: + self.messages: list[ChatMessage] = [] + elif isinstance(messages, ChatMessage): + self.messages = [messages] + else: + # Handle both ChatMessage objects and dicts (for from_dict support) + processed_messages: list[ChatMessage] = [] + for msg in messages: + if isinstance(msg, ChatMessage): + processed_messages.append(msg) + elif isinstance(msg, dict): + processed_messages.append(ChatMessage.from_dict(msg)) + else: + processed_messages.append(msg) + self.messages = processed_messages self.response_id = response_id + self.agent_id = agent_id self.created_at = created_at self.usage_details = usage_details self._value: TResponseModel | None = value self._response_format: type[BaseModel] | None = response_format self._value_parsed: bool = value is not None self.additional_properties = additional_properties or {} - self.additional_properties.update(kwargs or {}) self.raw_representation = raw_representation @property @@ -2567,7 +2303,7 @@ class AgentResponse(SerializationMixin, Generic[TResponseModel]): @overload @classmethod - def from_agent_run_response_updates( + def from_updates( cls: type["AgentResponse[Any]"], updates: Sequence["AgentResponseUpdate"], *, @@ -2576,7 +2312,7 @@ class AgentResponse(SerializationMixin, Generic[TResponseModel]): @overload @classmethod - def from_agent_run_response_updates( + def from_updates( cls: type["AgentResponse[Any]"], updates: Sequence["AgentResponseUpdate"], *, @@ -2584,7 +2320,7 @@ class AgentResponse(SerializationMixin, Generic[TResponseModel]): ) -> "AgentResponse[Any]": ... @classmethod - def from_agent_run_response_updates( + def from_updates( cls: type[TAgentRunResponse], updates: Sequence["AgentResponseUpdate"], *, @@ -2602,8 +2338,6 @@ class AgentResponse(SerializationMixin, Generic[TResponseModel]): for update in updates: _process_update(msg, update) _finalize_response(msg) - if output_format_type: - msg.try_parse_value(output_format_type) return msg @overload @@ -2643,54 +2377,11 @@ class AgentResponse(SerializationMixin, Generic[TResponseModel]): async for update in updates: _process_update(msg, update) _finalize_response(msg) - if output_format_type: - msg.try_parse_value(output_format_type) return msg def __str__(self) -> str: return self.text - @overload - def try_parse_value(self, output_format_type: type[TResponseModelT]) -> TResponseModelT | None: ... - - @overload - def try_parse_value(self, output_format_type: None = None) -> TResponseModel | None: ... - - def try_parse_value(self, output_format_type: type[BaseModel] | None = None) -> BaseModel | None: - """Try to parse the text into a typed value. - - This is the safe alternative when you need to parse the response text into a typed value. - Returns the parsed value on success, or None on failure. - - Args: - output_format_type: The Pydantic model type to parse into. - If None, uses the response_format from initialization. - - Returns: - The parsed value as the specified type, or None if parsing fails. - """ - format_type = output_format_type or self._response_format - if format_type is None or not (isinstance(format_type, type) and issubclass(format_type, BaseModel)): - return None - - # Cache the result unless a different schema than the configured response_format is requested. - # This prevents calls with a different schema from polluting the cached value. - use_cache = ( - self._response_format is None or output_format_type is None or output_format_type is self._response_format - ) - - if use_cache and self._value_parsed and self._value is not None: - return self._value # type: ignore[return-value, no-any-return] - try: - parsed_value = format_type.model_validate_json(self.text) # type: ignore[reportUnknownMemberType] - if use_cache: - self._value = cast(TResponseModel, parsed_value) - self._value_parsed = True - return parsed_value # type: ignore[return-value] - except ValidationError as ex: - logger.warning("Failed to parse value from agent run response text: %s", ex) - return None - # region AgentResponseUpdate @@ -2698,6 +2389,20 @@ class AgentResponse(SerializationMixin, Generic[TResponseModel]): class AgentResponseUpdate(SerializationMixin): """Represents a single streaming response chunk from an Agent. + Attributes: + contents: The content items in this update. + role: The role of the author of the response update. + author_name: The name of the author of the response update. In multi-agent scenarios, + this identifies which agent generated this update. When updates are combined into + an `AgentResponse`, the `author_name` is propagated to the resulting `ChatMessage` objects. + agent_id: The identifier of the agent that produced this update. Useful in multi-agent + scenarios to track which agent generated specific parts of the response. + response_id: The ID of the response of which this update is a part. + message_id: The ID of the message of which this update is a part. + created_at: A timestamp for the response update. + additional_properties: Any additional properties associated with the update. + raw_representation: The raw representation from an underlying implementation. + Examples: .. code-block:: python @@ -2717,7 +2422,7 @@ class AgentResponseUpdate(SerializationMixin): # Serialization - to_dict and from_dict update_dict = update.to_dict() # {'type': 'agent_response_update', 'contents': [{'type': 'text', 'text': 'Processing...'}], - # 'role': {'type': 'role', 'value': 'assistant'}, 'response_id': 'run_123'} + # 'role': 'assistant', 'response_id': 'run_123'} restored_update = AgentResponseUpdate.from_dict(update_dict) print(restored_update.response_id) # "run_123" @@ -2733,48 +2438,52 @@ class AgentResponseUpdate(SerializationMixin): def __init__( self, *, - contents: Sequence[Content | MutableMapping[str, Any]] | None = None, - text: Content | str | None = None, - role: Role | MutableMapping[str, Any] | str | None = None, + contents: Sequence[Content] | None = None, + role: RoleLiteral | str | None = None, author_name: str | None = None, + agent_id: str | None = None, response_id: str | None = None, message_id: str | None = None, created_at: CreatedAtT | None = None, - additional_properties: MutableMapping[str, Any] | None = None, + additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, - **kwargs: Any, ) -> None: """Initialize an AgentResponseUpdate. Keyword Args: - contents: Optional list of BaseContent items or dicts to include in the update. - text: Optional text content of the update. - role: The role of the author of the response update (Role, string, or dict - author_name: Optional name of the author of the response update. + contents: Optional list of Content items to include in the update. + role: The role of the author of the response update (e.g., "user", "assistant"). + author_name: Optional name of the author of the response update. Used in multi-agent + scenarios to identify which agent generated this update. + agent_id: Optional identifier of the agent that produced this update. 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. created_at: Optional timestamp for the chat response update. additional_properties: Optional additional properties associated with the chat response update. raw_representation: Optional raw representation of the chat response update. - kwargs: will be combined with additional_properties if provided. """ - parsed_contents: list[Content] = [] if contents is None else _parse_content_list(contents) + # Handle contents - support dict conversion for from_dict + if contents is None: + self.contents: list[Content] = [] + else: + processed_contents: list[Content] = [] + for c in contents: + if isinstance(c, Content): + processed_contents.append(c) + elif isinstance(c, dict): + processed_contents.append(Content.from_dict(c)) + else: + processed_contents.append(c) + self.contents = processed_contents - if text is not None: - if isinstance(text, str): - text = Content.from_text(text=text) - parsed_contents.append(text) + # Handle legacy dict format for role + if isinstance(role, dict) and "value" in role: + role = role["value"] - # Convert role from dict if needed (for SerializationMixin support) - if isinstance(role, MutableMapping): - role = Role.from_dict(role) - elif isinstance(role, str): - role = Role(value=role) - - self.contents = parsed_contents - self.role = role + self.role: str | None = role self.author_name = author_name + self.agent_id = agent_id self.response_id = response_id self.message_id = message_id self.created_at = created_at diff --git a/python/packages/core/agent_framework/_workflows/_agent.py b/python/packages/core/agent_framework/_workflows/_agent.py index 1543ed7db6..28482820a0 100644 --- a/python/packages/core/agent_framework/_workflows/_agent.py +++ b/python/packages/core/agent_framework/_workflows/_agent.py @@ -16,7 +16,6 @@ from agent_framework import ( BaseAgent, ChatMessage, Content, - Role, UsageDetails, ) @@ -344,7 +343,7 @@ class WorkflowAgent(BaseAgent): return None return AgentResponseUpdate( contents=contents, - role=Role.ASSISTANT, + role="assistant", author_name=executor_id, response_id=response_id, message_id=str(uuid.uuid4()), @@ -370,7 +369,7 @@ class WorkflowAgent(BaseAgent): ) return AgentResponseUpdate( contents=[function_call, approval_request], - role=Role.ASSISTANT, + role="assistant", author_name=self.name, response_id=response_id, message_id=str(uuid.uuid4()), @@ -453,7 +452,7 @@ class WorkflowAgent(BaseAgent): - Group updates by response_id; within each response_id, group by message_id and keep a dangling bucket for updates without message_id. - Convert each group (per message and dangling) into an intermediate AgentResponse via - AgentResponse.from_agent_run_response_updates, then sort by created_at and merge. + AgentResponse.from_updates, then sort by created_at and merge. - Append messages from updates without any response_id at the end (global dangling), while aggregating metadata. Args: @@ -548,9 +547,9 @@ class WorkflowAgent(BaseAgent): per_message_responses: list[AgentResponse] = [] for _, msg_updates in by_msg.items(): if msg_updates: - per_message_responses.append(AgentResponse.from_agent_run_response_updates(msg_updates)) + per_message_responses.append(AgentResponse.from_updates(msg_updates)) if dangling: - per_message_responses.append(AgentResponse.from_agent_run_response_updates(dangling)) + per_message_responses.append(AgentResponse.from_updates(dangling)) per_message_responses.sort(key=lambda r: _parse_dt(r.created_at)) @@ -584,7 +583,7 @@ class WorkflowAgent(BaseAgent): # These are updates that couldn't be associated with any response_id # (e.g., orphan FunctionResultContent with no matching FunctionCallContent) if global_dangling: - flattened = AgentResponse.from_agent_run_response_updates(global_dangling) + flattened = AgentResponse.from_updates(global_dangling) final_messages.extend(flattened.messages) if flattened.usage_details: merged_usage = add_usage_details(merged_usage, flattened.usage_details) # type: ignore[arg-type] diff --git a/python/packages/core/agent_framework/_workflows/_agent_executor.py b/python/packages/core/agent_framework/_workflows/_agent_executor.py index 80bd4aba43..9849d351d1 100644 --- a/python/packages/core/agent_framework/_workflows/_agent_executor.py +++ b/python/packages/core/agent_framework/_workflows/_agent_executor.py @@ -198,7 +198,7 @@ class AgentExecutor(Executor): if not self._pending_agent_requests: # All pending requests have been resolved; resume agent execution - self._cache = normalize_messages_input(ChatMessage(role="user", contents=self._pending_responses_to_agent)) + self._cache = normalize_messages_input(ChatMessage("user", self._pending_responses_to_agent)) self._pending_responses_to_agent.clear() await self._run_agent_and_emit(ctx) @@ -378,12 +378,12 @@ class AgentExecutor(Executor): # Build the final AgentResponse from the collected updates if isinstance(self._agent, ChatAgent): response_format = self._agent.default_options.get("response_format") - response = AgentResponse.from_agent_run_response_updates( + response = AgentResponse.from_updates( updates, output_format_type=response_format, ) else: - response = AgentResponse.from_agent_run_response_updates(updates) + response = AgentResponse.from_updates(updates) # Handle any user input requests after the streaming completes if user_input_requests: diff --git a/python/packages/core/agent_framework/_workflows/_base_group_chat_orchestrator.py b/python/packages/core/agent_framework/_workflows/_base_group_chat_orchestrator.py index e3cc4bc7d2..4c4d69f7bd 100644 --- a/python/packages/core/agent_framework/_workflows/_base_group_chat_orchestrator.py +++ b/python/packages/core/agent_framework/_workflows/_base_group_chat_orchestrator.py @@ -14,7 +14,7 @@ from typing import Any, ClassVar, TypeAlias from typing_extensions import Never -from .._types import ChatMessage, Role +from .._types import ChatMessage from ._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse from ._events import WorkflowEvent from ._executor import Executor, handler @@ -214,7 +214,7 @@ class BaseGroupChatOrchestrator(Executor, ABC): Usage: workflow.run("Write a blog post about AI agents") """ - await self._handle_messages([ChatMessage(role=Role.USER, text=task)], ctx) + await self._handle_messages([ChatMessage("user", [task])], ctx) @handler async def handle_message( @@ -231,7 +231,7 @@ class BaseGroupChatOrchestrator(Executor, ABC): ctx: Workflow context Usage: - workflow.run(ChatMessage(role=Role.USER, text="Write a blog post about AI agents")) + workflow.run(ChatMessage("user", ["Write a blog post about AI agents"])) """ await self._handle_messages([task], ctx) @@ -250,8 +250,8 @@ class BaseGroupChatOrchestrator(Executor, ABC): ctx: Workflow context Usage: workflow.run([ - ChatMessage(role=Role.USER, text="Write a blog post about AI agents"), - ChatMessage(role=Role.USER, text="Make it engaging and informative.") + ChatMessage("user", ["Write a blog post about AI agents"]), + ChatMessage("user", ["Make it engaging and informative."]) ]) """ if not task: @@ -401,7 +401,7 @@ class BaseGroupChatOrchestrator(Executor, ABC): Returns: ChatMessage with completion content """ - return ChatMessage(role=Role.ASSISTANT, text=message, author_name=self._name) + return ChatMessage("assistant", [message], author_name=self._name) # Participant routing (shared across all patterns) @@ -465,7 +465,7 @@ class BaseGroupChatOrchestrator(Executor, ABC): # AgentExecutors receive simple message list messages: list[ChatMessage] = [] if additional_instruction: - messages.append(ChatMessage(role=Role.USER, text=additional_instruction)) + messages.append(ChatMessage("user", [additional_instruction])) request = AgentExecutorRequest(messages=messages, should_respond=True) await ctx.send_message(request, target_id=target) await ctx.add_event( diff --git a/python/packages/core/agent_framework/_workflows/_concurrent.py b/python/packages/core/agent_framework/_workflows/_concurrent.py index 4204c8cd6d..afa0ef99e7 100644 --- a/python/packages/core/agent_framework/_workflows/_concurrent.py +++ b/python/packages/core/agent_framework/_workflows/_concurrent.py @@ -8,7 +8,7 @@ from typing import Any from typing_extensions import Never -from agent_framework import AgentProtocol, ChatMessage, Role +from agent_framework import AgentProtocol, ChatMessage from ._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse from ._agent_utils import resolve_agent_id @@ -91,16 +91,13 @@ class _AggregateAgentConversations(Executor): logger.error("Concurrent aggregator received empty results list") raise ValueError("Aggregation failed: no results provided") - def _is_role(msg: Any, role: Role) -> bool: + def _is_role(msg: Any, role: str) -> bool: r = getattr(msg, "role", None) if r is None: return False # Normalize both r and role to lowercase strings for comparison r_str = str(r).lower() if isinstance(r, str) or hasattr(r, "__str__") else r - role_str = getattr(role, "value", None) - if role_str is None: - role_str = str(role) - role_str = role_str.lower() + role_str = str(role).lower() return r_str == role_str prompt_message: ChatMessage | None = None @@ -117,14 +114,14 @@ class _AggregateAgentConversations(Executor): # Capture a single user prompt (first encountered across any conversation) if prompt_message is None: - found_user = next((m for m in conv if _is_role(m, Role.USER)), None) + found_user = next((m for m in conv if _is_role(m, "user")), None) if found_user is not None: prompt_message = found_user # Pick the final assistant message from the response; fallback to conversation search - final_assistant = next((m for m in reversed(resp_messages) if _is_role(m, Role.ASSISTANT)), None) + final_assistant = next((m for m in reversed(resp_messages) if _is_role(m, "assistant")), None) if final_assistant is None: - final_assistant = next((m for m in reversed(conv) if _is_role(m, Role.ASSISTANT)), None) + final_assistant = next((m for m in reversed(conv) if _is_role(m, "assistant")), None) if final_assistant is not None: assistant_replies.append(final_assistant) diff --git a/python/packages/core/agent_framework/_workflows/_conversation_state.py b/python/packages/core/agent_framework/_workflows/_conversation_state.py index 8c21513f6c..084cf9cda3 100644 --- a/python/packages/core/agent_framework/_workflows/_conversation_state.py +++ b/python/packages/core/agent_framework/_workflows/_conversation_state.py @@ -3,7 +3,7 @@ from collections.abc import Iterable from typing import Any, cast -from agent_framework import ChatMessage, Role +from agent_framework import ChatMessage from ._checkpoint_encoding import decode_checkpoint_value, encode_checkpoint_value @@ -40,15 +40,13 @@ def decode_chat_messages(payload: Iterable[dict[str, Any]]) -> list[ChatMessage] continue role_value = decode_checkpoint_value(item.get("role")) - if isinstance(role_value, Role): + if isinstance(role_value, str): role = role_value - elif isinstance(role_value, dict): - role_dict = cast(dict[str, Any], role_value) - role = Role.from_dict(role_dict) - elif isinstance(role_value, str): - role = Role(value=role_value) + elif isinstance(role_value, dict) and "value" in role_value: + # Handle legacy serialization format + role = role_value["value"] else: - role = Role.ASSISTANT + role = "assistant" contents_field = item.get("contents", []) contents: list[Any] = [] diff --git a/python/packages/core/agent_framework/_workflows/_group_chat.py b/python/packages/core/agent_framework/_workflows/_group_chat.py index 3f92d9ebf2..4b25ca1b77 100644 --- a/python/packages/core/agent_framework/_workflows/_group_chat.py +++ b/python/packages/core/agent_framework/_workflows/_group_chat.py @@ -31,7 +31,7 @@ from typing_extensions import Never from .._agents import AgentProtocol, ChatAgent from .._threads import AgentThread -from .._types import ChatMessage, Role +from .._types import ChatMessage from ._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse from ._agent_utils import resolve_agent_id from ._base_group_chat_orchestrator import ( @@ -424,7 +424,7 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator): ]) ) # Prepend instruction as system message - current_conversation.append(ChatMessage(role=Role.USER, text=instruction)) + current_conversation.append(ChatMessage("user", [instruction])) retry_attempts = self._retry_attempts while True: @@ -439,7 +439,7 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator): # We don't need the full conversation since the thread should maintain history current_conversation = [ ChatMessage( - role=Role.USER, + role="user", text=f"Your input could not be parsed due to an error: {ex}. Please try again.", ) ] @@ -782,7 +782,7 @@ class GroupChatBuilder: def stop_after_two_calls(conversation: list[ChatMessage]) -> bool: - calls = sum(1 for msg in conversation if msg.role == Role.ASSISTANT and msg.author_name == "specialist") + calls = sum(1 for msg in conversation if msg.role == "assistant" and msg.author_name == "specialist") return calls >= 2 diff --git a/python/packages/core/agent_framework/_workflows/_handoff.py b/python/packages/core/agent_framework/_workflows/_handoff.py index e529e09111..875fdc36c8 100644 --- a/python/packages/core/agent_framework/_workflows/_handoff.py +++ b/python/packages/core/agent_framework/_workflows/_handoff.py @@ -42,7 +42,7 @@ from .._agents import AgentProtocol, ChatAgent from .._middleware import FunctionInvocationContext, FunctionMiddleware from .._threads import AgentThread from .._tools import FunctionTool, tool -from .._types import AgentResponse, ChatMessage, Role +from .._types import AgentResponse, ChatMessage from ._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse from ._agent_utils import resolve_agent_id from ._base_group_chat_orchestrator import TerminationCondition @@ -162,7 +162,7 @@ class HandoffAgentUserRequest: """Create a HandoffAgentUserRequest from a simple text response.""" messages: list[ChatMessage] = [] if isinstance(response, str): - messages.append(ChatMessage(role=Role.USER, text=response)) + messages.append(ChatMessage("user", [response])) elif isinstance(response, ChatMessage): messages.append(response) elif isinstance(response, list): @@ -170,7 +170,7 @@ class HandoffAgentUserRequest: if isinstance(item, ChatMessage): messages.append(item) elif isinstance(item, str): - messages.append(ChatMessage(role=Role.USER, text=item)) + messages.append(ChatMessage("user", [item])) else: raise TypeError("List items must be either str or ChatMessage instances") else: @@ -427,7 +427,7 @@ class HandoffAgentExecutor(AgentExecutor): # or a termination condition is met. # This allows the agent to perform long-running tasks without returning control # to the coordinator or user prematurely. - self._cache.extend([ChatMessage(role=Role.USER, text=self._autonomous_mode_prompt)]) + self._cache.extend([ChatMessage("user", [self._autonomous_mode_prompt])]) self._autonomous_mode_turns += 1 await self._run_agent_and_emit(ctx) else: diff --git a/python/packages/core/agent_framework/_workflows/_magentic.py b/python/packages/core/agent_framework/_workflows/_magentic.py index eff87fd5f0..221f16bae6 100644 --- a/python/packages/core/agent_framework/_workflows/_magentic.py +++ b/python/packages/core/agent_framework/_workflows/_magentic.py @@ -18,7 +18,6 @@ from agent_framework import ( AgentProtocol, AgentResponse, ChatMessage, - Role, ) from ._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse @@ -607,14 +606,14 @@ class StandardMagenticManager(MagenticManagerBase): # Gather facts facts_user = ChatMessage( - role=Role.USER, + role="user", text=self.task_ledger_facts_prompt.format(task=magentic_context.task), ) facts_msg = await self._complete([*magentic_context.chat_history, facts_user]) # Create plan plan_user = ChatMessage( - role=Role.USER, + role="user", text=self.task_ledger_plan_prompt.format(team=team_text), ) plan_msg = await self._complete([*magentic_context.chat_history, facts_user, facts_msg, plan_user]) @@ -632,7 +631,7 @@ class StandardMagenticManager(MagenticManagerBase): facts=facts_msg.text, plan=plan_msg.text, ) - return ChatMessage(role=Role.ASSISTANT, text=combined, author_name=MAGENTIC_MANAGER_NAME) + return ChatMessage("assistant", [combined], author_name=MAGENTIC_MANAGER_NAME) async def replan(self, magentic_context: MagenticContext) -> ChatMessage: """Update facts and plan when stalling or looping has been detected.""" @@ -643,17 +642,19 @@ class StandardMagenticManager(MagenticManagerBase): # Update facts facts_update_user = ChatMessage( - role=Role.USER, - text=self.task_ledger_facts_update_prompt.format( - task=magentic_context.task, old_facts=self.task_ledger.facts.text - ), + "user", + [ + self.task_ledger_facts_update_prompt.format( + task=magentic_context.task, old_facts=self.task_ledger.facts.text + ) + ], ) updated_facts = await self._complete([*magentic_context.chat_history, facts_update_user]) # Update plan plan_update_user = ChatMessage( - role=Role.USER, - text=self.task_ledger_plan_update_prompt.format(team=team_text), + "user", + [self.task_ledger_plan_update_prompt.format(team=team_text)], ) updated_plan = await self._complete([ *magentic_context.chat_history, @@ -675,7 +676,7 @@ class StandardMagenticManager(MagenticManagerBase): facts=updated_facts.text, plan=updated_plan.text, ) - return ChatMessage(role=Role.ASSISTANT, text=combined, author_name=MAGENTIC_MANAGER_NAME) + return ChatMessage("assistant", [combined], author_name=MAGENTIC_MANAGER_NAME) async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: """Use the model to produce a JSON progress ledger based on the conversation so far. @@ -695,7 +696,7 @@ class StandardMagenticManager(MagenticManagerBase): team=team_text, names=names_csv, ) - user_message = ChatMessage(role=Role.USER, text=prompt) + user_message = ChatMessage("user", [prompt]) # Include full context to help the model decide current stage, with small retry loop attempts = 0 @@ -722,11 +723,11 @@ class StandardMagenticManager(MagenticManagerBase): async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: """Ask the model to produce the final answer addressed to the user.""" prompt = self.final_answer_prompt.format(task=magentic_context.task) - user_message = ChatMessage(role=Role.USER, text=prompt) + user_message = ChatMessage("user", [prompt]) response = await self._complete([*magentic_context.chat_history, user_message]) # Ensure role is assistant return ChatMessage( - role=Role.ASSISTANT, + role="assistant", text=response.text, author_name=response.author_name or MAGENTIC_MANAGER_NAME, ) @@ -812,11 +813,11 @@ class MagenticPlanReviewResponse: def revise(feedback: str | list[str] | ChatMessage | list[ChatMessage]) -> "MagenticPlanReviewResponse": """Create a revision response with feedback.""" if isinstance(feedback, str): - feedback = [ChatMessage(role=Role.USER, text=feedback)] + feedback = [ChatMessage("user", [feedback])] elif isinstance(feedback, ChatMessage): feedback = [feedback] elif isinstance(feedback, list): - feedback = [ChatMessage(role=Role.USER, text=item) if isinstance(item, str) else item for item in feedback] + feedback = [ChatMessage("user", [item]) if isinstance(item, str) else item for item in feedback] return MagenticPlanReviewResponse(review=feedback) @@ -1118,7 +1119,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): # Add instruction to conversation (assistant guidance) instruction_msg = ChatMessage( - role=Role.ASSISTANT, + role="assistant", text=str(instruction), author_name=MAGENTIC_MANAGER_NAME, ) @@ -1227,7 +1228,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): await ctx.yield_output([ *self._magentic_context.chat_history, ChatMessage( - role=Role.ASSISTANT, + role="assistant", text=f"Workflow terminated due to reaching maximum {limit_type} count.", author_name=MAGENTIC_MANAGER_NAME, ), @@ -1810,7 +1811,7 @@ class MagenticBuilder: class MyManager(MagenticManagerBase): async def plan(self, context: MagenticContext) -> ChatMessage: # Custom planning logic - return ChatMessage(role=Role.ASSISTANT, text="...") + return ChatMessage("assistant", ["..."]) manager = MyManager() diff --git a/python/packages/core/agent_framework/_workflows/_message_utils.py b/python/packages/core/agent_framework/_workflows/_message_utils.py index ad4a9b55f6..78a2f3f626 100644 --- a/python/packages/core/agent_framework/_workflows/_message_utils.py +++ b/python/packages/core/agent_framework/_workflows/_message_utils.py @@ -4,7 +4,7 @@ from collections.abc import Sequence -from agent_framework import ChatMessage, Role +from agent_framework import ChatMessage def normalize_messages_input( @@ -22,7 +22,7 @@ def normalize_messages_input( return [] if isinstance(messages, str): - return [ChatMessage(role=Role.USER, text=messages)] + return [ChatMessage("user", [messages])] if isinstance(messages, ChatMessage): return [messages] @@ -30,7 +30,7 @@ def normalize_messages_input( normalized: list[ChatMessage] = [] for item in messages: if isinstance(item, str): - normalized.append(ChatMessage(role=Role.USER, text=item)) + normalized.append(ChatMessage("user", [item])) elif isinstance(item, ChatMessage): normalized.append(item) else: diff --git a/python/packages/core/agent_framework/_workflows/_orchestration_request_info.py b/python/packages/core/agent_framework/_workflows/_orchestration_request_info.py index dc1e282a12..cc4b1ed15d 100644 --- a/python/packages/core/agent_framework/_workflows/_orchestration_request_info.py +++ b/python/packages/core/agent_framework/_workflows/_orchestration_request_info.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from .._agents import AgentProtocol -from .._types import ChatMessage, Role +from .._types import ChatMessage from ._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse from ._agent_utils import resolve_agent_id from ._executor import Executor, handler @@ -72,7 +72,7 @@ class AgentRequestInfoResponse: Returns: AgentRequestInfoResponse instance. """ - return AgentRequestInfoResponse(messages=[ChatMessage(role=Role.USER, text=text) for text in texts]) + return AgentRequestInfoResponse(messages=[ChatMessage("user", [text]) for text in texts]) @staticmethod def approve() -> "AgentRequestInfoResponse": diff --git a/python/packages/core/agent_framework/_workflows/_orchestrator_helpers.py b/python/packages/core/agent_framework/_workflows/_orchestrator_helpers.py index 82f6532ea2..0d74f53c39 100644 --- a/python/packages/core/agent_framework/_workflows/_orchestrator_helpers.py +++ b/python/packages/core/agent_framework/_workflows/_orchestrator_helpers.py @@ -8,7 +8,7 @@ No inheritance required - just import and call. import logging -from .._types import ChatMessage, Role +from .._types import ChatMessage logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ def clean_conversation_for_handoff(conversation: list[ChatMessage]) -> list[Chat Removes: - function_approval_request and function_call from assistant messages - - Tool response messages (Role.TOOL) + - Tool response messages (role="tool") - Messages with only tool calls and no text Preserves: @@ -40,7 +40,7 @@ def clean_conversation_for_handoff(conversation: list[ChatMessage]) -> list[Chat cleaned: list[ChatMessage] = [] for msg in conversation: # Skip tool response messages entirely - if msg.role == Role.TOOL: + if msg.role == "tool": continue # Check for tool-related content @@ -85,11 +85,11 @@ def create_completion_message( reason: Reason for completion (for default text generation) Returns: - ChatMessage with ASSISTANT role + ChatMessage with assistant role """ message_text = text or f"Conversation {reason}." return ChatMessage( - role=Role.ASSISTANT, - text=message_text, + "assistant", + [message_text], author_name=author_name, ) diff --git a/python/packages/core/agent_framework/_workflows/_workflow.py b/python/packages/core/agent_framework/_workflows/_workflow.py index bd14dc6bcc..dfd0331282 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow.py +++ b/python/packages/core/agent_framework/_workflows/_workflow.py @@ -851,7 +851,7 @@ class Workflow(DictConvertible): The returned agent converts standard agent inputs (strings, ChatMessage, or lists of these) into a list[ChatMessage] that is passed to the workflow's start executor. This conversion happens in WorkflowAgent._normalize_messages() which transforms: - - str -> [ChatMessage(role=USER, text=str)] + - str -> [ChatMessage(USER, [str])] - ChatMessage -> [ChatMessage] - list[str | ChatMessage] -> list[ChatMessage] (with string elements converted) diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index 2d294daddd..8e2d736c42 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -41,7 +41,6 @@ if TYPE_CHECKING: # pragma: no cover ChatResponse, ChatResponseUpdate, Content, - FinishReason, ) __all__ = [ @@ -1211,7 +1210,7 @@ def _trace_get_streaming_response( duration = (end_time_stamp or perf_counter()) - start_time_stamp from ._types import ChatResponse - response = ChatResponse.from_chat_response_updates(all_updates) + response = ChatResponse.from_updates(all_updates) attributes = _get_response_attributes(attributes, response, duration=duration) _capture_response( span=span, @@ -1450,7 +1449,7 @@ def _trace_agent_run_stream( capture_exception(span=span, exception=exception, timestamp=time_ns()) raise else: - response = AgentResponse.from_agent_run_response_updates(all_updates) + response = AgentResponse.from_updates(all_updates) attributes = _get_response_attributes(attributes, response, capture_usage=capture_usage) _capture_response(span=span, attributes=attributes) if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages: @@ -1715,7 +1714,7 @@ def _capture_messages( messages: "str | ChatMessage | list[str] | list[ChatMessage]", system_instructions: str | list[str] | None = None, output: bool = False, - finish_reason: "FinishReason | None" = None, + finish_reason: str | None = None, ) -> None: """Log messages with extra information.""" from ._types import prepare_messages @@ -1730,13 +1729,13 @@ def _capture_messages( logger.info( otel_message, extra={ - OtelAttr.EVENT_NAME: OtelAttr.CHOICE if output else ROLE_EVENT_MAP.get(message.role.value), + OtelAttr.EVENT_NAME: OtelAttr.CHOICE if output else ROLE_EVENT_MAP.get(message.role), OtelAttr.PROVIDER_NAME: provider_name, ChatMessageListTimestampFilter.INDEX_KEY: index, }, ) if finish_reason: - otel_messages[-1]["finish_reason"] = FINISH_REASON_MAP[finish_reason.value] + otel_messages[-1]["finish_reason"] = FINISH_REASON_MAP[finish_reason] span.set_attribute(OtelAttr.OUTPUT_MESSAGES if output else OtelAttr.INPUT_MESSAGES, json.dumps(otel_messages)) if system_instructions: if not isinstance(system_instructions, list): @@ -1747,7 +1746,7 @@ def _capture_messages( def _to_otel_message(message: "ChatMessage") -> dict[str, Any]: """Create a otel representation of a message.""" - return {"role": message.role.value, "parts": [_to_otel_part(content) for content in message.contents]} + return {"role": message.role, "parts": [_to_otel_part(content) for content in message.contents]} def _to_otel_part(content: "Content") -> dict[str, Any] | None: @@ -1806,7 +1805,9 @@ def _get_response_attributes( getattr(response.raw_representation, "finish_reason", None) if response.raw_representation else None ) if finish_reason: - attributes[OtelAttr.FINISH_REASONS] = json.dumps([finish_reason.value]) + # Handle both string and object with .value attribute for backward compatibility + finish_reason_str = finish_reason.value if hasattr(finish_reason, "value") else finish_reason + attributes[OtelAttr.FINISH_REASONS] = json.dumps([finish_reason_str]) if model_id := getattr(response, "model_id", None): attributes[SpanAttributes.LLM_RESPONSE_MODEL] = model_id if capture_usage and (usage := response.usage_details): diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py index 22852bea53..f653e22d42 100644 --- a/python/packages/core/agent_framework/openai/_assistants_client.py +++ b/python/packages/core/agent_framework/openai/_assistants_client.py @@ -41,7 +41,6 @@ from .._types import ( ChatResponse, ChatResponseUpdate, Content, - Role, UsageDetails, prepare_function_call_results, ) @@ -345,7 +344,7 @@ class OpenAIAssistantsClient( options: dict[str, Any], **kwargs: Any, ) -> ChatResponse: - return await ChatResponse.from_chat_response_generator( + return await ChatResponse.from_update_generator( updates=self._inner_get_streaming_response(messages=messages, options=options, **kwargs), output_format_type=options.get("response_format"), ) @@ -479,19 +478,19 @@ class OpenAIAssistantsClient( message_id=response_id, raw_representation=response.data, response_id=response_id, - role=Role.ASSISTANT, + role="assistant", ) elif response.event == "thread.run.step.created" and isinstance(response.data, RunStep): response_id = response.data.run_id elif response.event == "thread.message.delta" and isinstance(response.data, MessageDeltaEvent): delta = response.data.delta - role = Role.USER if delta.role == "user" else Role.ASSISTANT + role = "user" if delta.role == "user" else "assistant" for delta_block in delta.content or []: if isinstance(delta_block, TextDeltaBlock) and delta_block.text and delta_block.text.value: yield ChatResponseUpdate( role=role, - text=delta_block.text.value, + contents=[Content.from_text(text=delta_block.text.value)], conversation_id=thread_id, message_id=response_id, raw_representation=response.data, @@ -501,7 +500,7 @@ class OpenAIAssistantsClient( contents = self._parse_function_calls_from_assistants(response.data, response_id) if contents: yield ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=contents, conversation_id=thread_id, message_id=response_id, @@ -522,7 +521,7 @@ class OpenAIAssistantsClient( ) ) yield ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=[usage_content], conversation_id=thread_id, message_id=response_id, @@ -536,7 +535,7 @@ class OpenAIAssistantsClient( message_id=response_id, raw_representation=response.data, response_id=response_id, - role=Role.ASSISTANT, + role="assistant", ) def _parse_function_calls_from_assistants(self, event_data: Run, response_id: str | None) -> list[Content]: @@ -670,7 +669,7 @@ class OpenAIAssistantsClient( # since there is no such message roles in OpenAI Assistants. # All other messages are added 1:1. for chat_message in messages: - if chat_message.role.value in ["system", "developer"]: + if chat_message.role in ["system", "developer"]: for text_content in [content for content in chat_message.contents if content.type == "text"]: text = getattr(text_content, "text", None) if text: @@ -697,7 +696,7 @@ class OpenAIAssistantsClient( additional_messages = [] additional_messages.append( AdditionalMessage( - role="assistant" if chat_message.role == Role.ASSISTANT else "user", + role="assistant" if chat_message.role == "assistant" else "user", content=message_contents, ) ) diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index e70b4790f6..1a0529f50f 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -26,8 +26,6 @@ from .._types import ( ChatResponse, ChatResponseUpdate, Content, - FinishReason, - Role, UsageDetails, prepare_function_call_results, ) @@ -285,11 +283,11 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener """Parse a response from OpenAI into a ChatResponse.""" response_metadata = self._get_metadata_from_chat_response(response) messages: list[ChatMessage] = [] - finish_reason: FinishReason | None = None + finish_reason: str | None = None for choice in response.choices: response_metadata.update(self._get_metadata_from_chat_choice(choice)) if choice.finish_reason: - finish_reason = FinishReason(value=choice.finish_reason) + finish_reason = choice.finish_reason contents: list[Content] = [] if text_content := self._parse_text_from_openai(choice): contents.append(text_content) @@ -297,7 +295,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener contents.extend(parsed_tool_calls) if reasoning_details := getattr(choice.message, "reasoning_details", None): contents.append(Content.from_text_reasoning(protected_data=json.dumps(reasoning_details))) - messages.append(ChatMessage(role="assistant", contents=contents)) + messages.append(ChatMessage("assistant", contents)) return ChatResponse( response_id=response.id, created_at=datetime.fromtimestamp(response.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), @@ -317,7 +315,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener chunk_metadata = self._get_metadata_from_streaming_chat_response(chunk) if chunk.usage: return ChatResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_usage( usage_details=self._parse_usage_from_openai(chunk.usage), raw_representation=chunk @@ -329,12 +327,12 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener message_id=chunk.id, ) contents: list[Content] = [] - finish_reason: FinishReason | None = None + finish_reason: str | None = None for choice in chunk.choices: chunk_metadata.update(self._get_metadata_from_chat_choice(choice)) contents.extend(self._parse_tool_calls_from_openai(choice)) if choice.finish_reason: - finish_reason = FinishReason(value=choice.finish_reason) + finish_reason = choice.finish_reason if text_content := self._parse_text_from_openai(choice): contents.append(text_content) @@ -343,7 +341,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener return ChatResponseUpdate( created_at=datetime.fromtimestamp(chunk.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), contents=contents, - role=Role.ASSISTANT, + role="assistant", model_id=chunk.model, additional_properties=chunk_metadata, finish_reason=finish_reason, @@ -430,7 +428,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener Allowing customization of the key names for role/author, and optionally overriding the role. - Role.TOOL messages need to be formatted different than system/user/assistant messages: + "tool" messages need to be formatted different than system/user/assistant messages: They require a "tool_call_id" and (function) "name" key, and the "metadata" key should be removed. The "encoding" key should also be removed. @@ -459,9 +457,9 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener continue args: dict[str, Any] = { - "role": message.role.value if isinstance(message.role, Role) else message.role, + "role": message.role, } - if message.author_name and message.role != Role.TOOL: + if message.author_name and message.role != "tool": args["name"] = message.author_name if "reasoning_details" in message.additional_properties and ( details := message.additional_properties["reasoning_details"] diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index 9a3436e5ce..125ff1cd20 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -54,7 +54,6 @@ from .._types import ( ChatResponse, ChatResponseUpdate, Content, - Role, TextSpanRegion, UsageDetails, detect_media_type_from_base64, @@ -610,7 +609,7 @@ class OpenAIBaseResponsesClient( Allowing customization of the key names for role/author, and optionally overriding the role. - Role.TOOL messages need to be formatted different than system/user/assistant messages: + "tool" messages need to be formatted different than system/user/assistant messages: They require a "tool_call_id" and (function) "name" key, and the "metadata" key should be removed. The "encoding" key should also be removed. @@ -643,7 +642,7 @@ class OpenAIBaseResponsesClient( """Prepare a chat message for the OpenAI Responses API format.""" all_messages: list[dict[str, Any]] = [] args: dict[str, Any] = { - "role": message.role.value if isinstance(message.role, Role) else message.role, + "role": message.role, } for content in message.contents: match content.type: @@ -669,7 +668,7 @@ class OpenAIBaseResponsesClient( def _prepare_content_for_openai( self, - role: Role, + role: str, content: Content, call_id_to_id: dict[str, str], ) -> dict[str, Any]: @@ -677,7 +676,7 @@ class OpenAIBaseResponsesClient( match content.type: case "text": return { - "type": "output_text" if role == Role.ASSISTANT else "input_text", + "type": "output_text" if role == "assistant" else "input_text", "text": content.text, } case "text_reasoning": @@ -1027,7 +1026,7 @@ class OpenAIBaseResponsesClient( ) case _: logger.debug("Unparsed output of type: %s: %s", item.type, item) - response_message = ChatMessage(role="assistant", contents=contents) + response_message = ChatMessage("assistant", contents) args: dict[str, Any] = { "response_id": response.id, "created_at": datetime.fromtimestamp(response.created_at, tz=timezone.utc).strftime( @@ -1387,7 +1386,7 @@ class OpenAIBaseResponsesClient( contents=contents, conversation_id=conversation_id, response_id=response_id, - role=Role.ASSISTANT, + role="assistant", model_id=model, additional_properties=metadata, raw_representation=event, diff --git a/python/packages/core/tests/azure/test_azure_assistants_client.py b/python/packages/core/tests/azure/test_azure_assistants_client.py index 32f1b13252..0187e98ddc 100644 --- a/python/packages/core/tests/azure/test_azure_assistants_client.py +++ b/python/packages/core/tests/azure/test_azure_assistants_client.py @@ -277,7 +277,7 @@ async def test_azure_assistants_client_get_response() -> None: "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(ChatMessage("user", ["What's the weather like today?"])) # Test that the client can be used to get a response response = await azure_assistants_client.get_response(messages=messages) @@ -295,7 +295,7 @@ async def test_azure_assistants_client_get_response_tools() -> None: assert isinstance(azure_assistants_client, ChatClientProtocol) messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages.append(ChatMessage("user", ["What's the weather like in Seattle?"])) # Test that the client can be used to get a response response = await azure_assistants_client.get_response( @@ -323,7 +323,7 @@ async def test_azure_assistants_client_streaming() -> None: "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(ChatMessage("user", ["What's the weather like today?"])) # Test that the client can be used to get a response response = azure_assistants_client.get_streaming_response(messages=messages) @@ -347,7 +347,7 @@ async def test_azure_assistants_client_streaming_tools() -> None: assert isinstance(azure_assistants_client, ChatClientProtocol) messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages.append(ChatMessage("user", ["What's the weather like in Seattle?"])) # Test that the client can be used to get a response response = azure_assistants_client.get_streaming_response( @@ -372,7 +372,7 @@ async def test_azure_assistants_client_with_existing_assistant() -> None: # First create an assistant to use in the test async with AzureOpenAIAssistantsClient(credential=AzureCliCredential()) as temp_client: # Get the assistant ID by triggering assistant creation - messages = [ChatMessage(role="user", text="Hello")] + messages = [ChatMessage("user", ["Hello"])] await temp_client.get_response(messages=messages) assistant_id = temp_client.assistant_id @@ -383,7 +383,7 @@ async def test_azure_assistants_client_with_existing_assistant() -> None: assert isinstance(azure_assistants_client, ChatClientProtocol) assert azure_assistants_client.assistant_id == assistant_id - messages = [ChatMessage(role="user", text="What can you do?")] + messages = [ChatMessage("user", ["What can you do?"])] # Test that the client can be used to get a response response = await azure_assistants_client.get_response(messages=messages) diff --git a/python/packages/core/tests/azure/test_azure_chat_client.py b/python/packages/core/tests/azure/test_azure_chat_client.py index caba327dc7..99df3bbdf5 100644 --- a/python/packages/core/tests/azure/test_azure_chat_client.py +++ b/python/packages/core/tests/azure/test_azure_chat_client.py @@ -665,7 +665,7 @@ async def test_azure_openai_chat_client_response() -> None: "of climate change.", ) ) - messages.append(ChatMessage(role="user", text="who are Emily and David?")) + messages.append(ChatMessage("user", ["who are Emily and David?"])) # Test that the client can be used to get a response response = await azure_chat_client.get_response(messages=messages) @@ -686,7 +686,7 @@ async def test_azure_openai_chat_client_response_tools() -> None: assert isinstance(azure_chat_client, ChatClientProtocol) messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="who are Emily and David?")) + messages.append(ChatMessage("user", ["who are Emily and David?"])) # Test that the client can be used to get a response response = await azure_chat_client.get_response( @@ -716,7 +716,7 @@ async def test_azure_openai_chat_client_streaming() -> None: "of climate change.", ) ) - messages.append(ChatMessage(role="user", text="who are Emily and David?")) + messages.append(ChatMessage("user", ["who are Emily and David?"])) # Test that the client can be used to get a response response = azure_chat_client.get_streaming_response(messages=messages) @@ -742,7 +742,7 @@ async def test_azure_openai_chat_client_streaming_tools() -> None: assert isinstance(azure_chat_client, ChatClientProtocol) messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="who are Emily and David?")) + messages.append(ChatMessage("user", ["who are Emily and David?"])) # Test that the client can be used to get a response response = azure_chat_client.get_streaming_response( diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/core/tests/azure/test_azure_responses_client.py index 35d92c7b98..13dfee819d 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/core/tests/azure/test_azure_responses_client.py @@ -221,14 +221,14 @@ async def test_integration_options( # Prepare test message if option_name == "tools" or option_name == "tool_choice": # Use weather-related prompt for tool tests - messages = [ChatMessage(role="user", text="What is the weather in Seattle?")] + messages = [ChatMessage("user", ["What is the weather in Seattle?"])] elif option_name == "response_format": # Use prompt that works well with structured output - messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")] - messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) + messages = [ChatMessage("user", ["The weather in Seattle is sunny"])] + messages.append(ChatMessage("user", ["What is the weather in Seattle?"])) else: # Generic prompt for simple options - messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")] + messages = [ChatMessage("user", ["Say 'Hello World' briefly."])] # Build options dict options: dict[str, Any] = {option_name: option_value} @@ -245,7 +245,7 @@ async def test_integration_options( ) output_format = option_value if option_name == "response_format" else None - response = await ChatResponse.from_chat_response_generator(response_gen, output_format_type=output_format) + response = await ChatResponse.from_update_generator(response_gen, output_format_type=output_format) else: # Test non-streaming mode response = await client.get_response( @@ -293,7 +293,7 @@ async def test_integration_web_search() -> None: }, } if streaming: - response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content)) + response = await ChatResponse.from_update_generator(client.get_streaming_response(**content)) else: response = await client.get_response(**content) @@ -318,7 +318,7 @@ async def test_integration_web_search() -> None: }, } if streaming: - response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content)) + response = await ChatResponse.from_update_generator(client.get_streaming_response(**content)) else: response = await client.get_response(**content) assert response.text is not None @@ -367,7 +367,7 @@ async def test_integration_client_file_search_streaming() -> None: ) assert response is not None - full_response = await ChatResponse.from_chat_response_generator(response) + full_response = await ChatResponse.from_update_generator(response) assert "sunny" in full_response.text.lower() assert "75" in full_response.text finally: diff --git a/python/packages/core/tests/core/conftest.py b/python/packages/core/tests/core/conftest.py index ed8de28c11..c5b7be9687 100644 --- a/python/packages/core/tests/core/conftest.py +++ b/python/packages/core/tests/core/conftest.py @@ -21,7 +21,6 @@ from agent_framework import ( ChatResponse, ChatResponseUpdate, Content, - Role, ToolProtocol, tool, use_chat_middleware, @@ -95,7 +94,7 @@ class MockChatClient: self.call_count += 1 if self.responses: return self.responses.pop(0) - return ChatResponse(messages=ChatMessage(role="assistant", text="test response")) + return ChatResponse(messages=ChatMessage("assistant", ["test response"])) async def get_streaming_response( self, @@ -108,7 +107,7 @@ class MockChatClient: for update in self.streaming_responses.pop(0): yield update else: - yield ChatResponseUpdate(text=Content.from_text(text="test streaming response "), role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(text="test streaming response ")], role="assistant") yield ChatResponseUpdate(contents=[Content.from_text(text="another update")], role="assistant") @@ -143,7 +142,7 @@ class MockBaseChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]): logger.debug(f"Running base chat client inner, with: {messages=}, {options=}, {kwargs=}") self.call_count += 1 if not self.run_responses: - return ChatResponse(messages=ChatMessage(role="assistant", text=f"test response - {messages[-1].text}")) + return ChatResponse(messages=ChatMessage("assistant", [f"test response - {messages[-1].text}"])) response = self.run_responses.pop(0) @@ -168,10 +167,14 @@ class MockBaseChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]): ) -> AsyncIterable[ChatResponseUpdate]: logger.debug(f"Running base chat client inner stream, with: {messages=}, {options=}, {kwargs=}") if not self.streaming_responses: - yield ChatResponseUpdate(text=f"update - {messages[0].text}", role="assistant") + yield ChatResponseUpdate( + contents=[Content.from_text(text=f"update - {messages[0].text}")], role="assistant" + ) return if options.get("tool_choice") == "none": - yield ChatResponseUpdate(text="I broke out of the function invocation loop...", role="assistant") + yield ChatResponseUpdate( + contents=[Content.from_text(text="I broke out of the function invocation loop...")], role="assistant" + ) return response = self.streaming_responses.pop(0) for update in response: @@ -233,7 +236,7 @@ class MockAgent(AgentProtocol): **kwargs: Any, ) -> AgentResponse: logger.debug(f"Running mock agent, with: {messages=}, {thread=}, {kwargs=}") - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text("Response")])]) + return AgentResponse(messages=[ChatMessage("assistant", [Content.from_text("Response")])]) async def run_stream( self, diff --git a/python/packages/core/tests/core/test_agents.py b/python/packages/core/tests/core/test_agents.py index 1f4d1cadce..09ef1bbbe1 100644 --- a/python/packages/core/tests/core/test_agents.py +++ b/python/packages/core/tests/core/test_agents.py @@ -24,7 +24,6 @@ from agent_framework import ( Context, ContextProvider, HostedCodeInterpreterTool, - Role, ToolProtocol, tool, ) @@ -43,7 +42,7 @@ def test_agent_type(agent: AgentProtocol) -> None: async def test_agent_run(agent: AgentProtocol) -> None: response = await agent.run("test") - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert response.messages[0].text == "Response" @@ -104,12 +103,12 @@ async def test_chat_client_agent_get_new_thread(chat_client: ChatClientProtocol) async def test_chat_client_agent_prepare_thread_and_messages(chat_client: ChatClientProtocol) -> None: agent = ChatAgent(chat_client=chat_client) - message = ChatMessage(role=Role.USER, text="Hello") + message = ChatMessage("user", ["Hello"]) thread = AgentThread(message_store=ChatMessageStore(messages=[message])) _, _, result_messages = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] thread=thread, - input_messages=[ChatMessage(role=Role.USER, text="Test")], + input_messages=[ChatMessage("user", ["Test"])], ) assert len(result_messages) == 2 @@ -127,7 +126,7 @@ async def test_prepare_thread_does_not_mutate_agent_chat_options(chat_client: Ch _, prepared_chat_options, _ = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] thread=thread, - input_messages=[ChatMessage(role=Role.USER, text="Test")], + input_messages=[ChatMessage("user", ["Test"])], ) assert prepared_chat_options.get("tools") is not None @@ -139,7 +138,7 @@ async def test_prepare_thread_does_not_mutate_agent_chat_options(chat_client: Ch async def test_chat_client_agent_update_thread_id(chat_client_base: ChatClientProtocol) -> None: mock_response = ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text("test response")])], + messages=[ChatMessage("assistant", [Content.from_text("test response")])], conversation_id="123", ) chat_client_base.run_responses = [mock_response] @@ -202,11 +201,7 @@ async def test_chat_client_agent_author_name_as_agent_name(chat_client: ChatClie async def test_chat_client_agent_author_name_is_used_from_response(chat_client_base: ChatClientProtocol) -> None: chat_client_base.run_responses = [ ChatResponse( - messages=[ - ChatMessage( - role=Role.ASSISTANT, contents=[Content.from_text("test response")], author_name="TestAuthor" - ) - ] + messages=[ChatMessage("assistant", [Content.from_text("test response")], author_name="TestAuthor")] ) ] @@ -256,7 +251,7 @@ class MockContextProvider(ContextProvider): async def test_chat_agent_context_providers_model_invoking(chat_client: ChatClientProtocol) -> None: """Test that context providers' invoking is called during agent run.""" - mock_provider = MockContextProvider(messages=[ChatMessage(role=Role.SYSTEM, text="Test context instructions")]) + mock_provider = MockContextProvider(messages=[ChatMessage("system", ["Test context instructions"])]) agent = ChatAgent(chat_client=chat_client, context_provider=mock_provider) await agent.run("Hello") @@ -269,7 +264,7 @@ async def test_chat_agent_context_providers_thread_created(chat_client_base: Cha mock_provider = MockContextProvider() chat_client_base.run_responses = [ ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text("test response")])], + messages=[ChatMessage("assistant", [Content.from_text("test response")])], conversation_id="test-thread-id", ) ] @@ -296,19 +291,19 @@ async def test_chat_agent_context_providers_messages_adding(chat_client: ChatCli async def test_chat_agent_context_instructions_in_messages(chat_client: ChatClientProtocol) -> None: """Test that AI context instructions are included in messages.""" - mock_provider = MockContextProvider(messages=[ChatMessage(role="system", text="Context-specific instructions")]) + mock_provider = MockContextProvider(messages=[ChatMessage("system", ["Context-specific instructions"])]) agent = ChatAgent(chat_client=chat_client, instructions="Agent instructions", context_provider=mock_provider) # We need to test the _prepare_thread_and_messages method directly _, _, messages = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] - thread=None, input_messages=[ChatMessage(role=Role.USER, text="Hello")] + thread=None, input_messages=[ChatMessage("user", ["Hello"])] ) # Should have context instructions, and user message assert len(messages) == 2 - assert messages[0].role == Role.SYSTEM + assert messages[0].role == "system" assert messages[0].text == "Context-specific instructions" - assert messages[1].role == Role.USER + assert messages[1].role == "user" assert messages[1].text == "Hello" # instructions system message is added by a chat_client @@ -319,18 +314,18 @@ async def test_chat_agent_no_context_instructions(chat_client: ChatClientProtoco agent = ChatAgent(chat_client=chat_client, instructions="Agent instructions", context_provider=mock_provider) _, _, messages = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] - thread=None, input_messages=[ChatMessage(role=Role.USER, text="Hello")] + thread=None, input_messages=[ChatMessage("user", ["Hello"])] ) # Should have agent instructions and user message only assert len(messages) == 1 - assert messages[0].role == Role.USER + assert messages[0].role == "user" assert messages[0].text == "Hello" async def test_chat_agent_run_stream_context_providers(chat_client: ChatClientProtocol) -> None: """Test that context providers work with run_stream method.""" - mock_provider = MockContextProvider(messages=[ChatMessage(role=Role.SYSTEM, text="Stream context instructions")]) + mock_provider = MockContextProvider(messages=[ChatMessage("system", ["Stream context instructions"])]) agent = ChatAgent(chat_client=chat_client, context_provider=mock_provider) # Collect all stream updates @@ -350,7 +345,7 @@ async def test_chat_agent_context_providers_with_thread_service_id(chat_client_b mock_provider = MockContextProvider() chat_client_base.run_responses = [ ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text("test response")])], + messages=[ChatMessage("assistant", [Content.from_text("test response")])], conversation_id="service-thread-123", ) ] @@ -585,7 +580,7 @@ async def test_agent_tool_receives_thread_in_kwargs(chat_client_base: Any) -> No ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] agent = ChatAgent( @@ -928,7 +923,7 @@ async def test_chat_agent_context_provider_adds_tools_when_agent_has_none(chat_c # Run the agent and verify context tools are added _, options, _ = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] - thread=None, input_messages=[ChatMessage(role=Role.USER, text="Hello")] + thread=None, input_messages=[ChatMessage("user", ["Hello"])] ) # The context tools should now be in the options @@ -952,7 +947,7 @@ async def test_chat_agent_context_provider_adds_instructions_when_agent_has_none # Run the agent and verify context instructions are available _, options, _ = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] - thread=None, input_messages=[ChatMessage(role=Role.USER, text="Hello")] + thread=None, input_messages=[ChatMessage("user", ["Hello"])] ) # The context instructions should now be in the options @@ -972,7 +967,7 @@ async def test_chat_agent_raises_on_conversation_id_mismatch(chat_client_base: C with pytest.raises(AgentExecutionException, match="conversation_id set on the agent is different"): await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] - thread=thread, input_messages=[ChatMessage(role=Role.USER, text="Hello")] + thread=thread, input_messages=[ChatMessage("user", ["Hello"])] ) diff --git a/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py b/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py index 39f441eb49..e3457f6625 100644 --- a/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py +++ b/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py @@ -28,7 +28,7 @@ class TestAsToolKwargsPropagation: # Setup mock response chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from sub-agent")]), + ChatResponse(messages=[ChatMessage("assistant", ["Response from sub-agent"])]), ] # Create sub-agent with middleware @@ -70,7 +70,7 @@ class TestAsToolKwargsPropagation: # Setup mock response chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from sub-agent")]), + ChatResponse(messages=[ChatMessage("assistant", ["Response from sub-agent"])]), ] sub_agent = ChatAgent( @@ -122,8 +122,8 @@ class TestAsToolKwargsPropagation: ) ] ), - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from agent_c")]), - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from agent_b")]), + ChatResponse(messages=[ChatMessage("assistant", ["Response from agent_c"])]), + ChatResponse(messages=[ChatMessage("assistant", ["Response from agent_b"])]), ] # Create agent C (bottom level) @@ -173,7 +173,7 @@ class TestAsToolKwargsPropagation: from agent_framework import ChatResponseUpdate chat_client.streaming_responses = [ - [ChatResponseUpdate(text=Content.from_text(text="Streaming response"), role="assistant")], + [ChatResponseUpdate(contents=[Content.from_text(text="Streaming response")], role="assistant")], ] sub_agent = ChatAgent( @@ -204,7 +204,7 @@ class TestAsToolKwargsPropagation: """Test that as_tool works correctly when no extra kwargs are provided.""" # Setup mock response chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from agent")]), + ChatResponse(messages=[ChatMessage("assistant", ["Response from agent"])]), ] sub_agent = ChatAgent( @@ -233,7 +233,7 @@ class TestAsToolKwargsPropagation: # Setup mock response chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="Response with options")]), + ChatResponse(messages=[ChatMessage("assistant", ["Response with options"])]), ] sub_agent = ChatAgent( @@ -280,8 +280,8 @@ class TestAsToolKwargsPropagation: # Setup mock responses for both calls chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="First response")]), - ChatResponse(messages=[ChatMessage(role="assistant", text="Second response")]), + ChatResponse(messages=[ChatMessage("assistant", ["First response"])]), + ChatResponse(messages=[ChatMessage("assistant", ["Second response"])]), ] sub_agent = ChatAgent( @@ -327,7 +327,7 @@ class TestAsToolKwargsPropagation: # Setup mock response chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from sub-agent")]), + ChatResponse(messages=[ChatMessage("assistant", ["Response from sub-agent"])]), ] sub_agent = ChatAgent( diff --git a/python/packages/core/tests/core/test_clients.py b/python/packages/core/tests/core/test_clients.py index 67ecd54a8d..c151451227 100644 --- a/python/packages/core/tests/core/test_clients.py +++ b/python/packages/core/tests/core/test_clients.py @@ -7,7 +7,6 @@ from agent_framework import ( BaseChatClient, ChatClientProtocol, ChatMessage, - Role, ) @@ -16,15 +15,15 @@ def test_chat_client_type(chat_client: ChatClientProtocol): async def test_chat_client_get_response(chat_client: ChatClientProtocol): - response = await chat_client.get_response(ChatMessage(role="user", text="Hello")) + response = await chat_client.get_response(ChatMessage("user", ["Hello"])) assert response.text == "test response" - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" async def test_chat_client_get_streaming_response(chat_client: ChatClientProtocol): - async for update in chat_client.get_streaming_response(ChatMessage(role="user", text="Hello")): + async for update in chat_client.get_streaming_response(ChatMessage("user", ["Hello"])): assert update.text == "test streaming response " or update.text == "another update" - assert update.role == Role.ASSISTANT + assert update.role == "assistant" def test_base_client(chat_client_base: ChatClientProtocol): @@ -33,13 +32,13 @@ def test_base_client(chat_client_base: ChatClientProtocol): async def test_base_client_get_response(chat_client_base: ChatClientProtocol): - response = await chat_client_base.get_response(ChatMessage(role="user", text="Hello")) - assert response.messages[0].role == Role.ASSISTANT + response = await chat_client_base.get_response(ChatMessage("user", ["Hello"])) + assert response.messages[0].role == "assistant" assert response.messages[0].text == "test response - Hello" async def test_base_client_get_streaming_response(chat_client_base: ChatClientProtocol): - async for update in chat_client_base.get_streaming_response(ChatMessage(role="user", text="Hello")): + async for update in chat_client_base.get_streaming_response(ChatMessage("user", ["Hello"])): assert update.text == "update - Hello" or update.text == "another update" @@ -54,17 +53,17 @@ async def test_chat_client_instructions_handling(chat_client_base: ChatClientPro _, kwargs = mock_inner_get_response.call_args messages = kwargs.get("messages", []) assert len(messages) == 1 - assert messages[0].role == Role.USER + assert messages[0].role == "user" assert messages[0].text == "hello" from agent_framework._types import prepend_instructions_to_messages appended_messages = prepend_instructions_to_messages( - [ChatMessage(role=Role.USER, text="hello")], + [ChatMessage("user", ["hello"])], instructions, ) assert len(appended_messages) == 2 - assert appended_messages[0].role == Role.SYSTEM + assert appended_messages[0].role == "system" assert appended_messages[0].text == "You are a helpful assistant." - assert appended_messages[1].role == Role.USER + assert appended_messages[1].role == "user" assert appended_messages[1].text == "hello" diff --git a/python/packages/core/tests/core/test_function_invocation_logic.py b/python/packages/core/tests/core/test_function_invocation_logic.py index 720d5a31d7..8d89c63bb7 100644 --- a/python/packages/core/tests/core/test_function_invocation_logic.py +++ b/python/packages/core/tests/core/test_function_invocation_logic.py @@ -13,7 +13,6 @@ from agent_framework import ( ChatResponse, ChatResponseUpdate, Content, - Role, tool, ) from agent_framework._middleware import FunctionInvocationContext, FunctionMiddleware @@ -37,21 +36,21 @@ async def test_base_client_with_function_calling(chat_client_base: ChatClientPro ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [ai_func]}) assert exec_counter == 1 assert len(response.messages) == 3 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert response.messages[0].contents[0].type == "function_call" assert response.messages[0].contents[0].name == "test_function" assert response.messages[0].contents[0].arguments == '{"arg1": "value1"}' assert response.messages[0].contents[0].call_id == "1" - assert response.messages[1].role == Role.TOOL + assert response.messages[1].role == "tool" assert response.messages[1].contents[0].type == "function_result" assert response.messages[1].contents[0].call_id == "1" assert response.messages[1].contents[0].result == "Processed value1" - assert response.messages[2].role == Role.ASSISTANT + assert response.messages[2].role == "assistant" assert response.messages[2].text == "done" @@ -81,16 +80,16 @@ async def test_base_client_with_function_calling_resets(chat_client_base: ChatCl ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [ai_func]}) assert exec_counter == 2 assert len(response.messages) == 5 - assert response.messages[0].role == Role.ASSISTANT - assert response.messages[1].role == Role.TOOL - assert response.messages[2].role == Role.ASSISTANT - assert response.messages[3].role == Role.TOOL - assert response.messages[4].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" + assert response.messages[1].role == "tool" + assert response.messages[2].role == "assistant" + assert response.messages[3].role == "tool" + assert response.messages[4].role == "assistant" assert response.messages[0].contents[0].type == "function_call" assert response.messages[1].contents[0].type == "function_result" assert response.messages[2].contents[0].type == "function_call" @@ -162,7 +161,7 @@ async def test_function_invocation_inside_aiohttp_server(chat_client_base: ChatC ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] agent = ChatAgent(chat_client=chat_client_base, tools=[ai_func]) @@ -219,7 +218,7 @@ async def test_function_invocation_in_threaded_aiohttp_app(chat_client_base: Cha ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] agent = ChatAgent(chat_client=chat_client_base, tools=[ai_func]) @@ -339,11 +338,11 @@ async def test_function_invocation_scenarios( # Single function call content func_call = Content.from_function_call(call_id="1", name=function_name, arguments='{"arg1": "value1"}') - completion = ChatMessage(role="assistant", text="done") + completion = ChatMessage("assistant", ["done"]) - chat_client_base.run_responses = [ - ChatResponse(messages=ChatMessage(role="assistant", contents=[func_call])) - ] + ([] if approval_required else [ChatResponse(messages=completion)]) + chat_client_base.run_responses = [ChatResponse(messages=ChatMessage("assistant", [func_call]))] + ( + [] if approval_required else [ChatResponse(messages=completion)] + ) chat_client_base.streaming_responses = [ [ @@ -371,7 +370,7 @@ async def test_function_invocation_scenarios( Content.from_function_call(call_id="2", name="approval_func", arguments='{"arg1": "value2"}'), ] - chat_client_base.run_responses = [ChatResponse(messages=ChatMessage(role="assistant", contents=func_calls))] + chat_client_base.run_responses = [ChatResponse(messages=ChatMessage("assistant", func_calls))] chat_client_base.streaming_responses = [ [ @@ -432,7 +431,7 @@ async def test_function_invocation_scenarios( assert messages[0].contents[0].type == "function_call" assert messages[1].contents[0].type == "function_result" assert messages[1].contents[0].result == "Processed value1" - assert messages[2].role == Role.ASSISTANT + assert messages[2].role == "assistant" assert messages[2].text == "done" assert exec_counter == 1 else: @@ -497,7 +496,7 @@ async def test_rejected_approval(chat_client_base: ChatClientProtocol): ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Get the response with approval requests @@ -527,7 +526,7 @@ async def test_rejected_approval(chat_client_base: ChatClientProtocol): ) # Continue conversation with one approved and one rejected - all_messages = response.messages + [ChatMessage(role="user", contents=[approved_response, rejected_response])] + all_messages = response.messages + [ChatMessage("user", [approved_response, rejected_response])] # Call get_response which will process the approvals await chat_client_base.get_response( @@ -561,9 +560,7 @@ async def test_rejected_approval(chat_client_base: ChatClientProtocol): for msg in all_messages: for content in msg.contents: if content.type == "function_result": - assert msg.role == Role.TOOL, ( - f"Message with FunctionResultContent must have role='tool', got '{msg.role}'" - ) + assert msg.role == "tool", f"Message with FunctionResultContent must have role='tool', got '{msg.role}'" async def test_approval_requests_in_assistant_message(chat_client_base: ChatClientProtocol): @@ -593,7 +590,7 @@ async def test_approval_requests_in_assistant_message(chat_client_base: ChatClie # Should have one assistant message containing both the call and approval request assert len(response.messages) == 1 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert len(response.messages[0].contents) == 2 assert response.messages[0].contents[0].type == "function_call" assert response.messages[0].contents[1].type == "function_approval_request" @@ -620,7 +617,7 @@ async def test_persisted_approval_messages_replay_correctly(chat_client_base: Ch ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Get approval request @@ -630,7 +627,7 @@ async def test_persisted_approval_messages_replay_correctly(chat_client_base: Ch # Store messages (like a thread would) persisted_messages = [ - ChatMessage(role="user", contents=[Content.from_text(text="hello")]), + ChatMessage("user", [Content.from_text(text="hello")]), *response1.messages, ] @@ -641,7 +638,7 @@ async def test_persisted_approval_messages_replay_correctly(chat_client_base: Ch function_call=approval_req.function_call, approved=True, ) - persisted_messages.append(ChatMessage(role="user", contents=[approval_response])) + persisted_messages.append(ChatMessage("user", [approval_response])) # Continue with all persisted messages response2 = await chat_client_base.get_response( @@ -670,7 +667,7 @@ async def test_no_duplicate_function_calls_after_approval_processing(chat_client ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] response1 = await chat_client_base.get_response( @@ -684,7 +681,7 @@ async def test_no_duplicate_function_calls_after_approval_processing(chat_client approved=True, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])] + all_messages = response1.messages + [ChatMessage("user", [approval_response])] await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [func_with_approval]}) # Count function calls with the same call_id @@ -714,7 +711,7 @@ async def test_rejection_result_uses_function_call_id(chat_client_base: ChatClie ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] response1 = await chat_client_base.get_response( @@ -728,7 +725,7 @@ async def test_rejection_result_uses_function_call_id(chat_client_base: ChatClie approved=False, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[rejection_response])] + all_messages = response1.messages + [ChatMessage("user", [rejection_response])] await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [func_with_approval]}) # Find the rejection result @@ -771,7 +768,7 @@ async def test_max_iterations_limit(chat_client_base: ChatClientProtocol): ) ), # Failsafe response when tool_choice is set to "none" - ChatResponse(messages=ChatMessage(role="assistant", text="giving up on tools")), + ChatResponse(messages=ChatMessage("assistant", ["giving up on tools"])), ] # Set max_iterations to 1 in additional_properties @@ -798,7 +795,7 @@ async def test_function_invocation_config_enabled_false(chat_client_base: ChatCl return f"Processed {arg1}" chat_client_base.run_responses = [ - ChatResponse(messages=ChatMessage(role="assistant", text="response without function calling")), + ChatResponse(messages=ChatMessage("assistant", ["response without function calling"])), ] # Disable function invocation @@ -853,7 +850,7 @@ async def test_function_invocation_config_max_consecutive_errors(chat_client_bas ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="final response")), + ChatResponse(messages=ChatMessage("assistant", ["final response"])), ] # Set max_consecutive_errors to 2 @@ -898,7 +895,7 @@ async def test_function_invocation_config_terminate_on_unknown_calls_false(chat_ ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Set terminate_on_unknown_calls to False (default) @@ -971,7 +968,7 @@ async def test_function_invocation_config_additional_tools(chat_client_base: Cha ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Add hidden_func to additional_tools @@ -1010,7 +1007,7 @@ async def test_function_invocation_config_include_detailed_errors_false(chat_cli ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Set include_detailed_errors to False (default) @@ -1044,7 +1041,7 @@ async def test_function_invocation_config_include_detailed_errors_true(chat_clie ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Set include_detailed_errors to True @@ -1114,7 +1111,7 @@ async def test_argument_validation_error_with_detailed_errors(chat_client_base: ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Set include_detailed_errors to True @@ -1148,7 +1145,7 @@ async def test_argument_validation_error_without_detailed_errors(chat_client_bas ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Set include_detailed_errors to False (default) @@ -1184,12 +1181,12 @@ async def test_hosted_tool_approval_response(chat_client_base: ChatClientProtoco ) chat_client_base.run_responses = [ - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Send the approval response response = await chat_client_base.get_response( - [ChatMessage(role="user", contents=[approval_response])], + [ChatMessage("user", [approval_response])], tool_choice="auto", tools=[local_func], ) @@ -1215,7 +1212,7 @@ async def test_unapproved_tool_execution_raises_exception(chat_client_base: Chat ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Get approval request @@ -1231,7 +1228,7 @@ async def test_unapproved_tool_execution_raises_exception(chat_client_base: Chat ) # Continue conversation with rejection - all_messages = response1.messages + [ChatMessage(role="user", contents=[rejection_response])] + all_messages = response1.messages + [ChatMessage("user", [rejection_response])] # This should handle the rejection gracefully (not raise ToolException to user) await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [test_func]}) @@ -1270,7 +1267,7 @@ async def test_approved_function_call_with_error_without_detailed_errors(chat_cl contents=[Content.from_function_call(call_id="1", name="error_func", arguments='{"arg1": "value1"}')], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Set include_detailed_errors to False (default) @@ -1288,7 +1285,7 @@ async def test_approved_function_call_with_error_without_detailed_errors(chat_cl approved=True, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])] + all_messages = response1.messages + [ChatMessage("user", [approval_response])] # Execute the approved function (which will error) await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [error_func]}) @@ -1333,7 +1330,7 @@ async def test_approved_function_call_with_error_with_detailed_errors(chat_clien contents=[Content.from_function_call(call_id="1", name="error_func", arguments='{"arg1": "value1"}')], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Set include_detailed_errors to True @@ -1351,7 +1348,7 @@ async def test_approved_function_call_with_error_with_detailed_errors(chat_clien approved=True, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])] + all_messages = response1.messages + [ChatMessage("user", [approval_response])] # Execute the approved function (which will error) await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [error_func]}) @@ -1396,7 +1393,7 @@ async def test_approved_function_call_with_validation_error(chat_client_base: Ch ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Set include_detailed_errors to True to see validation details @@ -1414,7 +1411,7 @@ async def test_approved_function_call_with_validation_error(chat_client_base: Ch approved=True, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])] + all_messages = response1.messages + [ChatMessage("user", [approval_response])] # Execute the approved function (which will fail validation) await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [typed_func]}) @@ -1455,7 +1452,7 @@ async def test_approved_function_call_successful_execution(chat_client_base: Cha contents=[Content.from_function_call(call_id="1", name="success_func", arguments='{"arg1": "value1"}')], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Get approval request @@ -1470,7 +1467,7 @@ async def test_approved_function_call_successful_execution(chat_client_base: Cha approved=True, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])] + all_messages = response1.messages + [ChatMessage("user", [approval_response])] # Execute the approved function await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [success_func]}) @@ -1516,7 +1513,7 @@ async def test_declaration_only_tool(chat_client_base: ChatClientProtocol): ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] response = await chat_client_base.get_response( @@ -1572,7 +1569,7 @@ async def test_multiple_function_calls_parallel_execution(chat_client_base: Chat ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [func1, func2]}) @@ -1608,7 +1605,7 @@ async def test_callable_function_converted_to_tool(chat_client_base: ChatClientP ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] # Pass plain function (will be auto-converted) @@ -1639,7 +1636,7 @@ async def test_conversation_id_handling(chat_client_base: ChatClientProtocol): conversation_id="conv_123", # Simulate service-side thread ), ChatResponse( - messages=ChatMessage(role="assistant", text="done"), + messages=ChatMessage("assistant", ["done"]), conversation_id="conv_123", ), ] @@ -1668,7 +1665,7 @@ async def test_function_result_appended_to_existing_assistant_message(chat_clien ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [test_func]}) @@ -1712,7 +1709,7 @@ async def test_error_recovery_resets_counter(chat_client_base: ChatClientProtoco ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [sometimes_fails]}) @@ -2324,7 +2321,7 @@ async def test_terminate_loop_single_function_call(chat_client_base: ChatClientP ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] response = await chat_client_base.get_response( @@ -2339,9 +2336,9 @@ async def test_terminate_loop_single_function_call(chat_client_base: ChatClientP # There should be 2 messages: assistant with function call, tool result from middleware # The loop should NOT have continued to call the LLM again assert len(response.messages) == 2 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert response.messages[0].contents[0].type == "function_call" - assert response.messages[1].role == Role.TOOL + assert response.messages[1].role == "tool" assert response.messages[1].contents[0].type == "function_result" assert response.messages[1].contents[0].result == "terminated by middleware" @@ -2393,7 +2390,7 @@ async def test_terminate_loop_multiple_function_calls_one_terminates(chat_client ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=ChatMessage("assistant", ["done"])), ] response = await chat_client_base.get_response( @@ -2410,9 +2407,9 @@ async def test_terminate_loop_multiple_function_calls_one_terminates(chat_client # There should be 2 messages: assistant with function calls, tool results # The loop should NOT have continued to call the LLM again assert len(response.messages) == 2 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert len(response.messages[0].contents) == 2 - assert response.messages[1].role == Role.TOOL + assert response.messages[1].role == "tool" # Both function results should be present assert len(response.messages[1].contents) == 2 diff --git a/python/packages/core/tests/core/test_kwargs_propagation_to_ai_function.py b/python/packages/core/tests/core/test_kwargs_propagation_to_ai_function.py index 34798a4a16..18e60c383c 100644 --- a/python/packages/core/tests/core/test_kwargs_propagation_to_ai_function.py +++ b/python/packages/core/tests/core/test_kwargs_propagation_to_ai_function.py @@ -49,7 +49,7 @@ class TestKwargsPropagationToFunctionTool: ] ) # Second call: return final response - return ChatResponse(messages=[ChatMessage(role="assistant", text="Done!")]) + return ChatResponse(messages=[ChatMessage("assistant", ["Done!"])]) # Wrap the function with function invocation decorator wrapped = _handle_function_calls_response(mock_get_response) @@ -101,7 +101,7 @@ class TestKwargsPropagationToFunctionTool: ) ] ) - return ChatResponse(messages=[ChatMessage(role="assistant", text="Completed!")]) + return ChatResponse(messages=[ChatMessage("assistant", ["Completed!"])]) wrapped = _handle_function_calls_response(mock_get_response) @@ -149,7 +149,7 @@ class TestKwargsPropagationToFunctionTool: ) ] ) - return ChatResponse(messages=[ChatMessage(role="assistant", text="All done!")]) + return ChatResponse(messages=[ChatMessage("assistant", ["All done!"])]) wrapped = _handle_function_calls_response(mock_get_response) @@ -196,13 +196,10 @@ class TestKwargsPropagationToFunctionTool: arguments='{"value": "streaming-test"}', ) ], - is_finished=True, ) else: # Second call: return final response - yield ChatResponseUpdate( - text=Content.from_text(text="Stream complete!"), role="assistant", is_finished=True - ) + yield ChatResponseUpdate(contents=[Content.from_text(text="Stream complete!")], role="assistant") wrapped = _handle_function_calls_streaming_response(mock_get_streaming_response) diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index f6d2b535d8..7695affb5a 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -18,7 +18,6 @@ from agent_framework import ( MCPStdioTool, MCPStreamableHTTPTool, MCPWebsocketTool, - Role, ToolProtocol, ) from agent_framework._mcp import ( @@ -63,7 +62,7 @@ def test_mcp_prompt_message_to_ai_content(): ai_content = _parse_message_from_mcp(mcp_message) assert isinstance(ai_content, ChatMessage) - assert ai_content.role.value == "user" + assert ai_content.role == "user" assert len(ai_content.contents) == 1 assert ai_content.contents[0].type == "text" assert ai_content.contents[0].text == "Hello, world!" @@ -1056,7 +1055,7 @@ async def test_local_mcp_server_prompt_execution(): assert len(result) == 1 assert isinstance(result[0], ChatMessage) - assert result[0].role == Role.USER + assert result[0].role == "user" assert len(result[0].contents) == 1 assert result[0].contents[0].text == "Test message" @@ -1414,7 +1413,7 @@ async def test_mcp_tool_sampling_callback_chat_client_exception(): async def test_mcp_tool_sampling_callback_no_valid_content(): """Test sampling callback when response has no valid content types.""" - from agent_framework import ChatMessage, Role + from agent_framework import ChatMessage tool = MCPStdioTool(name="test_tool", command="python") @@ -1423,7 +1422,7 @@ async def test_mcp_tool_sampling_callback_no_valid_content(): mock_response = Mock() mock_response.messages = [ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_uri( uri="data:application/json;base64,e30K", diff --git a/python/packages/core/tests/core/test_memory.py b/python/packages/core/tests/core/test_memory.py index bcc299ed37..78b48afd87 100644 --- a/python/packages/core/tests/core/test_memory.py +++ b/python/packages/core/tests/core/test_memory.py @@ -4,7 +4,7 @@ import sys from collections.abc import MutableSequence from typing import Any -from agent_framework import ChatMessage, Role +from agent_framework import ChatMessage from agent_framework._memory import Context, ContextProvider @@ -69,7 +69,7 @@ class TestContext: def test_context_with_values(self) -> None: """Test Context can be initialized with values.""" - messages = [ChatMessage(role=Role.USER, text="Test message")] + messages = [ChatMessage("user", ["Test message"])] context = Context(instructions="Test instructions", messages=messages) assert context.instructions == "Test instructions" assert len(context.messages) == 1 @@ -89,15 +89,15 @@ class TestContextProvider: async def test_invoked(self) -> None: """Test invoked is called.""" provider = MockContextProvider() - message = ChatMessage(role=Role.USER, text="Test message") + message = ChatMessage("user", ["Test message"]) await provider.invoked(message) assert provider.invoked_called assert provider.new_messages == message async def test_invoking(self) -> None: """Test invoking is called and returns context.""" - provider = MockContextProvider(messages=[ChatMessage(role=Role.USER, text="Context message")]) - message = ChatMessage(role=Role.USER, text="Test message") + provider = MockContextProvider(messages=[ChatMessage("user", ["Context message"])]) + message = ChatMessage("user", ["Test message"]) context = await provider.invoking(message) assert provider.invoking_called assert provider.model_invoking_messages == message @@ -114,7 +114,7 @@ class TestContextProvider: async def test_base_invoked_does_nothing(self) -> None: """Test that base ContextProvider.invoked does nothing by default.""" provider = MinimalContextProvider() - message = ChatMessage(role=Role.USER, text="Test") + message = ChatMessage("user", ["Test"]) await provider.invoked(message) await provider.invoked(message, response_messages=message) await provider.invoked(message, invoke_exception=Exception("test")) diff --git a/python/packages/core/tests/core/test_middleware.py b/python/packages/core/tests/core/test_middleware.py index a62cca2c76..b0536ac94c 100644 --- a/python/packages/core/tests/core/test_middleware.py +++ b/python/packages/core/tests/core/test_middleware.py @@ -15,7 +15,6 @@ from agent_framework import ( ChatResponse, ChatResponseUpdate, Content, - Role, ) from agent_framework._middleware import ( AgentMiddleware, @@ -36,7 +35,7 @@ class TestAgentRunContext: def test_init_with_defaults(self, mock_agent: AgentProtocol) -> None: """Test AgentRunContext initialization with default values.""" - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) assert context.agent is mock_agent @@ -46,7 +45,7 @@ class TestAgentRunContext: def test_init_with_custom_values(self, mock_agent: AgentProtocol) -> None: """Test AgentRunContext initialization with custom values.""" - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] metadata = {"key": "value"} context = AgentRunContext(agent=mock_agent, messages=messages, is_streaming=True, metadata=metadata) @@ -59,7 +58,7 @@ class TestAgentRunContext: """Test AgentRunContext initialization with thread parameter.""" from agent_framework import AgentThread - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] thread = AgentThread() context = AgentRunContext(agent=mock_agent, messages=messages, thread=thread) @@ -98,7 +97,7 @@ class TestChatContext: def test_init_with_defaults(self, mock_chat_client: Any) -> None: """Test ChatContext initialization with default values.""" - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) @@ -112,7 +111,7 @@ class TestChatContext: def test_init_with_custom_values(self, mock_chat_client: Any) -> None: """Test ChatContext initialization with custom values.""" - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {"temperature": 0.5} metadata = {"key": "value"} @@ -169,10 +168,10 @@ class TestAgentMiddlewarePipeline: async def test_execute_no_middleware(self, mock_agent: AgentProtocol) -> None: """Test pipeline execution with no middleware.""" pipeline = AgentMiddlewarePipeline() - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) - expected_response = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + expected_response = AgentResponse(messages=[ChatMessage("assistant", ["response"])]) async def final_handler(ctx: AgentRunContext) -> AgentResponse: return expected_response @@ -197,10 +196,10 @@ class TestAgentMiddlewarePipeline: middleware = OrderTrackingMiddleware("test") pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) - expected_response = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + expected_response = AgentResponse(messages=[ChatMessage("assistant", ["response"])]) async def final_handler(ctx: AgentRunContext) -> AgentResponse: execution_order.append("handler") @@ -213,7 +212,7 @@ class TestAgentMiddlewarePipeline: async def test_execute_stream_no_middleware(self, mock_agent: AgentProtocol) -> None: """Test pipeline streaming execution with no middleware.""" pipeline = AgentMiddlewarePipeline() - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]: @@ -245,7 +244,7 @@ class TestAgentMiddlewarePipeline: middleware = StreamOrderTrackingMiddleware("test") pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]: @@ -267,14 +266,14 @@ class TestAgentMiddlewarePipeline: """Test pipeline execution with termination before next().""" middleware = self.PreNextTerminateMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) execution_order: list[str] = [] async def final_handler(ctx: AgentRunContext) -> AgentResponse: # Handler should not be executed when terminated before next() execution_order.append("handler") - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["response"])]) response = await pipeline.execute(mock_agent, messages, context, final_handler) assert response is not None @@ -287,13 +286,13 @@ class TestAgentMiddlewarePipeline: """Test pipeline execution with termination after next().""" middleware = self.PostNextTerminateMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) execution_order: list[str] = [] async def final_handler(ctx: AgentRunContext) -> AgentResponse: execution_order.append("handler") - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["response"])]) response = await pipeline.execute(mock_agent, messages, context, final_handler) assert response is not None @@ -306,7 +305,7 @@ class TestAgentMiddlewarePipeline: """Test pipeline streaming execution with termination before next().""" middleware = self.PreNextTerminateMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) execution_order: list[str] = [] @@ -330,7 +329,7 @@ class TestAgentMiddlewarePipeline: """Test pipeline streaming execution with termination after next().""" middleware = self.PostNextTerminateMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) execution_order: list[str] = [] @@ -366,11 +365,11 @@ class TestAgentMiddlewarePipeline: middleware = ThreadCapturingMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] thread = AgentThread() context = AgentRunContext(agent=mock_agent, messages=messages, thread=thread) - expected_response = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + expected_response = AgentResponse(messages=[ChatMessage("assistant", ["response"])]) async def final_handler(ctx: AgentRunContext) -> AgentResponse: return expected_response @@ -393,10 +392,10 @@ class TestAgentMiddlewarePipeline: middleware = ThreadCapturingMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages, thread=None) - expected_response = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + expected_response = AgentResponse(messages=[ChatMessage("assistant", ["response"])]) async def final_handler(ctx: AgentRunContext) -> AgentResponse: return expected_response @@ -560,11 +559,11 @@ class TestChatMiddlewarePipeline: async def test_execute_no_middleware(self, mock_chat_client: Any) -> None: """Test pipeline execution with no middleware.""" pipeline = ChatMiddlewarePipeline() - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) - expected_response = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + expected_response = ChatResponse(messages=[ChatMessage("assistant", ["response"])]) async def final_handler(ctx: ChatContext) -> ChatResponse: return expected_response @@ -587,11 +586,11 @@ class TestChatMiddlewarePipeline: middleware = OrderTrackingChatMiddleware("test") pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) - expected_response = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + expected_response = ChatResponse(messages=[ChatMessage("assistant", ["response"])]) async def final_handler(ctx: ChatContext) -> ChatResponse: execution_order.append("handler") @@ -604,7 +603,7 @@ class TestChatMiddlewarePipeline: async def test_execute_stream_no_middleware(self, mock_chat_client: Any) -> None: """Test pipeline streaming execution with no middleware.""" pipeline = ChatMiddlewarePipeline() - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) @@ -635,7 +634,7 @@ class TestChatMiddlewarePipeline: middleware = StreamOrderTrackingChatMiddleware("test") pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, is_streaming=True) @@ -658,7 +657,7 @@ class TestChatMiddlewarePipeline: """Test pipeline execution with termination before next().""" middleware = self.PreNextTerminateChatMiddleware() pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) execution_order: list[str] = [] @@ -666,7 +665,7 @@ class TestChatMiddlewarePipeline: async def final_handler(ctx: ChatContext) -> ChatResponse: # Handler should not be executed when terminated before next() execution_order.append("handler") - return ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return ChatResponse(messages=[ChatMessage("assistant", ["response"])]) response = await pipeline.execute(mock_chat_client, messages, chat_options, context, final_handler) assert response is None @@ -678,14 +677,14 @@ class TestChatMiddlewarePipeline: """Test pipeline execution with termination after next().""" middleware = self.PostNextTerminateChatMiddleware() pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) execution_order: list[str] = [] async def final_handler(ctx: ChatContext) -> ChatResponse: execution_order.append("handler") - return ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return ChatResponse(messages=[ChatMessage("assistant", ["response"])]) response = await pipeline.execute(mock_chat_client, messages, chat_options, context, final_handler) assert response is not None @@ -698,7 +697,7 @@ class TestChatMiddlewarePipeline: """Test pipeline streaming execution with termination before next().""" middleware = self.PreNextTerminateChatMiddleware() pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, is_streaming=True) execution_order: list[str] = [] @@ -723,7 +722,7 @@ class TestChatMiddlewarePipeline: """Test pipeline streaming execution with termination after next().""" middleware = self.PostNextTerminateChatMiddleware() pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, is_streaming=True) execution_order: list[str] = [] @@ -764,12 +763,12 @@ class TestClassBasedMiddleware: middleware = MetadataAgentMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AgentResponse: metadata_updates.append("handler") - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["response"])]) result = await pipeline.execute(mock_agent, messages, context, final_handler) @@ -827,12 +826,12 @@ class TestFunctionBasedMiddleware: execution_order.append("function_after") pipeline = AgentMiddlewarePipeline([test_agent_middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AgentResponse: execution_order.append("handler") - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["response"])]) result = await pipeline.execute(mock_agent, messages, context, final_handler) @@ -890,12 +889,12 @@ class TestMixedMiddleware: execution_order.append("function_after") pipeline = AgentMiddlewarePipeline([ClassMiddleware(), function_middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AgentResponse: execution_order.append("handler") - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["response"])]) result = await pipeline.execute(mock_agent, messages, context, final_handler) @@ -954,13 +953,13 @@ class TestMixedMiddleware: execution_order.append("function_after") pipeline = ChatMiddlewarePipeline([ClassChatMiddleware(), function_chat_middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) async def final_handler(ctx: ChatContext) -> ChatResponse: execution_order.append("handler") - return ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return ChatResponse(messages=[ChatMessage("assistant", ["response"])]) result = await pipeline.execute(mock_chat_client, messages, chat_options, context, final_handler) @@ -1001,12 +1000,12 @@ class TestMultipleMiddlewareOrdering: middleware = [FirstMiddleware(), SecondMiddleware(), ThirdMiddleware()] pipeline = AgentMiddlewarePipeline(middleware) # type: ignore - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AgentResponse: execution_order.append("handler") - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["response"])]) result = await pipeline.execute(mock_agent, messages, context, final_handler) @@ -1085,13 +1084,13 @@ class TestMultipleMiddlewareOrdering: middleware = [FirstChatMiddleware(), SecondChatMiddleware(), ThirdChatMiddleware()] pipeline = ChatMiddlewarePipeline(middleware) # type: ignore - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) async def final_handler(ctx: ChatContext) -> ChatResponse: execution_order.append("handler") - return ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return ChatResponse(messages=[ChatMessage("assistant", ["response"])]) result = await pipeline.execute(mock_chat_client, messages, chat_options, context, final_handler) @@ -1127,7 +1126,7 @@ class TestContextContentValidation: # Verify context content assert context.agent is mock_agent assert len(context.messages) == 1 - assert context.messages[0].role == Role.USER + assert context.messages[0].role == "user" assert context.messages[0].text == "test" assert context.is_streaming is False assert isinstance(context.metadata, dict) @@ -1139,13 +1138,13 @@ class TestContextContentValidation: middleware = ContextValidationMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AgentResponse: # Verify metadata was set by middleware assert ctx.metadata.get("validated") is True - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["response"])]) result = await pipeline.execute(mock_agent, messages, context, final_handler) assert result is not None @@ -1205,7 +1204,7 @@ class TestContextContentValidation: # Verify context content assert context.chat_client is mock_chat_client assert len(context.messages) == 1 - assert context.messages[0].role == Role.USER + assert context.messages[0].role == "user" assert context.messages[0].text == "test" assert context.is_streaming is False assert isinstance(context.metadata, dict) @@ -1219,14 +1218,14 @@ class TestContextContentValidation: middleware = ChatContextValidationMiddleware() pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {"temperature": 0.5} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) async def final_handler(ctx: ChatContext) -> ChatResponse: # Verify metadata was set by middleware assert ctx.metadata.get("validated") is True - return ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return ChatResponse(messages=[ChatMessage("assistant", ["response"])]) result = await pipeline.execute(mock_chat_client, messages, chat_options, context, final_handler) assert result is not None @@ -1248,14 +1247,14 @@ class TestStreamingScenarios: middleware = StreamingFlagMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] # Test non-streaming context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AgentResponse: streaming_flags.append(ctx.is_streaming) - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["response"])]) await pipeline.execute(mock_agent, messages, context, final_handler) @@ -1287,7 +1286,7 @@ class TestStreamingScenarios: middleware = StreamProcessingMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_stream_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]: @@ -1323,7 +1322,7 @@ class TestStreamingScenarios: middleware = ChatStreamingFlagMiddleware() pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} # Test non-streaming @@ -1331,7 +1330,7 @@ class TestStreamingScenarios: async def final_handler(ctx: ChatContext) -> ChatResponse: streaming_flags.append(ctx.is_streaming) - return ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return ChatResponse(messages=[ChatMessage("assistant", ["response"])]) await pipeline.execute(mock_chat_client, messages, chat_options, context, final_handler) @@ -1365,7 +1364,7 @@ class TestStreamingScenarios: middleware = ChatStreamProcessingMiddleware() pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, is_streaming=True) @@ -1447,7 +1446,7 @@ class TestMiddlewareExecutionControl: middleware = NoNextMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) handler_called = False @@ -1455,7 +1454,7 @@ class TestMiddlewareExecutionControl: async def final_handler(ctx: AgentRunContext) -> AgentResponse: nonlocal handler_called handler_called = True - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="should not execute")]) + return AgentResponse(messages=[ChatMessage("assistant", ["should not execute"])]) result = await pipeline.execute(mock_agent, messages, context, final_handler) @@ -1478,7 +1477,7 @@ class TestMiddlewareExecutionControl: middleware = NoNextStreamingMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) handler_called = False @@ -1551,7 +1550,7 @@ class TestMiddlewareExecutionControl: await next(context) pipeline = AgentMiddlewarePipeline([FirstMiddleware(), SecondMiddleware()]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) handler_called = False @@ -1559,7 +1558,7 @@ class TestMiddlewareExecutionControl: async def final_handler(ctx: AgentRunContext) -> AgentResponse: nonlocal handler_called handler_called = True - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="should not execute")]) + return AgentResponse(messages=[ChatMessage("assistant", ["should not execute"])]) result = await pipeline.execute(mock_agent, messages, context, final_handler) @@ -1580,7 +1579,7 @@ class TestMiddlewareExecutionControl: middleware = NoNextChatMiddleware() pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) @@ -1589,7 +1588,7 @@ class TestMiddlewareExecutionControl: async def final_handler(ctx: ChatContext) -> ChatResponse: nonlocal handler_called handler_called = True - return ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="should not execute")]) + return ChatResponse(messages=[ChatMessage("assistant", ["should not execute"])]) result = await pipeline.execute(mock_chat_client, messages, chat_options, context, final_handler) @@ -1608,7 +1607,7 @@ class TestMiddlewareExecutionControl: middleware = NoNextStreamingChatMiddleware() pipeline = ChatMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, is_streaming=True) @@ -1644,7 +1643,7 @@ class TestMiddlewareExecutionControl: await next(context) pipeline = ChatMiddlewarePipeline([FirstChatMiddleware(), SecondChatMiddleware()]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] chat_options: dict[str, Any] = {} context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) @@ -1653,7 +1652,7 @@ class TestMiddlewareExecutionControl: async def final_handler(ctx: ChatContext) -> ChatResponse: nonlocal handler_called handler_called = True - return ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="should not execute")]) + return ChatResponse(messages=[ChatMessage("assistant", ["should not execute"])]) result = await pipeline.execute(mock_chat_client, messages, chat_options, context, final_handler) diff --git a/python/packages/core/tests/core/test_middleware_context_result.py b/python/packages/core/tests/core/test_middleware_context_result.py index 0f3b506fab..21f893a62c 100644 --- a/python/packages/core/tests/core/test_middleware_context_result.py +++ b/python/packages/core/tests/core/test_middleware_context_result.py @@ -14,7 +14,6 @@ from agent_framework import ( ChatAgent, ChatMessage, Content, - Role, ) from agent_framework._middleware import ( AgentMiddleware, @@ -40,7 +39,7 @@ class TestResultOverrideMiddleware: async def test_agent_middleware_response_override_non_streaming(self, mock_agent: AgentProtocol) -> None: """Test that agent middleware can override response for non-streaming execution.""" - override_response = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="overridden response")]) + override_response = AgentResponse(messages=[ChatMessage("assistant", ["overridden response"])]) class ResponseOverrideMiddleware(AgentMiddleware): async def process( @@ -52,7 +51,7 @@ class TestResultOverrideMiddleware: middleware = ResponseOverrideMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) handler_called = False @@ -60,7 +59,7 @@ class TestResultOverrideMiddleware: async def final_handler(ctx: AgentRunContext) -> AgentResponse: nonlocal handler_called handler_called = True - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="original response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["original response"])]) result = await pipeline.execute(mock_agent, messages, context, final_handler) @@ -88,7 +87,7 @@ class TestResultOverrideMiddleware: middleware = StreamResponseOverrideMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]: @@ -149,7 +148,7 @@ class TestResultOverrideMiddleware: # Then conditionally override based on content if any("special" in msg.text for msg in context.messages if msg.text): context.result = AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Special response from middleware!")] + messages=[ChatMessage("assistant", ["Special response from middleware!"])] ) # Create ChatAgent with override middleware @@ -157,14 +156,14 @@ class TestResultOverrideMiddleware: agent = ChatAgent(chat_client=mock_chat_client, middleware=[middleware]) # Test override case - override_messages = [ChatMessage(role=Role.USER, text="Give me a special response")] + override_messages = [ChatMessage("user", ["Give me a special response"])] override_response = await agent.run(override_messages) assert override_response.messages[0].text == "Special response from middleware!" # Verify chat client was called since middleware called next() assert mock_chat_client.call_count == 1 # Test normal case - normal_messages = [ChatMessage(role=Role.USER, text="Normal request")] + normal_messages = [ChatMessage("user", ["Normal request"])] normal_response = await agent.run(normal_messages) assert normal_response.messages[0].text == "test response" # Verify chat client was called for normal case @@ -194,7 +193,7 @@ class TestResultOverrideMiddleware: agent = ChatAgent(chat_client=mock_chat_client, middleware=[middleware]) # Test streaming override case - override_messages = [ChatMessage(role=Role.USER, text="Give me a custom stream")] + override_messages = [ChatMessage("user", ["Give me a custom stream"])] override_updates: list[AgentResponseUpdate] = [] async for update in agent.run_stream(override_messages): override_updates.append(update) @@ -205,7 +204,7 @@ class TestResultOverrideMiddleware: assert override_updates[2].text == " response!" # Test normal streaming case - normal_messages = [ChatMessage(role=Role.USER, text="Normal streaming request")] + normal_messages = [ChatMessage("user", ["Normal streaming request"])] normal_updates: list[AgentResponseUpdate] = [] async for update in agent.run_stream(normal_messages): normal_updates.append(update) @@ -234,10 +233,10 @@ class TestResultOverrideMiddleware: async def final_handler(ctx: AgentRunContext) -> AgentResponse: nonlocal handler_called handler_called = True - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="executed response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["executed response"])]) # Test case where next() is NOT called - no_execute_messages = [ChatMessage(role=Role.USER, text="Don't run this")] + no_execute_messages = [ChatMessage("user", ["Don't run this"])] no_execute_context = AgentRunContext(agent=mock_agent, messages=no_execute_messages) no_execute_result = await pipeline.execute(mock_agent, no_execute_messages, no_execute_context, final_handler) @@ -252,7 +251,7 @@ class TestResultOverrideMiddleware: handler_called = False # Test case where next() IS called - execute_messages = [ChatMessage(role=Role.USER, text="Please execute this")] + execute_messages = [ChatMessage("user", ["Please execute this"])] execute_context = AgentRunContext(agent=mock_agent, messages=execute_messages) execute_result = await pipeline.execute(mock_agent, execute_messages, execute_context, final_handler) @@ -332,11 +331,11 @@ class TestResultObservability: middleware = ObservabilityMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AgentResponse: - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="executed response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["executed response"])]) result = await pipeline.execute(mock_agent, messages, context, final_handler) @@ -396,17 +395,15 @@ class TestResultObservability: if "modify" in context.result.messages[0].text: # Override after observing - context.result = AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="modified after execution")] - ) + context.result = AgentResponse(messages=[ChatMessage("assistant", ["modified after execution"])]) middleware = PostExecutionOverrideMiddleware() pipeline = AgentMiddlewarePipeline([middleware]) - messages = [ChatMessage(role=Role.USER, text="test")] + messages = [ChatMessage("user", ["test"])] context = AgentRunContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentRunContext) -> AgentResponse: - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response to modify")]) + return AgentResponse(messages=[ChatMessage("assistant", ["response to modify"])]) result = await pipeline.execute(mock_agent, messages, context, final_handler) diff --git a/python/packages/core/tests/core/test_middleware_with_agent.py b/python/packages/core/tests/core/test_middleware_with_agent.py index a9f410b609..51c227e0b2 100644 --- a/python/packages/core/tests/core/test_middleware_with_agent.py +++ b/python/packages/core/tests/core/test_middleware_with_agent.py @@ -15,7 +15,6 @@ from agent_framework import ( ChatResponseUpdate, Content, FunctionTool, - Role, agent_middleware, chat_middleware, function_middleware, @@ -58,13 +57,13 @@ class TestChatAgentClassBasedMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response assert response is not None assert len(response.messages) > 0 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" # Note: conftest "MockChatClient" returns different text format assert "test response" in response.messages[0].text @@ -93,7 +92,7 @@ class TestChatAgentClassBasedMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response @@ -128,8 +127,8 @@ class TestChatAgentFunctionBasedMiddleware: # Execute the agent with multiple messages messages = [ - ChatMessage(role=Role.USER, text="message1"), - ChatMessage(role=Role.USER, text="message2"), # This should not be processed due to termination + ChatMessage("user", ["message1"]), + ChatMessage("user", ["message2"]), # This should not be processed due to termination ] response = await agent.run(messages) @@ -158,15 +157,15 @@ class TestChatAgentFunctionBasedMiddleware: # Execute the agent with multiple messages messages = [ - ChatMessage(role=Role.USER, text="message1"), - ChatMessage(role=Role.USER, text="message2"), + ChatMessage("user", ["message1"]), + ChatMessage("user", ["message2"]), ] response = await agent.run(messages) # Verify response assert response is not None assert len(response.messages) == 1 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert "test response" in response.messages[0].text # Verify middleware execution order @@ -190,7 +189,7 @@ class TestChatAgentFunctionBasedMiddleware: execution_order.append("middleware_after") # Create a message to start the conversation - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] # Set up chat client to return a function call, then a final response # If terminate works correctly, only the first response should be consumed @@ -198,7 +197,7 @@ class TestChatAgentFunctionBasedMiddleware: ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="test_call", name="test_function", arguments={"text": "test"} @@ -207,7 +206,7 @@ class TestChatAgentFunctionBasedMiddleware: ) ] ), - ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="this should not be consumed")]), + ChatResponse(messages=[ChatMessage("assistant", ["this should not be consumed"])]), ] # Create the test function with the expected signature @@ -251,7 +250,7 @@ class TestChatAgentFunctionBasedMiddleware: context.terminate = True # Create a message to start the conversation - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] # Set up chat client to return a function call, then a final response # If terminate works correctly, only the first response should be consumed @@ -259,7 +258,7 @@ class TestChatAgentFunctionBasedMiddleware: ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="test_call", name="test_function", arguments={"text": "test"} @@ -268,7 +267,7 @@ class TestChatAgentFunctionBasedMiddleware: ) ] ), - ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="this should not be consumed")]), + ChatResponse(messages=[ChatMessage("assistant", ["this should not be consumed"])]), ] # Create the test function with the expected signature @@ -312,13 +311,13 @@ class TestChatAgentFunctionBasedMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[tracking_agent_middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response assert response is not None assert len(response.messages) > 0 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert response.messages[0].text == "test response" assert chat_client.call_count == 1 @@ -340,7 +339,7 @@ class TestChatAgentFunctionBasedMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[tracking_function_middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response @@ -376,13 +375,13 @@ class TestChatAgentStreamingMiddleware: # Set up mock streaming responses chat_client.streaming_responses = [ [ - ChatResponseUpdate(contents=[Content.from_text(text="Streaming")], role=Role.ASSISTANT), - ChatResponseUpdate(contents=[Content.from_text(text=" response")], role=Role.ASSISTANT), + ChatResponseUpdate(contents=[Content.from_text(text="Streaming")], role="assistant"), + ChatResponseUpdate(contents=[Content.from_text(text=" response")], role="assistant"), ] ] # Execute streaming - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] updates: list[AgentResponseUpdate] = [] async for update in agent.run_stream(messages): updates.append(update) @@ -411,7 +410,7 @@ class TestChatAgentStreamingMiddleware: # Create ChatAgent with middleware middleware = FlagTrackingMiddleware() agent = ChatAgent(chat_client=chat_client, middleware=[middleware]) - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] # Test non-streaming execution response = await agent.run(messages) @@ -452,7 +451,7 @@ class TestChatAgentMultipleMiddlewareOrdering: agent = ChatAgent(chat_client=chat_client, middleware=[middleware1, middleware2, middleware3]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response @@ -511,7 +510,7 @@ class TestChatAgentMultipleMiddlewareOrdering: ) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response @@ -567,7 +566,7 @@ class TestChatAgentFunctionMiddlewareWithTools: function_call_response = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="call_123", @@ -578,7 +577,7 @@ class TestChatAgentFunctionMiddlewareWithTools: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Final response")]) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["Final response"])]) chat_client.responses = [function_call_response, final_response] @@ -591,7 +590,7 @@ class TestChatAgentFunctionMiddlewareWithTools: ) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="Get weather for Seattle")] + messages = [ChatMessage("user", ["Get weather for Seattle"])] response = await agent.run(messages) # Verify response @@ -627,7 +626,7 @@ class TestChatAgentFunctionMiddlewareWithTools: function_call_response = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="call_456", @@ -638,7 +637,7 @@ class TestChatAgentFunctionMiddlewareWithTools: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Final response")]) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["Final response"])]) chat_client.responses = [function_call_response, final_response] @@ -650,7 +649,7 @@ class TestChatAgentFunctionMiddlewareWithTools: ) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="Get weather for San Francisco")] + messages = [ChatMessage("user", ["Get weather for San Francisco"])] response = await agent.run(messages) # Verify response @@ -699,7 +698,7 @@ class TestChatAgentFunctionMiddlewareWithTools: function_call_response = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="call_789", @@ -710,7 +709,7 @@ class TestChatAgentFunctionMiddlewareWithTools: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Final response")]) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["Final response"])]) chat_client.responses = [function_call_response, final_response] @@ -722,7 +721,7 @@ class TestChatAgentFunctionMiddlewareWithTools: ) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="Get weather for New York")] + messages = [ChatMessage("user", ["Get weather for New York"])] response = await agent.run(messages) # Verify response @@ -786,7 +785,7 @@ class TestChatAgentFunctionMiddlewareWithTools: ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="test_call", name="sample_tool_function", arguments={"location": "Seattle"} @@ -795,16 +794,14 @@ class TestChatAgentFunctionMiddlewareWithTools: ) ] ), - ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text("Function completed")])] - ), + ChatResponse(messages=[ChatMessage("assistant", [Content.from_text("Function completed")])]), ] # Create ChatAgent with function middleware agent = ChatAgent(chat_client=chat_client, middleware=[kwargs_middleware], tools=[sample_tool_function]) # Execute the agent with custom parameters passed as kwargs - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages, custom_param="test_value") # Verify response @@ -1068,7 +1065,7 @@ class TestRunLevelMiddleware: # Verify response is correct assert response is not None assert len(response.messages) > 0 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert "test response" in response.messages[0].text # Verify middleware was executed @@ -1097,8 +1094,8 @@ class TestRunLevelMiddleware: # Set up mock streaming responses chat_client.streaming_responses = [ [ - ChatResponseUpdate(contents=[Content.from_text(text="Stream")], role=Role.ASSISTANT), - ChatResponseUpdate(contents=[Content.from_text(text=" response")], role=Role.ASSISTANT), + ChatResponseUpdate(contents=[Content.from_text(text="Stream")], role="assistant"), + ChatResponseUpdate(contents=[Content.from_text(text=" response")], role="assistant"), ] ] @@ -1182,7 +1179,7 @@ class TestRunLevelMiddleware: function_call_response = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="test_call", @@ -1193,7 +1190,7 @@ class TestRunLevelMiddleware: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Final response")]) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["Final response"])]) chat_client.responses = [function_call_response, final_response] # Create agent with agent-level middleware @@ -1275,7 +1272,7 @@ class TestMiddlewareDecoratorLogic: function_call_response = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="test_call", @@ -1286,7 +1283,7 @@ class TestMiddlewareDecoratorLogic: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Final response")]) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["Final response"])]) chat_client.responses = [function_call_response, final_response] # Should work without errors @@ -1296,7 +1293,7 @@ class TestMiddlewareDecoratorLogic: tools=[custom_tool_wrapped], ) - response = await agent.run([ChatMessage(role=Role.USER, text="test")]) + response = await agent.run([ChatMessage("user", ["test"])]) assert response is not None assert "decorator_type_match_agent" in execution_order @@ -1317,7 +1314,7 @@ class TestMiddlewareDecoratorLogic: await next(context) agent = ChatAgent(chat_client=chat_client, middleware=[mismatched_middleware]) - await agent.run([ChatMessage(role=Role.USER, text="test")]) + await agent.run([ChatMessage("user", ["test"])]) async def test_only_decorator_specified(self, chat_client: Any) -> None: """Only decorator specified - rely on decorator.""" @@ -1346,7 +1343,7 @@ class TestMiddlewareDecoratorLogic: function_call_response = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="test_call", @@ -1357,7 +1354,7 @@ class TestMiddlewareDecoratorLogic: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Final response")]) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["Final response"])]) chat_client.responses = [function_call_response, final_response] # Should work - relies on decorator @@ -1367,7 +1364,7 @@ class TestMiddlewareDecoratorLogic: tools=[custom_tool_wrapped], ) - response = await agent.run([ChatMessage(role=Role.USER, text="test")]) + response = await agent.run([ChatMessage("user", ["test"])]) assert response is not None assert "decorator_only_agent" in execution_order @@ -1402,7 +1399,7 @@ class TestMiddlewareDecoratorLogic: function_call_response = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="test_call", @@ -1413,7 +1410,7 @@ class TestMiddlewareDecoratorLogic: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Final response")]) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["Final response"])]) chat_client.responses = [function_call_response, final_response] # Should work - relies on type annotations @@ -1421,7 +1418,7 @@ class TestMiddlewareDecoratorLogic: chat_client=chat_client, middleware=[type_only_agent, type_only_function], tools=[custom_tool_wrapped] ) - response = await agent.run([ChatMessage(role=Role.USER, text="test")]) + response = await agent.run([ChatMessage("user", ["test"])]) assert response is not None assert "type_only_agent" in execution_order @@ -1436,7 +1433,7 @@ class TestMiddlewareDecoratorLogic: # Should raise MiddlewareException with pytest.raises(MiddlewareException, match="Cannot determine middleware type"): agent = ChatAgent(chat_client=chat_client, middleware=[no_info_middleware]) - await agent.run([ChatMessage(role=Role.USER, text="test")]) + await agent.run([ChatMessage("user", ["test"])]) async def test_insufficient_parameters_error(self, chat_client: Any) -> None: """Test that middleware with insufficient parameters raises an error.""" @@ -1450,7 +1447,7 @@ class TestMiddlewareDecoratorLogic: pass agent = ChatAgent(chat_client=chat_client, middleware=[insufficient_params_middleware]) - await agent.run([ChatMessage(role=Role.USER, text="test")]) + await agent.run([ChatMessage("user", ["test"])]) async def test_decorator_markers_preserved(self) -> None: """Test that decorator markers are properly set on functions.""" @@ -1523,7 +1520,7 @@ class TestChatAgentThreadBehavior: thread = agent.get_new_thread() # First run - first_messages = [ChatMessage(role=Role.USER, text="first message")] + first_messages = [ChatMessage("user", ["first message"])] first_response = await agent.run(first_messages, thread=thread) # Verify first response @@ -1531,7 +1528,7 @@ class TestChatAgentThreadBehavior: assert len(first_response.messages) > 0 # Second run - use the same thread - second_messages = [ChatMessage(role=Role.USER, text="second message")] + second_messages = [ChatMessage("user", ["second message"])] second_response = await agent.run(second_messages, thread=thread) # Verify second response @@ -1603,13 +1600,13 @@ class TestChatAgentChatMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response assert response is not None assert len(response.messages) > 0 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert "test response" in response.messages[0].text assert execution_order == ["chat_middleware_before", "chat_middleware_after"] @@ -1629,13 +1626,13 @@ class TestChatAgentChatMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[tracking_chat_middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response assert response is not None assert len(response.messages) > 0 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert "test response" in response.messages[0].text assert execution_order == ["chat_middleware_before", "chat_middleware_after"] @@ -1649,10 +1646,10 @@ class TestChatAgentChatMiddleware: # Modify the first message by adding a prefix if context.messages: for idx, msg in enumerate(context.messages): - if msg.role.value == "system": + if msg.role == "system": continue original_text = msg.text or "" - context.messages[idx] = ChatMessage(role=msg.role, text=f"MODIFIED: {original_text}") + context.messages[idx] = ChatMessage(msg.role, [f"MODIFIED: {original_text}"]) break await next(context) @@ -1661,7 +1658,7 @@ class TestChatAgentChatMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[message_modifier_middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify that the message was modified (MockBaseChatClient echoes back the input) @@ -1677,7 +1674,7 @@ class TestChatAgentChatMiddleware: ) -> None: # Override the response without calling next() context.result = ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Middleware overridden response")], + messages=[ChatMessage("assistant", ["Middleware overridden response"])], response_id="middleware-response-123", ) context.terminate = True @@ -1687,7 +1684,7 @@ class TestChatAgentChatMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[response_override_middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify that the response was overridden @@ -1717,7 +1714,7 @@ class TestChatAgentChatMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[first_middleware, second_middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response @@ -1743,13 +1740,13 @@ class TestChatAgentChatMiddleware: # Set up mock streaming responses chat_client.streaming_responses = [ [ - ChatResponseUpdate(contents=[Content.from_text(text="Stream")], role=Role.ASSISTANT), - ChatResponseUpdate(contents=[Content.from_text(text=" response")], role=Role.ASSISTANT), + ChatResponseUpdate(contents=[Content.from_text(text="Stream")], role="assistant"), + ChatResponseUpdate(contents=[Content.from_text(text=" response")], role="assistant"), ] ] # Execute streaming - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] updates: list[AgentResponseUpdate] = [] async for update in agent.run_stream(messages): updates.append(update) @@ -1770,9 +1767,7 @@ class TestChatAgentChatMiddleware: execution_order.append("middleware_before") context.terminate = True # Set a custom response since we're terminating - context.result = ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Terminated by middleware")] - ) + context.result = ChatResponse(messages=[ChatMessage("assistant", ["Terminated by middleware"])]) # We call next() but since terminate=True, execution should stop await next(context) execution_order.append("middleware_after") @@ -1782,7 +1777,7 @@ class TestChatAgentChatMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[PreTerminationChatMiddleware()]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response was from middleware @@ -1807,7 +1802,7 @@ class TestChatAgentChatMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[PostTerminationChatMiddleware()]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response is from actual execution @@ -1843,7 +1838,7 @@ class TestChatAgentChatMiddleware: function_call_response = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="call_456", @@ -1854,7 +1849,7 @@ class TestChatAgentChatMiddleware: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Final response")]) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["Final response"])]) chat_client = use_function_invocation(MockBaseChatClient)() chat_client.run_responses = [function_call_response, final_response] @@ -1867,7 +1862,7 @@ class TestChatAgentChatMiddleware: ) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="Get weather for San Francisco")] + messages = [ChatMessage("user", ["Get weather for San Francisco"])] response = await agent.run(messages) # Verify response @@ -1924,7 +1919,7 @@ class TestChatAgentChatMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[kwargs_middleware]) # Execute the agent with custom parameters - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages, temperature=0.7, max_tokens=100, custom_param="test_value") # Verify response @@ -1973,7 +1968,7 @@ class TestMiddlewareWithProtocolOnlyAgent: self.middleware = [TrackingMiddleware()] async def run(self, messages=None, *, thread=None, **kwargs) -> AgentResponse: - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["response"])]) def run_stream(self, messages=None, *, thread=None, **kwargs) -> AsyncIterable[AgentResponseUpdate]: async def _stream(): diff --git a/python/packages/core/tests/core/test_middleware_with_chat.py b/python/packages/core/tests/core/test_middleware_with_chat.py index ef2f6f3c09..a3893e1a6e 100644 --- a/python/packages/core/tests/core/test_middleware_with_chat.py +++ b/python/packages/core/tests/core/test_middleware_with_chat.py @@ -12,7 +12,6 @@ from agent_framework import ( Content, FunctionInvocationContext, FunctionTool, - Role, chat_middleware, function_middleware, use_chat_middleware, @@ -43,13 +42,13 @@ class TestChatMiddleware: chat_client_base.middleware = [LoggingChatMiddleware()] # Execute chat client directly - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await chat_client_base.get_response(messages) # Verify response assert response is not None assert len(response.messages) > 0 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" # Verify middleware execution order assert execution_order == ["chat_middleware_before", "chat_middleware_after"] @@ -68,13 +67,13 @@ class TestChatMiddleware: chat_client_base.middleware = [logging_chat_middleware] # Execute chat client directly - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await chat_client_base.get_response(messages) # Verify response assert response is not None assert len(response.messages) > 0 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" # Verify middleware execution order assert execution_order == ["function_middleware_before", "function_middleware_after"] @@ -89,14 +88,14 @@ class TestChatMiddleware: # Modify the first message by adding a prefix if context.messages and len(context.messages) > 0: original_text = context.messages[0].text or "" - context.messages[0] = ChatMessage(role=context.messages[0].role, text=f"MODIFIED: {original_text}") + context.messages[0] = ChatMessage(context.messages[0].role, [f"MODIFIED: {original_text}"]) await next(context) # Add middleware to chat client chat_client_base.middleware = [message_modifier_middleware] # Execute chat client - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await chat_client_base.get_response(messages) # Verify that the message was modified (MockChatClient echoes back the input) @@ -114,7 +113,7 @@ class TestChatMiddleware: ) -> None: # Override the response without calling next() context.result = ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Middleware overridden response")], + messages=[ChatMessage("assistant", ["Middleware overridden response"])], response_id="middleware-response-123", ) context.terminate = True @@ -123,7 +122,7 @@ class TestChatMiddleware: chat_client_base.middleware = [response_override_middleware] # Execute chat client - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await chat_client_base.get_response(messages) # Verify that the response was overridden @@ -152,7 +151,7 @@ class TestChatMiddleware: chat_client_base.middleware = [first_middleware, second_middleware] # Execute chat client - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await chat_client_base.get_response(messages) # Verify response @@ -180,13 +179,13 @@ class TestChatMiddleware: agent = ChatAgent(chat_client=chat_client, middleware=[agent_level_chat_middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response assert response is not None assert len(response.messages) > 0 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" # Verify middleware execution order assert execution_order == ["agent_chat_middleware_before", "agent_chat_middleware_after"] @@ -211,7 +210,7 @@ class TestChatMiddleware: agent = ChatAgent(chat_client=chat_client_base, middleware=[first_middleware, second_middleware]) # Execute the agent - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await agent.run(messages) # Verify response @@ -237,7 +236,7 @@ class TestChatMiddleware: chat_client_base.middleware = [streaming_middleware] # Execute streaming response - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] updates: list[object] = [] async for update in chat_client_base.get_streaming_response(messages): updates.append(update) @@ -258,19 +257,19 @@ class TestChatMiddleware: await next(context) # First call with run-level middleware - messages = [ChatMessage(role=Role.USER, text="first message")] + messages = [ChatMessage("user", ["first message"])] response1 = await chat_client_base.get_response(messages, middleware=[counting_middleware]) assert response1 is not None assert execution_count["count"] == 1 # Second call WITHOUT run-level middleware - should not execute the middleware - messages = [ChatMessage(role=Role.USER, text="second message")] + messages = [ChatMessage("user", ["second message"])] response2 = await chat_client_base.get_response(messages) assert response2 is not None assert execution_count["count"] == 1 # Should still be 1, not 2 # Third call with run-level middleware again - should execute - messages = [ChatMessage(role=Role.USER, text="third message")] + messages = [ChatMessage("user", ["third message"])] response3 = await chat_client_base.get_response(messages, middleware=[counting_middleware]) assert response3 is not None assert execution_count["count"] == 2 # Should be 2 now @@ -301,7 +300,7 @@ class TestChatMiddleware: chat_client_base.middleware = [kwargs_middleware] # Execute chat client with custom parameters - messages = [ChatMessage(role=Role.USER, text="test message")] + messages = [ChatMessage("user", ["test message"])] response = await chat_client_base.get_response( messages, temperature=0.7, max_tokens=100, custom_param="test_value" ) @@ -355,7 +354,7 @@ class TestChatMiddleware: function_call_response = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="call_1", @@ -366,14 +365,12 @@ class TestChatMiddleware: ) ] ) - final_response = ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Based on the weather data, it's sunny!")] - ) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["Based on the weather data, it's sunny!"])]) chat_client.run_responses = [function_call_response, final_response] # Execute the chat client directly with tools - this should trigger function invocation and middleware - messages = [ChatMessage(role=Role.USER, text="What's the weather in San Francisco?")] + messages = [ChatMessage("user", ["What's the weather in San Francisco?"])] response = await chat_client.get_response(messages, options={"tools": [sample_tool_wrapped]}) # Verify response @@ -418,7 +415,7 @@ class TestChatMiddleware: function_call_response = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_function_call( call_id="call_2", @@ -430,13 +427,13 @@ class TestChatMiddleware: ] ) final_response = ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="The weather information has been retrieved!")] + messages=[ChatMessage("assistant", ["The weather information has been retrieved!"])] ) chat_client.run_responses = [function_call_response, final_response] # Execute the chat client directly with run-level middleware and tools - messages = [ChatMessage(role=Role.USER, text="What's the weather in New York?")] + messages = [ChatMessage("user", ["What's the weather in New York?"])] response = await chat_client.get_response( messages, options={"tools": [sample_tool_wrapped]}, middleware=[run_level_function_middleware] ) diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index 3818a057bb..726f19c1af 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -14,12 +14,13 @@ from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, AgentProtocol, AgentResponse, + AgentResponseUpdate, AgentThread, BaseChatClient, ChatMessage, ChatResponse, ChatResponseUpdate, - Role, + Content, UsageDetails, prepend_agent_framework_to_user_agent, tool, @@ -217,7 +218,7 @@ def mock_chat_client(): self, *, messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any ): return ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Test response")], + messages=[ChatMessage("assistant", ["Test response"])], usage_details=UsageDetails(input_token_count=10, output_token_count=20), finish_reason=None, ) @@ -225,8 +226,8 @@ def mock_chat_client(): async def _inner_get_streaming_response( self, *, messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any ): - yield ChatResponseUpdate(text="Hello", role=Role.ASSISTANT) - yield ChatResponseUpdate(text=" world", role=Role.ASSISTANT) + yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")], role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(text=" world")], role="assistant") return MockChatClient @@ -236,7 +237,7 @@ async def test_chat_client_observability(mock_chat_client, span_exporter: InMemo """Test that when diagnostics are enabled, telemetry is applied.""" client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test message")] + messages = [ChatMessage("user", ["Test message"])] span_exporter.clear() response = await client.get_response(messages=messages, model_id="Test") assert response is not None @@ -259,7 +260,7 @@ async def test_chat_client_streaming_observability( ): """Test streaming telemetry through the use_instrumentation decorator.""" client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] span_exporter.clear() # Collect all yielded updates updates = [] @@ -288,7 +289,7 @@ async def test_chat_client_observability_with_instructions( client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test message")] + messages = [ChatMessage("user", ["Test message"])] options = {"model_id": "Test", "instructions": "You are a helpful assistant."} span_exporter.clear() response = await client.get_response(messages=messages, options=options) @@ -317,7 +318,7 @@ async def test_chat_client_streaming_observability_with_instructions( import json client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] options = {"model_id": "Test", "instructions": "You are a helpful assistant."} span_exporter.clear() @@ -344,7 +345,7 @@ async def test_chat_client_observability_without_instructions( """Test that system_instructions attribute is not set when instructions are not provided.""" client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test message")] + messages = [ChatMessage("user", ["Test message"])] options = {"model_id": "Test"} # No instructions span_exporter.clear() response = await client.get_response(messages=messages, options=options) @@ -365,7 +366,7 @@ async def test_chat_client_observability_with_empty_instructions( """Test that system_instructions attribute is not set when instructions is an empty string.""" client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test message")] + messages = [ChatMessage("user", ["Test message"])] options = {"model_id": "Test", "instructions": ""} # Empty string span_exporter.clear() response = await client.get_response(messages=messages, options=options) @@ -388,7 +389,7 @@ async def test_chat_client_observability_with_list_instructions( client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test message")] + messages = [ChatMessage("user", ["Test message"])] options = {"model_id": "Test", "instructions": ["Instruction 1", "Instruction 2"]} span_exporter.clear() response = await client.get_response(messages=messages, options=options) @@ -409,7 +410,7 @@ async def test_chat_client_observability_with_list_instructions( async def test_chat_client_without_model_id_observability(mock_chat_client, span_exporter: InMemorySpanExporter): """Test telemetry shouldn't fail when the model_id is not provided for unknown reason.""" client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] span_exporter.clear() response = await client.get_response(messages=messages) @@ -428,7 +429,7 @@ async def test_chat_client_streaming_without_model_id_observability( ): """Test streaming telemetry shouldn't fail when the model_id is not provided for unknown reason.""" client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] span_exporter.clear() # Collect all yielded updates updates = [] @@ -535,17 +536,16 @@ def mock_chat_agent(): async def run(self, messages=None, *, thread=None, **kwargs): return AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Agent response")], + messages=[ChatMessage("assistant", ["Agent response"])], usage_details=UsageDetails(input_token_count=15, output_token_count=25), response_id="test_response_id", raw_representation=Mock(finish_reason=Mock(value="stop")), ) async def run_stream(self, messages=None, *, thread=None, **kwargs): - from agent_framework import AgentResponseUpdate - yield AgentResponseUpdate(text="Hello", role=Role.ASSISTANT) - yield AgentResponseUpdate(text=" from agent", role=Role.ASSISTANT) + yield AgentResponseUpdate(contents=[Content.from_text(text="Hello")], role="assistant") + yield AgentResponseUpdate(contents=[Content.from_text(text=" from agent")], role="assistant") return MockChatClientAgent @@ -1338,7 +1338,7 @@ async def test_chat_client_observability_exception(mock_chat_client, span_export raise ValueError("Test error") client = use_instrumentation(FailingChatClient)() - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] span_exporter.clear() with pytest.raises(ValueError, match="Test error"): @@ -1356,11 +1356,11 @@ async def test_chat_client_streaming_observability_exception(mock_chat_client, s class FailingStreamingChatClient(mock_chat_client): async def _inner_get_streaming_response(self, *, messages, options, **kwargs): - yield ChatResponseUpdate(text="Hello", role=Role.ASSISTANT) + yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")], role="assistant") raise ValueError("Streaming error") client = use_instrumentation(FailingStreamingChatClient)() - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] span_exporter.clear() with pytest.raises(ValueError, match="Streaming error"): @@ -1431,12 +1431,11 @@ def test_get_response_attributes_with_finish_reason(): """Test _get_response_attributes includes finish_reason.""" from unittest.mock import Mock - from agent_framework import FinishReason from agent_framework.observability import OtelAttr, _get_response_attributes response = Mock() response.response_id = None - response.finish_reason = FinishReason.STOP + response.finish_reason = "stop" response.raw_representation = None response.usage_details = None @@ -1608,11 +1607,10 @@ def test_get_response_attributes_finish_reason_from_raw(): """Test _get_response_attributes gets finish_reason from raw_representation.""" from unittest.mock import Mock - from agent_framework import FinishReason from agent_framework.observability import OtelAttr, _get_response_attributes raw_rep = Mock() - raw_rep.finish_reason = FinishReason.LENGTH + raw_rep.finish_reason = "length" response = Mock() response.response_id = None @@ -1668,8 +1666,7 @@ async def test_agent_observability(span_exporter: InMemorySpanExporter, enable_s **kwargs, ): return AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Test response")], - thread=thread, + messages=[ChatMessage("assistant", ["Test response"])], ) async def run_stream( @@ -1679,9 +1676,8 @@ async def test_agent_observability(span_exporter: InMemorySpanExporter, enable_s thread=None, **kwargs, ): - from agent_framework import AgentResponseUpdate - yield AgentResponseUpdate(text="Test", role=Role.ASSISTANT) + yield AgentResponseUpdate(contents=[Content.from_text(text="Test")], role="assistant") decorated_agent = use_agent_instrumentation(MockAgent) agent = decorated_agent() @@ -1697,7 +1693,6 @@ async def test_agent_observability(span_exporter: InMemorySpanExporter, enable_s @pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) async def test_agent_observability_with_exception(span_exporter: InMemorySpanExporter, enable_sensitive_data): """Test agent instrumentation captures exceptions.""" - from agent_framework import AgentResponseUpdate from agent_framework.observability import use_agent_instrumentation class FailingAgent(AgentProtocol): @@ -1730,7 +1725,7 @@ async def test_agent_observability_with_exception(span_exporter: InMemorySpanExp async def run_stream(self, messages=None, *, thread=None, **kwargs): # yield before raise to make this an async generator - yield AgentResponseUpdate(text="", role=Role.ASSISTANT) + yield AgentResponseUpdate(contents=[Content.from_text(text="")], role="assistant") raise RuntimeError("Agent failed") decorated_agent = use_agent_instrumentation(FailingAgent) @@ -1751,7 +1746,6 @@ async def test_agent_observability_with_exception(span_exporter: InMemorySpanExp @pytest.mark.parametrize("enable_sensitive_data", [True, False], indirect=True) async def test_agent_streaming_observability(span_exporter: InMemorySpanExporter, enable_sensitive_data): """Test agent streaming instrumentation.""" - from agent_framework import AgentResponseUpdate from agent_framework.observability import use_agent_instrumentation class StreamingAgent(AgentProtocol): @@ -1781,13 +1775,12 @@ async def test_agent_streaming_observability(span_exporter: InMemorySpanExporter async def run(self, messages=None, *, thread=None, **kwargs): return AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Test")], - thread=thread, + messages=[ChatMessage("assistant", ["Test"])], ) async def run_stream(self, messages=None, *, thread=None, **kwargs): - yield AgentResponseUpdate(text="Hello ", role=Role.ASSISTANT) - yield AgentResponseUpdate(text="World", role=Role.ASSISTANT) + yield AgentResponseUpdate(contents=[Content.from_text(text="Hello ")], role="assistant") + yield AgentResponseUpdate(contents=[Content.from_text(text="World")], role="assistant") decorated_agent = use_agent_instrumentation(StreamingAgent) agent = decorated_agent() @@ -1836,24 +1829,22 @@ async def test_capture_messages_with_finish_reason(mock_chat_client, span_export """Test that finish_reason is captured in output messages.""" import json - from agent_framework import FinishReason - class ClientWithFinishReason(mock_chat_client): async def _inner_get_response(self, *, messages, options, **kwargs): return ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Done")], + messages=[ChatMessage("assistant", ["Done"])], usage_details=UsageDetails(input_token_count=5, output_token_count=10), - finish_reason=FinishReason.STOP, + finish_reason="stop", ) client = use_instrumentation(ClientWithFinishReason)() - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] span_exporter.clear() response = await client.get_response(messages=messages, model_id="Test") assert response is not None - assert response.finish_reason == FinishReason.STOP + assert response.finish_reason == "stop" spans = span_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] @@ -1869,7 +1860,6 @@ async def test_capture_messages_with_finish_reason(mock_chat_client, span_export @pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) async def test_agent_streaming_exception(span_exporter: InMemorySpanExporter, enable_sensitive_data): """Test agent streaming captures exceptions.""" - from agent_framework import AgentResponseUpdate from agent_framework.observability import use_agent_instrumentation class FailingStreamingAgent(AgentProtocol): @@ -1898,10 +1888,10 @@ async def test_agent_streaming_exception(span_exporter: InMemorySpanExporter, en return self._default_options async def run(self, messages=None, *, thread=None, **kwargs): - return AgentResponse(messages=[], thread=thread) + return AgentResponse(messages=[]) async def run_stream(self, messages=None, *, thread=None, **kwargs): - yield AgentResponseUpdate(text="Starting", role=Role.ASSISTANT) + yield AgentResponseUpdate(contents=[Content.from_text(text="Starting")], role="assistant") raise RuntimeError("Stream failed") decorated_agent = use_agent_instrumentation(FailingStreamingAgent) @@ -1924,7 +1914,7 @@ async def test_agent_streaming_exception(span_exporter: InMemorySpanExporter, en async def test_chat_client_when_disabled(mock_chat_client, span_exporter: InMemorySpanExporter): """Test that no spans are created when instrumentation is disabled.""" client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] span_exporter.clear() response = await client.get_response(messages=messages, model_id="Test") @@ -1939,7 +1929,7 @@ async def test_chat_client_when_disabled(mock_chat_client, span_exporter: InMemo async def test_chat_client_streaming_when_disabled(mock_chat_client, span_exporter: InMemorySpanExporter): """Test streaming creates no spans when instrumentation is disabled.""" client = use_instrumentation(mock_chat_client)() - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] span_exporter.clear() updates = [] @@ -1982,12 +1972,11 @@ async def test_agent_when_disabled(span_exporter: InMemorySpanExporter): return self._default_options async def run(self, messages=None, *, thread=None, **kwargs): - return AgentResponse(messages=[], thread=thread) + return AgentResponse(messages=[]) async def run_stream(self, messages=None, *, thread=None, **kwargs): - from agent_framework import AgentResponseUpdate - yield AgentResponseUpdate(text="test", role=Role.ASSISTANT) + yield AgentResponseUpdate(contents=[Content.from_text(text="test")], role="assistant") decorated = use_agent_instrumentation(TestAgent) agent = decorated() @@ -2002,7 +1991,6 @@ async def test_agent_when_disabled(span_exporter: InMemorySpanExporter): @pytest.mark.parametrize("enable_instrumentation", [False], indirect=True) async def test_agent_streaming_when_disabled(span_exporter: InMemorySpanExporter): """Test agent streaming creates no spans when disabled.""" - from agent_framework import AgentResponseUpdate from agent_framework.observability import use_agent_instrumentation class TestAgent(AgentProtocol): @@ -2031,10 +2019,10 @@ async def test_agent_streaming_when_disabled(span_exporter: InMemorySpanExporter return self._default_options async def run(self, messages=None, *, thread=None, **kwargs): - return AgentResponse(messages=[], thread=thread) + return AgentResponse(messages=[]) async def run_stream(self, messages=None, *, thread=None, **kwargs): - yield AgentResponseUpdate(text="test", role=Role.ASSISTANT) + yield AgentResponseUpdate(contents=[Content.from_text(text="test")], role="assistant") decorated = use_agent_instrumentation(TestAgent) agent = decorated() diff --git a/python/packages/core/tests/core/test_threads.py b/python/packages/core/tests/core/test_threads.py index 01d5ceb98f..241cbf4a90 100644 --- a/python/packages/core/tests/core/test_threads.py +++ b/python/packages/core/tests/core/test_threads.py @@ -5,7 +5,7 @@ from typing import Any import pytest -from agent_framework import AgentThread, ChatMessage, ChatMessageStore, Role +from agent_framework import AgentThread, ChatMessage, ChatMessageStore from agent_framework._threads import AgentThreadState, ChatMessageStoreState from agent_framework.exceptions import AgentThreadException @@ -44,16 +44,16 @@ class MockChatMessageStore: def sample_messages() -> list[ChatMessage]: """Fixture providing sample chat messages for testing.""" return [ - ChatMessage(role=Role.USER, text="Hello", message_id="msg1"), - ChatMessage(role=Role.ASSISTANT, text="Hi there!", message_id="msg2"), - ChatMessage(role=Role.USER, text="How are you?", message_id="msg3"), + ChatMessage("user", ["Hello"], message_id="msg1"), + ChatMessage("assistant", ["Hi there!"], message_id="msg2"), + ChatMessage("user", ["How are you?"], message_id="msg3"), ] @pytest.fixture def sample_message() -> ChatMessage: """Fixture providing a single sample chat message for testing.""" - return ChatMessage(role=Role.USER, text="Test message", message_id="test1") + return ChatMessage("user", ["Test message"], message_id="test1") class TestAgentThread: @@ -178,7 +178,7 @@ class TestAgentThread: async def test_on_new_messages_with_existing_store(self, sample_message: ChatMessage) -> None: """Test _on_new_messages adds to existing message store.""" - initial_messages = [ChatMessage(role=Role.USER, text="Initial", message_id="init1")] + initial_messages = [ChatMessage("user", ["Initial"], message_id="init1")] store = ChatMessageStore(initial_messages) thread = AgentThread(message_store=store) @@ -226,7 +226,7 @@ class TestAgentThread: thread = AgentThread(message_store=store) serialized_data: dict[str, Any] = { "service_thread_id": None, - "chat_message_store_state": {"messages": [ChatMessage(role="user", text="test")]}, + "chat_message_store_state": {"messages": [ChatMessage("user", ["test"])]}, } await thread.update_from_thread_state(serialized_data) @@ -449,7 +449,7 @@ class TestThreadState: def test_init_with_chat_message_store_state_object(self) -> None: """Test AgentThreadState initialization with ChatMessageStoreState object.""" - store_state = ChatMessageStoreState(messages=[ChatMessage(role=Role.USER, text="test")]) + store_state = ChatMessageStoreState(messages=[ChatMessage("user", ["test"])]) state = AgentThreadState(chat_message_store_state=store_state) assert state.service_thread_id is None diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index a60018c7a4..9187c9f0f3 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -959,7 +959,7 @@ def mock_chat_client(): return response # Default response return ChatResponse( - messages=[ChatMessage(role="assistant", contents=["Default response"])], + messages=[ChatMessage("assistant", ["Default response"])], ) async def get_streaming_response(self, messages, **kwargs): @@ -973,7 +973,7 @@ def mock_chat_client(): yield ChatResponseUpdate(contents=[content], role=msg.role) else: # Default response - yield ChatResponseUpdate(text="Default response", role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(text="Default response")], role="assistant") return MockChatClient() @@ -1015,7 +1015,7 @@ async def test_non_streaming_single_function_no_approval(): ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="The result is 10")]) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["The result is 10"])]) call_count = [0] responses = [initial_response, final_response] @@ -1100,7 +1100,7 @@ async def test_non_streaming_two_functions_both_no_approval(): ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="Both tools executed successfully")]) + final_response = ChatResponse(messages=[ChatMessage("assistant", ["Both tools executed successfully"])]) call_count = [0] responses = [initial_response, final_response] @@ -1227,7 +1227,7 @@ async def test_streaming_single_function_no_approval(): role="assistant", ) ] - final_updates = [ChatResponseUpdate(text="The result is 10", role="assistant")] + final_updates = [ChatResponseUpdate(contents=[Content.from_text(text="The result is 10")], role="assistant")] call_count = [0] updates_list = [initial_updates, final_updates] @@ -1246,13 +1246,12 @@ async def test_streaming_single_function_no_approval(): updates.append(update) # Verify: should have function call update, tool result update (injected), and final update - from agent_framework import Role assert len(updates) >= 3 # First update is the function call assert updates[0].contents[0].type == "function_call" # Second update should be the tool result (injected by the wrapper) - assert updates[1].role == Role.TOOL + assert updates[1].role == "tool" assert updates[1].contents[0].type == "function_result" assert updates[1].contents[0].result == 10 # 5 * 2 # Last update is the final message @@ -1294,11 +1293,10 @@ async def test_streaming_single_function_requires_approval(): updates.append(update) # Verify: should yield function call and then approval request - from agent_framework import Role assert len(updates) == 2 assert updates[0].contents[0].type == "function_call" - assert updates[1].role == Role.ASSISTANT + assert updates[1].role == "assistant" assert updates[1].contents[0].type == "function_approval_request" @@ -1319,7 +1317,9 @@ async def test_streaming_two_functions_both_no_approval(): role="assistant", ), ] - final_updates = [ChatResponseUpdate(text="Both tools executed successfully", role="assistant")] + final_updates = [ + ChatResponseUpdate(contents=[Content.from_text(text="Both tools executed successfully")], role="assistant") + ] call_count = [0] updates_list = [initial_updates, final_updates] @@ -1338,7 +1338,6 @@ async def test_streaming_two_functions_both_no_approval(): updates.append(update) # Verify: should have both function calls, one tool result update with both results, and final message - from agent_framework import Role assert len(updates) >= 2 # First update has both function calls @@ -1346,7 +1345,7 @@ async def test_streaming_two_functions_both_no_approval(): assert updates[0].contents[0].type == "function_call" assert updates[0].contents[1].type == "function_call" # Should have a tool result update with both results - tool_updates = [u for u in updates if u.role == Role.TOOL] + tool_updates = [u for u in updates if u.role == "tool"] assert len(tool_updates) == 1 assert len(tool_updates[0].contents) == 2 assert all(c.type == "function_result" for c in tool_updates[0].contents) @@ -1392,13 +1391,12 @@ async def test_streaming_two_functions_both_require_approval(): updates.append(update) # Verify: should yield both function calls and then approval requests - from agent_framework import Role assert len(updates) == 3 assert updates[0].contents[0].type == "function_call" assert updates[1].contents[0].type == "function_call" # Assistant update with both approval requests - assert updates[2].role == Role.ASSISTANT + assert updates[2].role == "assistant" assert len(updates[2].contents) == 2 assert all(c.type == "function_approval_request" for c in updates[2].contents) @@ -1443,13 +1441,12 @@ async def test_streaming_two_functions_mixed_approval(): updates.append(update) # Verify: should yield both function calls and then approval requests (when one needs approval, all wait) - from agent_framework import Role assert len(updates) == 3 assert updates[0].contents[0].type == "function_call" assert updates[1].contents[0].type == "function_call" # Assistant update with both approval requests - assert updates[2].role == Role.ASSISTANT + assert updates[2].role == "assistant" assert len(updates[2].contents) == 2 assert all(c.type == "function_approval_request" for c in updates[2].contents) diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 8236d75d20..3e7e435077 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -19,8 +19,6 @@ from agent_framework import ( ChatResponse, ChatResponseUpdate, Content, - FinishReason, - Role, TextSpanRegion, ToolMode, ToolProtocol, @@ -36,6 +34,8 @@ from agent_framework._types import ( _parse_content_list, _validate_uri, add_usage_details, + normalize_messages, + prepare_messages, validate_tool_mode, ) from agent_framework.exceptions import ContentError @@ -573,10 +573,10 @@ def test_ai_content_serialization(args: dict): def test_chat_message_text(): """Test the ChatMessage class to ensure it initializes correctly with text content.""" # Create a ChatMessage with a role and text content - message = ChatMessage(role="user", text="Hello, how are you?") + message = ChatMessage("user", ["Hello, how are you?"]) # Check the type and content - assert message.role == Role.USER + assert message.role == "user" assert len(message.contents) == 1 assert message.contents[0].type == "text" assert message.contents[0].text == "Hello, how are you?" @@ -591,10 +591,10 @@ def test_chat_message_contents(): # Create a ChatMessage with a role and multiple contents content1 = Content.from_text("Hello, how are you?") content2 = Content.from_text("I'm fine, thank you!") - message = ChatMessage(role="user", contents=[content1, content2]) + message = ChatMessage("user", [content1, content2]) # Check the type and content - assert message.role == Role.USER + assert message.role == "user" assert len(message.contents) == 2 assert message.contents[0].type == "text" assert message.contents[1].type == "text" @@ -604,8 +604,8 @@ def test_chat_message_contents(): def test_chat_message_with_chatrole_instance(): - m = ChatMessage(role=Role.USER, text="hi") - assert m.role == Role.USER + m = ChatMessage("user", ["hi"]) + assert m.role == "user" assert m.text == "hi" @@ -615,13 +615,13 @@ def test_chat_message_with_chatrole_instance(): def test_chat_response(): """Test the ChatResponse class to ensure it initializes correctly with a message.""" # Create a ChatMessage - message = ChatMessage(role="assistant", text="I'm doing well, thank you!") + message = ChatMessage("assistant", ["I'm doing well, thank you!"]) # Create a ChatResponse with the message response = ChatResponse(messages=message) # Check the type and content - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert response.messages[0].text == "I'm doing well, thank you!" assert isinstance(response.messages[0], ChatMessage) # __str__ returns text @@ -635,32 +635,30 @@ class OutputModel(BaseModel): def test_chat_response_with_format(): """Test the ChatResponse class to ensure it initializes correctly with a message.""" # Create a ChatMessage - message = ChatMessage(role="assistant", text='{"response": "Hello"}') + message = ChatMessage("assistant", ['{"response": "Hello"}']) # Create a ChatResponse with the message response = ChatResponse(messages=message) # Check the type and content - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert response.messages[0].text == '{"response": "Hello"}' assert isinstance(response.messages[0], ChatMessage) assert response.text == '{"response": "Hello"}' + # Since no response_format was provided, value is None and accessing it returns None assert response.value is None - response.try_parse_value(OutputModel) - assert response.value is not None - assert response.value.response == "Hello" def test_chat_response_with_format_init(): """Test the ChatResponse class to ensure it initializes correctly with a message.""" # Create a ChatMessage - message = ChatMessage(role="assistant", text='{"response": "Hello"}') + message = ChatMessage("assistant", ['{"response": "Hello"}']) # Create a ChatResponse with the message response = ChatResponse(messages=message, response_format=OutputModel) # Check the type and content - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert response.messages[0].text == '{"response": "Hello"}' assert isinstance(response.messages[0], ChatMessage) assert response.text == '{"response": "Hello"}' @@ -676,7 +674,7 @@ def test_chat_response_value_raises_on_invalid_schema(): name: str = Field(min_length=10) score: int = Field(gt=0, le=100) - message = ChatMessage(role="assistant", text='{"id": 1, "name": "test", "score": -5}') + message = ChatMessage("assistant", ['{"id": 1, "name": "test", "score": -5}']) response = ChatResponse(messages=message, response_format=StrictSchema) with raises(ValidationError) as exc_info: @@ -689,32 +687,17 @@ def test_chat_response_value_raises_on_invalid_schema(): assert "score" in error_fields, "Expected 'score' gt constraint error" -def test_chat_response_try_parse_value_returns_none_on_invalid(): - """Test that try_parse_value returns None on validation failure with Field constraints.""" - - class StrictSchema(BaseModel): - id: Literal[5] - name: str = Field(min_length=10) - score: int = Field(gt=0, le=100) - - message = ChatMessage(role="assistant", text='{"id": 1, "name": "test", "score": -5}') - response = ChatResponse(messages=message) - - result = response.try_parse_value(StrictSchema) - assert result is None - - -def test_chat_response_try_parse_value_returns_value_on_success(): - """Test that try_parse_value returns parsed value when all constraints pass.""" +def test_chat_response_value_with_valid_schema(): + """Test that value property returns parsed value when all constraints pass.""" class MySchema(BaseModel): name: str = Field(min_length=3) score: int = Field(ge=0, le=100) - message = ChatMessage(role="assistant", text='{"name": "test", "score": 85}') - response = ChatResponse(messages=message) + message = ChatMessage("assistant", ['{"name": "test", "score": 85}']) + response = ChatResponse(messages=message, response_format=MySchema) - result = response.try_parse_value(MySchema) + result = response.value assert result is not None assert result.name == "test" assert result.score == 85 @@ -728,7 +711,7 @@ def test_agent_response_value_raises_on_invalid_schema(): name: str = Field(min_length=10) score: int = Field(gt=0, le=100) - message = ChatMessage(role="assistant", text='{"id": 1, "name": "test", "score": -5}') + message = ChatMessage("assistant", ['{"id": 1, "name": "test", "score": -5}']) response = AgentResponse(messages=message, response_format=StrictSchema) with raises(ValidationError) as exc_info: @@ -741,32 +724,17 @@ def test_agent_response_value_raises_on_invalid_schema(): assert "score" in error_fields, "Expected 'score' gt constraint error" -def test_agent_response_try_parse_value_returns_none_on_invalid(): - """Test that AgentResponse.try_parse_value returns None on Field constraint failure.""" - - class StrictSchema(BaseModel): - id: Literal[5] - name: str = Field(min_length=10) - score: int = Field(gt=0, le=100) - - message = ChatMessage(role="assistant", text='{"id": 1, "name": "test", "score": -5}') - response = AgentResponse(messages=message) - - result = response.try_parse_value(StrictSchema) - assert result is None - - -def test_agent_response_try_parse_value_returns_value_on_success(): - """Test that AgentResponse.try_parse_value returns parsed value when all constraints pass.""" +def test_agent_response_value_with_valid_schema(): + """Test that AgentResponse.value property returns parsed value when all constraints pass.""" class MySchema(BaseModel): name: str = Field(min_length=3) score: int = Field(ge=0, le=100) - message = ChatMessage(role="assistant", text='{"name": "test", "score": 85}') - response = AgentResponse(messages=message) + message = ChatMessage("assistant", ['{"name": "test", "score": 85}']) + response = AgentResponse(messages=message, response_format=MySchema) - result = response.try_parse_value(MySchema) + result = response.value assert result is not None assert result.name == "test" assert result.score == 85 @@ -797,12 +765,12 @@ def test_chat_response_updates_to_chat_response_one(): # Create a ChatResponseUpdate with the message response_updates = [ - ChatResponseUpdate(text=message1, message_id="1"), - ChatResponseUpdate(text=message2, message_id="1"), + ChatResponseUpdate(contents=[message1], message_id="1"), + ChatResponseUpdate(contents=[message2], message_id="1"), ] # Convert to ChatResponse - chat_response = ChatResponse.from_chat_response_updates(response_updates) + chat_response = ChatResponse.from_updates(response_updates) # Check the type and content assert len(chat_response.messages) == 1 @@ -820,12 +788,12 @@ def test_chat_response_updates_to_chat_response_two(): # Create a ChatResponseUpdate with the message response_updates = [ - ChatResponseUpdate(text=message1, message_id="1"), - ChatResponseUpdate(text=message2, message_id="2"), + ChatResponseUpdate(contents=[message1], message_id="1"), + ChatResponseUpdate(contents=[message2], message_id="2"), ] # Convert to ChatResponse - chat_response = ChatResponse.from_chat_response_updates(response_updates) + chat_response = ChatResponse.from_updates(response_updates) # Check the type and content assert len(chat_response.messages) == 2 @@ -844,13 +812,13 @@ def test_chat_response_updates_to_chat_response_multiple(): # Create a ChatResponseUpdate with the message response_updates = [ - ChatResponseUpdate(text=message1, message_id="1"), + ChatResponseUpdate(contents=[message1], message_id="1"), ChatResponseUpdate(contents=[Content.from_text_reasoning(text="Additional context")], message_id="1"), - ChatResponseUpdate(text=message2, message_id="1"), + ChatResponseUpdate(contents=[message2], message_id="1"), ] # Convert to ChatResponse - chat_response = ChatResponse.from_chat_response_updates(response_updates) + chat_response = ChatResponse.from_updates(response_updates) # Check the type and content assert len(chat_response.messages) == 1 @@ -868,15 +836,15 @@ def test_chat_response_updates_to_chat_response_multiple_multiple(): # Create a ChatResponseUpdate with the message response_updates = [ - ChatResponseUpdate(text=message1, message_id="1"), - ChatResponseUpdate(text=message2, message_id="1"), + ChatResponseUpdate(contents=[message1], message_id="1"), + ChatResponseUpdate(contents=[message2], message_id="1"), ChatResponseUpdate(contents=[Content.from_text_reasoning(text="Additional context")], message_id="1"), ChatResponseUpdate(contents=[Content.from_text(text="More context")], message_id="1"), - ChatResponseUpdate(text="Final part", message_id="1"), + ChatResponseUpdate(contents=[Content.from_text(text="Final part")], message_id="1"), ] # Convert to ChatResponse - chat_response = ChatResponse.from_chat_response_updates(response_updates) + chat_response = ChatResponse.from_updates(response_updates) # Check the type and content assert len(chat_response.messages) == 1 @@ -897,32 +865,30 @@ def test_chat_response_updates_to_chat_response_multiple_multiple(): async def test_chat_response_from_async_generator(): async def gen() -> AsyncIterable[ChatResponseUpdate]: - yield ChatResponseUpdate(text="Hello", message_id="1") - yield ChatResponseUpdate(text=" world", message_id="1") + yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")], message_id="1") + yield ChatResponseUpdate(contents=[Content.from_text(text=" world")], message_id="1") - resp = await ChatResponse.from_chat_response_generator(gen()) + resp = await ChatResponse.from_update_generator(gen()) assert resp.text == "Hello world" async def test_chat_response_from_async_generator_output_format(): async def gen() -> AsyncIterable[ChatResponseUpdate]: - yield ChatResponseUpdate(text='{ "respon', message_id="1") - yield ChatResponseUpdate(text='se": "Hello" }', message_id="1") + yield ChatResponseUpdate(contents=[Content.from_text(text='{ "respon')], message_id="1") + yield ChatResponseUpdate(contents=[Content.from_text(text='se": "Hello" }')], message_id="1") - resp = await ChatResponse.from_chat_response_generator(gen()) + # Note: Without output_format_type, value is None and we cannot parse + resp = await ChatResponse.from_update_generator(gen()) assert resp.text == '{ "response": "Hello" }' assert resp.value is None - resp.try_parse_value(OutputModel) - assert resp.value is not None - assert resp.value.response == "Hello" async def test_chat_response_from_async_generator_output_format_in_method(): async def gen() -> AsyncIterable[ChatResponseUpdate]: - yield ChatResponseUpdate(text='{ "respon', message_id="1") - yield ChatResponseUpdate(text='se": "Hello" }', message_id="1") + yield ChatResponseUpdate(contents=[Content.from_text(text='{ "respon')], message_id="1") + yield ChatResponseUpdate(contents=[Content.from_text(text='se": "Hello" }')], message_id="1") - resp = await ChatResponse.from_chat_response_generator(gen(), output_format_type=OutputModel) + resp = await ChatResponse.from_update_generator(gen(), output_format_type=OutputModel) assert resp.text == '{ "response": "Hello" }' assert resp.value is not None assert resp.value.response == "Hello" @@ -1080,7 +1046,7 @@ def test_chat_options_and_tool_choice_required_specific_function() -> None: @fixture def chat_message() -> ChatMessage: - return ChatMessage(role=Role.USER, text="Hello") + return ChatMessage("user", ["Hello"]) @fixture @@ -1095,7 +1061,7 @@ def agent_response(chat_message: ChatMessage) -> AgentResponse: @fixture def agent_response_update(text_content: Content) -> AgentResponseUpdate: - return AgentResponseUpdate(role=Role.ASSISTANT, contents=[text_content]) + return AgentResponseUpdate(role="assistant", contents=[text_content]) # region AgentResponse @@ -1129,7 +1095,7 @@ def test_agent_run_response_text_property_empty() -> None: def test_agent_run_response_from_updates(agent_response_update: AgentResponseUpdate) -> None: updates = [agent_response_update, agent_response_update] - response = AgentResponse.from_agent_run_response_updates(updates) + response = AgentResponse.from_updates(updates) assert len(response.messages) > 0 assert response.text == "Test contentTest content" @@ -1174,7 +1140,7 @@ def test_agent_run_response_update_created_at() -> None: utc_timestamp = "2024-12-01T00:31:30.000000Z" update = AgentResponseUpdate( contents=[Content.from_text(text="test")], - role=Role.ASSISTANT, + role="assistant", created_at=utc_timestamp, ) assert update.created_at == utc_timestamp @@ -1185,7 +1151,7 @@ def test_agent_run_response_update_created_at() -> None: formatted_utc = now_utc.strftime("%Y-%m-%dT%H:%M:%S.%fZ") update_with_now = AgentResponseUpdate( contents=[Content.from_text(text="test")], - role=Role.ASSISTANT, + role="assistant", created_at=formatted_utc, ) assert update_with_now.created_at == formatted_utc @@ -1197,7 +1163,7 @@ def test_agent_run_response_created_at() -> None: # Test with a properly formatted UTC timestamp utc_timestamp = "2024-12-01T00:31:30.000000Z" response = AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Hello")], + messages=[ChatMessage("assistant", ["Hello"])], created_at=utc_timestamp, ) assert response.created_at == utc_timestamp @@ -1207,7 +1173,7 @@ def test_agent_run_response_created_at() -> None: now_utc = datetime.now(tz=timezone.utc) formatted_utc = now_utc.strftime("%Y-%m-%dT%H:%M:%S.%fZ") response_with_now = AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Hello")], + messages=[ChatMessage("assistant", ["Hello"])], created_at=formatted_utc, ) assert response_with_now.created_at == formatted_utc @@ -1271,7 +1237,7 @@ def test_function_call_merge_in_process_update_and_usage_aggregation(): # plus usage u3 = ChatResponseUpdate(contents=[Content.from_usage(UsageDetails(input_token_count=1, output_token_count=2))]) - resp = ChatResponse.from_chat_response_updates([u1, u2, u3]) + resp = ChatResponse.from_updates([u1, u2, u3]) assert len(resp.messages) == 1 last_contents = resp.messages[0].contents assert any(c.type == "function_call" for c in last_contents) @@ -1287,7 +1253,7 @@ def test_function_call_incompatible_ids_are_not_merged(): u1 = ChatResponseUpdate(contents=[Content.from_function_call(call_id="a", name="f", arguments="x")], message_id="m") u2 = ChatResponseUpdate(contents=[Content.from_function_call(call_id="b", name="f", arguments="y")], message_id="m") - resp = ChatResponse.from_chat_response_updates([u1, u2]) + resp = ChatResponse.from_updates([u1, u2]) fcs = [c for c in resp.messages[0].contents if c.type == "function_call"] assert len(fcs) == 2 @@ -1295,18 +1261,23 @@ def test_function_call_incompatible_ids_are_not_merged(): # region Role & FinishReason basics -def test_chat_role_str_and_repr(): - assert str(Role.USER) == "user" - assert "Role(value=" in repr(Role.USER) +def test_chat_role_is_string(): + """Role is now a NewType of str, so roles are just strings.""" + role = "user" + assert role == "user" + assert isinstance(role, str) -def test_chat_finish_reason_constants(): - assert FinishReason.STOP.value == "stop" +def test_chat_finish_reason_is_string(): + """FinishReason is now a NewType of str, so finish reasons are just strings.""" + finish_reason = "stop" + assert finish_reason == "stop" + assert isinstance(finish_reason, str) def test_response_update_propagates_fields_and_metadata(): upd = ChatResponseUpdate( - text="hello", + contents=[Content.from_text(text="hello")], role="assistant", author_name="bot", response_id="rid", @@ -1314,17 +1285,17 @@ def test_response_update_propagates_fields_and_metadata(): conversation_id="cid", model_id="model-x", created_at="t0", - finish_reason=FinishReason.STOP, + finish_reason="stop", additional_properties={"k": "v"}, ) - resp = ChatResponse.from_chat_response_updates([upd]) + resp = ChatResponse.from_updates([upd]) assert resp.response_id == "rid" assert resp.created_at == "t0" assert resp.conversation_id == "cid" assert resp.model_id == "model-x" - assert resp.finish_reason == FinishReason.STOP + assert resp.finish_reason == "stop" assert resp.additional_properties and resp.additional_properties["k"] == "v" - assert resp.messages[0].role == Role.ASSISTANT + assert resp.messages[0].role == "assistant" assert resp.messages[0].author_name == "bot" assert resp.messages[0].message_id == "mid" @@ -1332,9 +1303,9 @@ def test_response_update_propagates_fields_and_metadata(): def test_text_coalescing_preserves_first_properties(): t1 = Content.from_text("A", raw_representation={"r": 1}, additional_properties={"p": 1}) t2 = Content.from_text("B") - upd1 = ChatResponseUpdate(text=t1, message_id="x") - upd2 = ChatResponseUpdate(text=t2, message_id="x") - resp = ChatResponse.from_chat_response_updates([upd1, upd2]) + upd1 = ChatResponseUpdate(contents=[t1], message_id="x") + upd2 = ChatResponseUpdate(contents=[t2], message_id="x") + resp = ChatResponse.from_updates([upd1, upd2]) # After coalescing there should be a single TextContent with merged text and preserved props from first items = [c for c in resp.messages[0].contents if c.type == "text"] assert len(items) >= 1 @@ -1359,7 +1330,7 @@ def test_chat_tool_mode_eq_with_string(): @fixture def agent_run_response_async() -> AgentResponse: - return AgentResponse(messages=[ChatMessage(role="user", text="Hello")]) + return AgentResponse(messages=[ChatMessage("user", ["Hello"])]) async def test_agent_run_response_from_async_generator(): @@ -1587,7 +1558,7 @@ def test_chat_message_complex_content_serialization(): Content.from_function_result(call_id="call1", result="success"), ] - message = ChatMessage(role=Role.ASSISTANT, contents=contents) + message = ChatMessage("assistant", contents) # Test to_dict message_dict = message.to_dict() @@ -1663,7 +1634,7 @@ def test_chat_response_complex_serialization(): {"role": "user", "contents": [{"type": "text", "text": "Hello"}]}, {"role": "assistant", "contents": [{"type": "text", "text": "Hi there"}]}, ], - "finish_reason": {"value": "stop"}, + "finish_reason": "stop", "usage_details": { "type": "usage_details", "input_token_count": 5, @@ -1676,7 +1647,7 @@ def test_chat_response_complex_serialization(): response = ChatResponse.from_dict(response_data) assert len(response.messages) == 2 assert isinstance(response.messages[0], ChatMessage) - assert isinstance(response.finish_reason, FinishReason) + assert isinstance(response.finish_reason, str) assert isinstance(response.usage_details, dict) assert response.model_id == "gpt-4" # Should be stored as model_id @@ -1684,7 +1655,7 @@ def test_chat_response_complex_serialization(): response_dict = response.to_dict() assert len(response_dict["messages"]) == 2 assert isinstance(response_dict["messages"][0], dict) - assert isinstance(response_dict["finish_reason"], dict) + assert isinstance(response_dict["finish_reason"], str) assert isinstance(response_dict["usage_details"], dict) assert response_dict["model_id"] == "gpt-4" # Should serialize as model_id @@ -1794,20 +1765,20 @@ def test_agent_run_response_update_all_content_types(): update = AgentResponseUpdate.from_dict(update_data) assert len(update.contents) == 12 # unknown_type is logged and ignored - assert isinstance(update.role, Role) - assert update.role.value == "assistant" + assert isinstance(update.role, str) + assert update.role == "assistant" # Test to_dict with role conversion update_dict = update.to_dict() assert len(update_dict["contents"]) == 12 # unknown_type was ignored during from_dict - assert isinstance(update_dict["role"], dict) + assert isinstance(update_dict["role"], str) # Test role as string conversion update_data_str_role = update_data.copy() update_data_str_role["role"] = "user" update_str = AgentResponseUpdate.from_dict(update_data_str_role) - assert isinstance(update_str.role, Role) - assert update_str.role.value == "user" + assert isinstance(update_str.role, str) + assert update_str.role == "user" # region Serialization @@ -1936,7 +1907,7 @@ def test_agent_run_response_update_all_content_types(): pytest.param( ChatMessage, { - "role": {"type": "role", "value": "user"}, + "role": "user", "contents": [ {"type": "text", "text": "Hello"}, {"type": "function_call", "call_id": "call-1", "name": "test_func", "arguments": {}}, @@ -1953,16 +1924,16 @@ def test_agent_run_response_update_all_content_types(): "messages": [ { "type": "chat_message", - "role": {"type": "role", "value": "user"}, + "role": "user", "contents": [{"type": "text", "text": "Hello"}], }, { "type": "chat_message", - "role": {"type": "role", "value": "assistant"}, + "role": "assistant", "contents": [{"type": "text", "text": "Hi there"}], }, ], - "finish_reason": {"type": "finish_reason", "value": "stop"}, + "finish_reason": "stop", "usage_details": { "type": "usage_details", "input_token_count": 10, @@ -1981,8 +1952,8 @@ def test_agent_run_response_update_all_content_types(): {"type": "text", "text": "Hello"}, {"type": "function_call", "call_id": "call-1", "name": "test_func", "arguments": {}}, ], - "role": {"type": "role", "value": "assistant"}, - "finish_reason": {"type": "finish_reason", "value": "stop"}, + "role": "assistant", + "finish_reason": "stop", "message_id": "msg-123", "response_id": "resp-123", }, @@ -1993,11 +1964,11 @@ def test_agent_run_response_update_all_content_types(): { "messages": [ { - "role": {"type": "role", "value": "user"}, + "role": "user", "contents": [{"type": "text", "text": "Question"}], }, { - "role": {"type": "role", "value": "assistant"}, + "role": "assistant", "contents": [{"type": "text", "text": "Answer"}], }, ], @@ -2018,7 +1989,7 @@ def test_agent_run_response_update_all_content_types(): {"type": "text", "text": "Streaming"}, {"type": "function_call", "call_id": "call-1", "name": "test_func", "arguments": {}}, ], - "role": {"type": "role", "value": "assistant"}, + "role": "assistant", "message_id": "msg-123", "response_id": "run-123", "author_name": "Agent", @@ -2519,3 +2490,1046 @@ def test_validate_uri_data_uri(): # endregion + + +# region Test normalize_messages and prepare_messages with Content + + +def test_normalize_messages_with_string(): + """Test normalize_messages converts a string to a user message.""" + result = normalize_messages("hello") + assert len(result) == 1 + assert result[0].role == "user" + assert result[0].text == "hello" + + +def test_normalize_messages_with_content(): + """Test normalize_messages converts a Content object to a user message.""" + content = Content.from_text("hello") + result = normalize_messages(content) + assert len(result) == 1 + assert result[0].role == "user" + assert len(result[0].contents) == 1 + assert result[0].contents[0].text == "hello" + + +def test_normalize_messages_with_sequence_including_content(): + """Test normalize_messages handles a sequence with Content objects.""" + content = Content.from_text("image caption") + msg = ChatMessage("assistant", ["response"]) + result = normalize_messages(["query", content, msg]) + assert len(result) == 3 + assert result[0].role == "user" + assert result[0].text == "query" + assert result[1].role == "user" + assert result[1].contents[0].text == "image caption" + assert result[2].role == "assistant" + assert result[2].text == "response" + + +def test_prepare_messages_with_content(): + """Test prepare_messages converts a Content object to a user message.""" + content = Content.from_text("hello") + result = prepare_messages(content) + assert len(result) == 1 + assert result[0].role == "user" + assert result[0].contents[0].text == "hello" + + +def test_prepare_messages_with_content_and_system_instructions(): + """Test prepare_messages handles Content with system instructions.""" + content = Content.from_text("hello") + result = prepare_messages(content, system_instructions="Be helpful") + assert len(result) == 2 + assert result[0].role == "system" + assert result[0].text == "Be helpful" + assert result[1].role == "user" + assert result[1].contents[0].text == "hello" + + +def test_parse_content_list_with_strings(): + """Test _parse_content_list converts strings to TextContent.""" + result = _parse_content_list(["hello", "world"]) + assert len(result) == 2 + assert result[0].type == "text" + assert result[0].text == "hello" + assert result[1].type == "text" + assert result[1].text == "world" + + +def test_parse_content_list_with_none_values(): + """Test _parse_content_list skips None values.""" + result = _parse_content_list(["hello", None, "world", None]) + assert len(result) == 2 + assert result[0].text == "hello" + assert result[1].text == "world" + + +def test_parse_content_list_with_invalid_dict(): + """Test _parse_content_list raises on invalid content dict missing type.""" + # Invalid dict without type raises ValueError + with pytest.raises(ValueError, match="requires 'type'"): + _parse_content_list([{"invalid": "data"}]) + + +# region detect_media_type_from_base64 additional formats + + +def test_detect_media_type_gif87a(): + """Test detecting GIF87a format.""" + gif_data = b"GIF87a" + b"fake_data" + assert detect_media_type_from_base64(data_bytes=gif_data) == "image/gif" + + +def test_detect_media_type_bmp(): + """Test detecting BMP format.""" + bmp_data = b"BM" + b"fake_data" + assert detect_media_type_from_base64(data_bytes=bmp_data) == "image/bmp" + + +def test_detect_media_type_svg(): + """Test detecting SVG format.""" + svg_data = b" int: + """A simple function.""" + return x * 2 + + result = normalize_tools(my_func) + assert len(result) == 1 + assert hasattr(result[0], "name") + + +def test_normalize_tools_list_of_callables(): + """Test normalize_tools with list of callables.""" + from agent_framework._types import normalize_tools + + def func1(x: int) -> int: + """Function 1.""" + return x + + def func2(y: str) -> str: + """Function 2.""" + return y + + result = normalize_tools([func1, func2]) + assert len(result) == 2 + + +def test_normalize_tools_single_mapping(): + """Test normalize_tools with single mapping (not treated as sequence).""" + from agent_framework._types import normalize_tools + + tool_dict = {"name": "test_tool", "description": "A test tool"} + result = normalize_tools(tool_dict) + assert len(result) == 1 + assert result[0] == tool_dict + + +# region validate_tool_mode edge cases + + +def test_validate_tool_mode_dict_missing_mode(): + """Test validate_tool_mode with dict missing mode key.""" + with pytest.raises(ContentError, match="must contain 'mode' key"): + validate_tool_mode({"required_function_name": "test"}) + + +def test_validate_tool_mode_dict_invalid_mode(): + """Test validate_tool_mode with dict having invalid mode.""" + with pytest.raises(ContentError, match="Invalid tool choice"): + validate_tool_mode({"mode": "invalid"}) + + +def test_validate_tool_mode_dict_required_function_with_wrong_mode(): + """Test validate_tool_mode with required_function_name but wrong mode.""" + with pytest.raises(ContentError, match="cannot have 'required_function_name'"): + validate_tool_mode({"mode": "auto", "required_function_name": "test"}) + + +def test_validate_tool_mode_dict_valid_required(): + """Test validate_tool_mode with valid required mode and function name.""" + result = validate_tool_mode({"mode": "required", "required_function_name": "test"}) + assert result["mode"] == "required" + assert result["required_function_name"] == "test" + + +# region merge_chat_options edge cases + + +def test_merge_chat_options_instructions_concatenation(): + """Test merge_chat_options concatenates instructions.""" + base: ChatOptions = {"instructions": "Base instructions"} + override: ChatOptions = {"instructions": "Override instructions"} + result = merge_chat_options(base, override) + assert "Base instructions" in result["instructions"] + assert "Override instructions" in result["instructions"] + + +def test_merge_chat_options_tools_merge(): + """Test merge_chat_options merges tools lists.""" + + @tool + def tool1(x: int) -> int: + """Tool 1.""" + return x + + @tool + def tool2(y: int) -> int: + """Tool 2.""" + return y + + base: ChatOptions = {"tools": [tool1]} + override: ChatOptions = {"tools": [tool2]} + result = merge_chat_options(base, override) + assert len(result["tools"]) == 2 + + +def test_merge_chat_options_metadata_merge(): + """Test merge_chat_options merges metadata dicts.""" + base: ChatOptions = {"metadata": {"key1": "value1"}} + override: ChatOptions = {"metadata": {"key2": "value2"}} + result = merge_chat_options(base, override) + assert result["metadata"]["key1"] == "value1" + assert result["metadata"]["key2"] == "value2" + + +def test_merge_chat_options_tool_choice_override(): + """Test merge_chat_options overrides tool_choice.""" + base: ChatOptions = {"tool_choice": {"mode": "auto"}} + override: ChatOptions = {"tool_choice": {"mode": "required"}} + result = merge_chat_options(base, override) + assert result["tool_choice"]["mode"] == "required" + + +def test_merge_chat_options_response_format_override(): + """Test merge_chat_options overrides response_format.""" + + class Format1(BaseModel): + field1: str + + class Format2(BaseModel): + field2: str + + base: ChatOptions = {"response_format": Format1} + override: ChatOptions = {"response_format": Format2} + result = merge_chat_options(base, override) + assert result["response_format"] == Format2 + + +def test_merge_chat_options_skip_none_values(): + """Test merge_chat_options skips None values in override.""" + base: ChatOptions = {"temperature": 0.5} + override: ChatOptions = {"temperature": None} # type: ignore[typeddict-item] + result = merge_chat_options(base, override) + assert result["temperature"] == 0.5 + + +def test_merge_chat_options_logit_bias_merge(): + """Test merge_chat_options merges logit_bias dicts.""" + base: ChatOptions = {"logit_bias": {"token1": 1.0}} + override: ChatOptions = {"logit_bias": {"token2": -1.0}} + result = merge_chat_options(base, override) + assert result["logit_bias"]["token1"] == 1.0 + assert result["logit_bias"]["token2"] == -1.0 + + +def test_merge_chat_options_additional_properties_merge(): + """Test merge_chat_options merges additional_properties.""" + base: ChatOptions = {"additional_properties": {"prop1": "val1"}} + override: ChatOptions = {"additional_properties": {"prop2": "val2"}} + result = merge_chat_options(base, override) + assert result["additional_properties"]["prop1"] == "val1" + assert result["additional_properties"]["prop2"] == "val2" + + +# region ChatMessage with legacy role format + + +def test_chat_message_with_legacy_role_dict(): + """Test ChatMessage handles legacy role dict format.""" + message = ChatMessage({"value": "user"}, ["hello"]) # type: ignore[arg-type] + assert message.role == "user" + + +# region _get_data_bytes edge cases + + +def test_get_data_bytes_non_data_uri(): + """Test _get_data_bytes with non-data URI returns None.""" + content = Content.from_uri("https://example.com/image.png", media_type="image/png") + result = _get_data_bytes(content) + assert result is None + + +def test_get_data_bytes_invalid_encoding(): + """Test _get_data_bytes with invalid encoding raises error.""" + content = Content(type="data", uri="data:text/plain;utf8,hello") + with pytest.raises(ContentError, match="must use base64 encoding"): + _get_data_bytes(content) + + +# region Content addition edge cases + + +def test_content_add_different_types(): + """Test Content addition raises error for different types.""" + text_content = Content.from_text("hello") + function_call = Content.from_function_call(call_id="1", name="test", arguments={}) + with pytest.raises(TypeError, match="Cannot add Content of type"): + text_content + function_call + + +def test_content_add_unsupported_type(): + """Test Content addition raises error for unsupported types.""" + content1 = Content.from_uri("https://example.com/a.png", media_type="image/png") + content2 = Content.from_uri("https://example.com/b.png", media_type="image/png") + with pytest.raises(ContentError, match="Addition not supported"): + content1 + content2 + + +def test_content_add_text_with_annotations(): + """Test Content addition merges annotations.""" + ann1 = [Annotation(type="citation", text="ref1", start_char_index=0, end_char_index=5)] + ann2 = [Annotation(type="citation", text="ref2", start_char_index=0, end_char_index=5)] + content1 = Content.from_text("hello", annotations=ann1) + content2 = Content.from_text(" world", annotations=ann2) + result = content1 + content2 + assert result.text == "hello world" + assert len(result.annotations) == 2 + + +def test_content_add_text_reasoning_with_annotations(): + """Test text_reasoning Content addition merges annotations.""" + ann1 = [Annotation(type="citation", text="ref1", start_char_index=0, end_char_index=5)] + ann2 = [Annotation(type="citation", text="ref2", start_char_index=0, end_char_index=5)] + content1 = Content.from_text_reasoning(text="step 1", annotations=ann1) + content2 = Content.from_text_reasoning(text=" step 2", annotations=ann2) + result = content1 + content2 + assert result.text == "step 1 step 2" + assert len(result.annotations) == 2 + + +def test_content_add_text_with_raw_representation(): + """Test Content addition merges raw representations.""" + content1 = Content.from_text("hello", raw_representation={"raw": 1}) + content2 = Content.from_text(" world", raw_representation={"raw": 2}) + result = content1 + content2 + assert isinstance(result.raw_representation, list) + assert len(result.raw_representation) == 2 + + +def test_content_add_function_call_empty_arguments(): + """Test function_call Content addition with empty arguments.""" + content1 = Content.from_function_call(call_id="1", name="func", arguments="") + content2 = Content.from_function_call(call_id="1", name="func", arguments='{"x": 1}') + result = content1 + content2 + assert result.arguments == '{"x": 1}' + + +def test_content_add_function_call_raw_representation(): + """Test function_call Content addition merges raw representations.""" + content1 = Content.from_function_call(call_id="1", name="func", arguments='{"a": 1}', raw_representation={"r": 1}) + content2 = Content.from_function_call(call_id="1", name="func", arguments='{"b": 2}', raw_representation={"r": 2}) + result = content1 + content2 + assert isinstance(result.raw_representation, list) + + +# region ChatResponse and ChatResponseUpdate edge cases + + +def test_chat_response_from_dict_messages(): + """Test ChatResponse handles dict messages.""" + response = ChatResponse(messages=[{"role": "user", "contents": [{"type": "text", "text": "hello"}]}]) + assert len(response.messages) == 1 + assert response.messages[0].role == "user" + + +def test_chat_response_update_with_dict_contents(): + """Test ChatResponseUpdate handles dict contents.""" + update = ChatResponseUpdate( + contents=[{"type": "text", "text": "hello"}], + role="assistant", + ) + assert len(update.contents) == 1 + assert update.contents[0].type == "text" + + +def test_chat_response_update_legacy_role_dict(): + """Test ChatResponseUpdate handles legacy role dict format.""" + update = ChatResponseUpdate( + contents=[Content.from_text("hello")], + role={"value": "assistant"}, # type: ignore[arg-type] + ) + assert update.role == "assistant" + + +def test_chat_response_update_legacy_finish_reason_dict(): + """Test ChatResponseUpdate handles legacy finish_reason dict format.""" + update = ChatResponseUpdate( + contents=[Content.from_text("hello")], + finish_reason={"value": "stop"}, # type: ignore[arg-type] + ) + assert update.finish_reason == "stop" + + +def test_chat_response_update_str(): + """Test ChatResponseUpdate.__str__ returns text.""" + update = ChatResponseUpdate(contents=[Content.from_text("hello")]) + assert str(update) == "hello" + + +# region prepend_instructions_to_messages + + +def test_prepend_instructions_none(): + """Test prepend_instructions_to_messages with None instructions.""" + from agent_framework._types import prepend_instructions_to_messages + + messages = [ChatMessage("user", ["hello"])] + result = prepend_instructions_to_messages(messages, None) + assert result is messages + + +def test_prepend_instructions_string(): + """Test prepend_instructions_to_messages with string instructions.""" + from agent_framework._types import prepend_instructions_to_messages + + messages = [ChatMessage("user", ["hello"])] + result = prepend_instructions_to_messages(messages, "Be helpful") + assert len(result) == 2 + assert result[0].role == "system" + assert result[0].text == "Be helpful" + + +def test_prepend_instructions_list(): + """Test prepend_instructions_to_messages with list instructions.""" + from agent_framework._types import prepend_instructions_to_messages + + messages = [ChatMessage("user", ["hello"])] + result = prepend_instructions_to_messages(messages, ["First", "Second"]) + assert len(result) == 3 + assert result[0].text == "First" + assert result[1].text == "Second" + + +# region Process update edge cases + + +def test_process_update_dict_content(): + """Test _process_update handles dict content.""" + from agent_framework._types import _process_update + + response = ChatResponse(messages=[]) + update = ChatResponseUpdate( + contents=[{"type": "text", "text": "hello"}], # type: ignore[list-item] + role="assistant", + message_id="1", + ) + _process_update(response, update) + assert len(response.messages) == 1 + assert response.messages[0].text == "hello" + + +def test_process_update_with_additional_properties(): + """Test _process_update merges additional properties.""" + from agent_framework._types import _process_update + + response = ChatResponse(messages=[ChatMessage("assistant", ["hi"], message_id="1")]) + update = ChatResponseUpdate( + contents=[], + message_id="1", + additional_properties={"key": "value"}, + ) + _process_update(response, update) + assert response.additional_properties["key"] == "value" + + +def test_process_update_raw_representation_not_list(): + """Test _process_update converts raw_representation to list.""" + from agent_framework._types import _process_update + + response = ChatResponse(messages=[], raw_representation="initial") + update = ChatResponseUpdate( + contents=[Content.from_text("hi")], + role="assistant", + raw_representation="update", + ) + _process_update(response, update) + assert isinstance(response.raw_representation, list) + + +# region validate_tools async edge case + + +async def test_validate_tools_with_callable(): + """Test validate_tools with callable.""" + from agent_framework._types import validate_tools + + def my_func(x: int) -> int: + """A function.""" + return x + + result = await validate_tools(my_func) + assert len(result) == 1 + + +# region _get_data_bytes returns None for non-data types + + +def test_get_data_bytes_non_data_type(): + """Test _get_data_bytes returns None for non-data/uri type.""" + content = Content.from_text("hello") + result = _get_data_bytes(content) + assert result is None + + +def test_get_data_bytes_uri_type_no_data(): + """Test _get_data_bytes returns None for uri type (not data URI).""" + content = Content.from_uri("https://example.com/img.png", media_type="image/png") + result = _get_data_bytes(content) + assert result is None + + +def test_get_data_bytes_uri_without_uri_attr(): + """Test _get_data_bytes returns None when uri attribute is None.""" + content = Content(type="data") # No uri attribute + result = _get_data_bytes(content) + assert result is None + + +# region validate_uri edge cases for media_type without scheme + + +def test_validate_uri_with_scheme_no_media_type(): + """Test _validate_uri with http scheme but no media type logs warning.""" + result = _validate_uri("http://example.com/image.png", None) + assert result["type"] == "uri" + assert result["media_type"] is None + + +# region AgentResponse and AgentResponseUpdate edge cases + + +def test_agent_response_from_dict_messages(): + """Test AgentResponse handles dict messages.""" + response = AgentResponse(messages=[{"role": "user", "contents": [{"type": "text", "text": "hello"}]}]) + assert len(response.messages) == 1 + assert response.messages[0].role == "user" + + +def test_agent_response_update_with_dict_contents(): + """Test AgentResponseUpdate handles dict contents.""" + update = AgentResponseUpdate( + contents=[{"type": "text", "text": "hello"}], # type: ignore[list-item] + role="assistant", + ) + assert len(update.contents) == 1 + assert update.contents[0].type == "text" + + +def test_agent_response_update_legacy_role_dict(): + """Test AgentResponseUpdate handles legacy role dict format.""" + update = AgentResponseUpdate( + contents=[Content.from_text("hello")], + role={"value": "assistant"}, # type: ignore[arg-type] + ) + assert update.role == "assistant" + + +def test_agent_response_update_user_input_requests(): + """Test AgentResponseUpdate.user_input_requests property.""" + fc = Content.from_function_call(call_id="1", name="test", arguments={}) + req = Content.from_function_approval_request(id="req-1", function_call=fc) + update = AgentResponseUpdate(contents=[req, Content.from_text("hello")]) + requests = update.user_input_requests + assert len(requests) == 1 + assert requests[0].type == "function_approval_request" + + +def test_agent_response_user_input_requests(): + """Test AgentResponse.user_input_requests property.""" + fc = Content.from_function_call(call_id="1", name="test", arguments={}) + req = Content.from_function_approval_request(id="req-1", function_call=fc) + message = ChatMessage("assistant", [req, Content.from_text("hello")]) + response = AgentResponse(messages=[message]) + requests = response.user_input_requests + assert len(requests) == 1 + + +# region detect_media_type_from_base64 error for multiple arguments + + +def test_detect_media_type_from_base64_data_uri_and_bytes(): + """Test detect_media_type_from_base64 raises error for data_uri and data_bytes.""" + with pytest.raises(ValueError, match="Provide exactly one"): + detect_media_type_from_base64(data_bytes=b"test", data_uri="data:text/plain;base64,dGVzdA==") + + +# region Content.from_data type error + + +def test_content_from_data_type_error(): + """Test Content.from_data raises TypeError for non-bytes data.""" + with pytest.raises(TypeError, match="Could not encode data"): + Content.from_data("not bytes", "text/plain") # type: ignore[arg-type] + + +# region normalize_tools with single tool protocol + + +def test_normalize_tools_with_single_tool_protocol(ai_tool): + """Test normalize_tools with single ToolProtocol.""" + from agent_framework._types import normalize_tools + + result = normalize_tools(ai_tool) + assert len(result) == 1 + assert result[0] is ai_tool + + +# region text_reasoning content addition with None annotations + + +def test_content_add_text_reasoning_one_none_annotation(): + """Test text_reasoning Content addition with one None annotations.""" + content1 = Content.from_text_reasoning(text="step 1", annotations=None) + ann2 = [Annotation(type="citation", text="ref", start_char_index=0, end_char_index=3)] + content2 = Content.from_text_reasoning(text=" step 2", annotations=ann2) + result = content1 + content2 + assert result.text == "step 1 step 2" + assert result.annotations == ann2 + + +def test_content_add_text_reasoning_both_none_annotations(): + """Test text_reasoning Content addition with both None annotations.""" + content1 = Content.from_text_reasoning(text="step 1", annotations=None) + content2 = Content.from_text_reasoning(text=" step 2", annotations=None) + result = content1 + content2 + assert result.text == "step 1 step 2" + assert result.annotations is None + + +# region text content addition with one None annotation + + +def test_content_add_text_one_none_annotation(): + """Test text Content addition with one None annotations.""" + content1 = Content.from_text("hello", annotations=None) + ann2 = [Annotation(type="citation", text="ref", start_char_index=0, end_char_index=3)] + content2 = Content.from_text(" world", annotations=ann2) + result = content1 + content2 + assert result.text == "hello world" + assert result.annotations == ann2 + + +# region function_call content addition - both empty arguments + + +def test_content_add_function_call_both_empty(): + """Test function_call Content addition with both empty arguments.""" + content1 = Content.from_function_call(call_id="1", name="func", arguments=None) + content2 = Content.from_function_call(call_id="1", name="func", arguments=None) + result = content1 + content2 + assert result.arguments is None + + +# region process_update with invalid content dict + + +def test_process_update_with_invalid_content_dict(): + """Test _process_update logs warning for invalid content dicts.""" + from agent_framework._types import _process_update + + response = ChatResponse(messages=[ChatMessage("assistant", ["hi"], message_id="1")]) + # Create update with content that doesn't have a type attribute (None) + # The code checks getattr(content, "type", None) first + update = ChatResponseUpdate( + contents=[], # Empty contents to avoid the issue + message_id="1", + ) + # Just verify it doesn't crash + _process_update(response, update) + + +# endregion diff --git a/python/packages/core/tests/openai/test_openai_assistants_client.py b/python/packages/core/tests/openai/test_openai_assistants_client.py index 331dea2579..246c9fa841 100644 --- a/python/packages/core/tests/openai/test_openai_assistants_client.py +++ b/python/packages/core/tests/openai/test_openai_assistants_client.py @@ -22,7 +22,6 @@ from agent_framework import ( Content, HostedCodeInterpreterTool, HostedFileSearchTool, - Role, tool, ) from agent_framework.exceptions import ServiceInitializationError @@ -405,7 +404,7 @@ async def test_process_stream_events_thread_run_created(mock_async_openai: Magic update = updates[0] assert isinstance(update, ChatResponseUpdate) assert update.conversation_id == thread_id - assert update.role == Role.ASSISTANT + assert update.role == "assistant" assert update.contents == [] assert update.raw_representation == mock_response.data @@ -449,7 +448,7 @@ async def test_process_stream_events_message_delta_text(mock_async_openai: Magic update = updates[0] assert isinstance(update, ChatResponseUpdate) assert update.conversation_id == thread_id - assert update.role == Role.ASSISTANT + assert update.role == "assistant" assert update.text == "Hello from assistant" assert update.raw_representation == mock_message_delta @@ -488,7 +487,7 @@ async def test_process_stream_events_requires_action(mock_async_openai: MagicMoc update = updates[0] assert isinstance(update, ChatResponseUpdate) assert update.conversation_id == thread_id - assert update.role == Role.ASSISTANT + assert update.role == "assistant" assert len(update.contents) == 1 assert update.contents[0] == test_function_content assert update.raw_representation == mock_run @@ -568,7 +567,7 @@ async def test_process_stream_events_run_completed_with_usage( update = updates[0] assert isinstance(update, ChatResponseUpdate) assert update.conversation_id == thread_id - assert update.role == Role.ASSISTANT + assert update.role == "assistant" assert len(update.contents) == 1 # Check the usage content @@ -696,7 +695,7 @@ def test_prepare_options_basic(mock_async_openai: MagicMock) -> None: "top_p": 0.9, } - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] # Call the method run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore @@ -725,7 +724,7 @@ def test_prepare_options_with_tool_tool(mock_async_openai: MagicMock) -> None: "tool_choice": "auto", } - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] # Call the method run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore @@ -750,7 +749,7 @@ def test_prepare_options_with_code_interpreter(mock_async_openai: MagicMock) -> "tool_choice": "auto", } - messages = [ChatMessage(role=Role.USER, text="Calculate something")] + messages = [ChatMessage("user", ["Calculate something"])] # Call the method run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore @@ -770,7 +769,7 @@ def test_prepare_options_tool_choice_none(mock_async_openai: MagicMock) -> None: "tool_choice": "none", } - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] # Call the method run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore @@ -791,7 +790,7 @@ def test_prepare_options_required_function(mock_async_openai: MagicMock) -> None "tool_choice": tool_choice, } - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage("user", ["Hello"])] # Call the method run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore @@ -817,7 +816,7 @@ def test_prepare_options_with_file_search_tool(mock_async_openai: MagicMock) -> "tool_choice": "auto", } - messages = [ChatMessage(role=Role.USER, text="Search for information")] + messages = [ChatMessage("user", ["Search for information"])] # Call the method run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore @@ -842,7 +841,7 @@ def test_prepare_options_with_mapping_tool(mock_async_openai: MagicMock) -> None "tool_choice": "auto", } - messages = [ChatMessage(role=Role.USER, text="Use custom tool")] + messages = [ChatMessage("user", ["Use custom tool"])] # Call the method run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore @@ -864,7 +863,7 @@ def test_prepare_options_with_pydantic_response_format(mock_async_openai: MagicM model_config = ConfigDict(extra="forbid") chat_client = create_test_openai_assistants_client(mock_async_openai) - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] options = {"response_format": TestResponse} run_options, _ = chat_client._prepare_options(messages, options) # type: ignore @@ -880,8 +879,8 @@ def test_prepare_options_with_system_message(mock_async_openai: MagicMock) -> No chat_client = create_test_openai_assistants_client(mock_async_openai) messages = [ - ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant."), - ChatMessage(role=Role.USER, text="Hello"), + ChatMessage("system", ["You are a helpful assistant."]), + ChatMessage("user", ["Hello"]), ] # Call the method @@ -901,7 +900,7 @@ def test_prepare_options_with_image_content(mock_async_openai: MagicMock) -> Non # Create message with image content image_content = Content.from_uri(uri="https://example.com/image.jpg", media_type="image/jpeg") - messages = [ChatMessage(role=Role.USER, contents=[image_content])] + messages = [ChatMessage("user", [image_content])] # Call the method run_options, tool_results = chat_client._prepare_options(messages, {}) # type: ignore @@ -1021,7 +1020,7 @@ async def test_get_response() -> None: "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(ChatMessage("user", ["What's the weather like today?"])) # Test that the client can be used to get a response response = await openai_assistants_client.get_response(messages=messages) @@ -1039,7 +1038,7 @@ async def test_get_response_tools() -> None: assert isinstance(openai_assistants_client, ChatClientProtocol) messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages.append(ChatMessage("user", ["What's the weather like in Seattle?"])) # Test that the client can be used to get a response response = await openai_assistants_client.get_response( @@ -1067,7 +1066,7 @@ async def test_streaming() -> None: "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(ChatMessage("user", ["What's the weather like today?"])) # Test that the client can be used to get a response response = openai_assistants_client.get_streaming_response(messages=messages) @@ -1091,7 +1090,7 @@ async def test_streaming_tools() -> None: assert isinstance(openai_assistants_client, ChatClientProtocol) messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages.append(ChatMessage("user", ["What's the weather like in Seattle?"])) # Test that the client can be used to get a response response = openai_assistants_client.get_streaming_response( @@ -1119,7 +1118,7 @@ async def test_with_existing_assistant() -> None: # First create an assistant to use in the test async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as temp_client: # Get the assistant ID by triggering assistant creation - messages = [ChatMessage(role="user", text="Hello")] + messages = [ChatMessage("user", ["Hello"])] await temp_client.get_response(messages=messages) assistant_id = temp_client.assistant_id @@ -1130,7 +1129,7 @@ async def test_with_existing_assistant() -> None: assert isinstance(openai_assistants_client, ChatClientProtocol) assert openai_assistants_client.assistant_id == assistant_id - messages = [ChatMessage(role="user", text="What can you do?")] + messages = [ChatMessage("user", ["What can you do?"])] # Test that the client can be used to get a response response = await openai_assistants_client.get_response(messages=messages) @@ -1149,7 +1148,7 @@ async def test_file_search() -> None: assert isinstance(openai_assistants_client, ChatClientProtocol) messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(ChatMessage("user", ["What's the weather like today?"])) file_id, vector_store = await create_vector_store(openai_assistants_client) response = await openai_assistants_client.get_response( @@ -1175,7 +1174,7 @@ async def test_file_search_streaming() -> None: assert isinstance(openai_assistants_client, ChatClientProtocol) messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(ChatMessage("user", ["What's the weather like today?"])) file_id, vector_store = await create_vector_store(openai_assistants_client) response = openai_assistants_client.get_streaming_response( diff --git a/python/packages/core/tests/openai/test_openai_chat_client.py b/python/packages/core/tests/openai/test_openai_chat_client.py index 44e9884471..06b255f14d 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client.py +++ b/python/packages/core/tests/openai/test_openai_chat_client.py @@ -154,7 +154,7 @@ def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None: 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() - messages = [ChatMessage(role="user", text="test message")] + messages = [ChatMessage("user", ["test message"])] # Create a mock BadRequestError with content_filter code mock_response = MagicMock() @@ -209,7 +209,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") - messages = [ChatMessage(role="user", text="test message")] + messages = [ChatMessage("user", ["test message"])] mock_response = MagicMock() original_error_message = "Invalid API request format" @@ -652,12 +652,12 @@ def test_function_approval_content_is_skipped_in_preparation(openai_unit_test_en ) # Test that approval request is skipped - message_with_request = ChatMessage(role="assistant", contents=[approval_request]) + message_with_request = ChatMessage("assistant", [approval_request]) prepared_request = client._prepare_message_for_openai(message_with_request) assert len(prepared_request) == 0 # Should be empty - approval content is skipped # Test that approval response is skipped - message_with_response = ChatMessage(role="user", contents=[approval_response]) + message_with_response = ChatMessage("user", [approval_response]) prepared_response = client._prepare_message_for_openai(message_with_response) assert len(prepared_response) == 0 # Should be empty - approval content is skipped @@ -752,7 +752,7 @@ def test_prepare_options_without_model_id(openai_unit_test_env: dict[str, str]) client = OpenAIChatClient() client.model_id = None # Remove model_id - messages = [ChatMessage(role="user", text="test")] + messages = [ChatMessage("user", ["test"])] with pytest.raises(ValueError, match="model_id must be a non-empty string"): client._prepare_options(messages, {}) @@ -786,7 +786,7 @@ def test_prepare_options_with_instructions(openai_unit_test_env: dict[str, str]) """Test that instructions are prepended as system message.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="Hello")] + messages = [ChatMessage("user", ["Hello"])] options = {"instructions": "You are a helpful assistant."} prepared_options = client._prepare_options(messages, options) @@ -836,7 +836,7 @@ def test_tool_choice_required_with_function_name(openai_unit_test_env: dict[str, """Test that tool_choice with required mode and function name is correctly prepared.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="test")] + messages = [ChatMessage("user", ["test"])] options = { "tools": [get_weather], "tool_choice": {"mode": "required", "required_function_name": "get_weather"}, @@ -854,7 +854,7 @@ def test_response_format_dict_passthrough(openai_unit_test_env: dict[str, str]) """Test that response_format as dict is passed through directly.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="test")] + messages = [ChatMessage("user", ["test"])] custom_format = { "type": "json_schema", "json_schema": {"name": "Test", "schema": {"type": "object"}}, @@ -894,7 +894,7 @@ def test_prepare_options_removes_parallel_tool_calls_when_no_tools(openai_unit_t """Test that parallel_tool_calls is removed when no tools are present.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="test")] + messages = [ChatMessage("user", ["test"])] options = {"allow_multiple_tool_calls": True} prepared_options = client._prepare_options(messages, options) @@ -906,7 +906,7 @@ def test_prepare_options_removes_parallel_tool_calls_when_no_tools(openai_unit_t async def test_streaming_exception_handling(openai_unit_test_env: dict[str, str]) -> None: """Test that streaming errors are properly handled.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="test")] + messages = [ChatMessage("user", ["test"])] # Create a mock error during streaming mock_error = Exception("Streaming error") @@ -1008,14 +1008,14 @@ async def test_integration_options( # Prepare test message if option_name.startswith("tools") or option_name.startswith("tool_choice"): # Use weather-related prompt for tool tests - messages = [ChatMessage(role="user", text="What is the weather in Seattle?")] + messages = [ChatMessage("user", ["What is the weather in Seattle?"])] elif option_name.startswith("response_format"): # Use prompt that works well with structured output - messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")] - messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) + messages = [ChatMessage("user", ["The weather in Seattle is sunny"])] + messages.append(ChatMessage("user", ["What is the weather in Seattle?"])) else: # Generic prompt for simple options - messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")] + messages = [ChatMessage("user", ["Say 'Hello World' briefly."])] # Build options dict options: dict[str, Any] = {option_name: option_value} @@ -1032,7 +1032,7 @@ async def test_integration_options( ) output_format = option_value if option_name.startswith("response_format") else None - response = await ChatResponse.from_chat_response_generator(response_gen, output_format_type=output_format) + response = await ChatResponse.from_update_generator(response_gen, output_format_type=output_format) else: # Test non-streaming mode response = await client.get_response( @@ -1080,7 +1080,7 @@ async def test_integration_web_search() -> None: }, } if streaming: - response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content)) + response = await ChatResponse.from_update_generator(client.get_streaming_response(**content)) else: response = await client.get_response(**content) @@ -1105,7 +1105,7 @@ async def test_integration_web_search() -> None: }, } if streaming: - response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content)) + response = await ChatResponse.from_update_generator(client.get_streaming_response(**content)) else: response = await client.get_response(**content) assert response.text is not None diff --git a/python/packages/core/tests/openai/test_openai_chat_client_base.py b/python/packages/core/tests/openai/test_openai_chat_client_base.py index 3c9a432db0..a8155fa665 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client_base.py +++ b/python/packages/core/tests/openai/test_openai_chat_client_base.py @@ -69,7 +69,7 @@ async def test_cmc( openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) openai_chat_completion = OpenAIChatClient() await openai_chat_completion.get_response(messages=chat_history) @@ -88,7 +88,7 @@ async def test_cmc_chat_options( openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) openai_chat_completion = OpenAIChatClient() await openai_chat_completion.get_response( @@ -109,7 +109,7 @@ async def test_cmc_no_fcc_in_response( openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) orig_chat_history = deepcopy(chat_history) openai_chat_completion = OpenAIChatClient() @@ -131,7 +131,7 @@ async def test_cmc_structured_output_no_fcc( openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) # Define a mock response format class Test(BaseModel): @@ -153,7 +153,7 @@ async def test_scmc_chat_options( openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_streaming_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) openai_chat_completion = OpenAIChatClient() async for msg in openai_chat_completion.get_streaming_response( @@ -178,7 +178,7 @@ async def test_cmc_general_exception( openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) openai_chat_completion = OpenAIChatClient() with pytest.raises(ServiceResponseException): @@ -195,7 +195,7 @@ async def test_cmc_additional_properties( openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) openai_chat_completion = OpenAIChatClient() await openai_chat_completion.get_response(messages=chat_history, options={"reasoning_effort": "low"}) @@ -233,7 +233,7 @@ async def test_get_streaming( stream = MagicMock(spec=AsyncStream) stream.__aiter__.return_value = [content1, content2] mock_create.return_value = stream - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) orig_chat_history = deepcopy(chat_history) openai_chat_completion = OpenAIChatClient() @@ -272,7 +272,7 @@ async def test_get_streaming_singular( stream = MagicMock(spec=AsyncStream) stream.__aiter__.return_value = [content1, content2] mock_create.return_value = stream - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) orig_chat_history = deepcopy(chat_history) openai_chat_completion = OpenAIChatClient() @@ -311,7 +311,7 @@ async def test_get_streaming_structured_output_no_fcc( stream = MagicMock(spec=AsyncStream) stream.__aiter__.return_value = [content1, content2] mock_create.return_value = stream - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) # Define a mock response format class Test(BaseModel): @@ -334,7 +334,7 @@ async def test_get_streaming_no_fcc_in_response( openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_streaming_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) orig_chat_history = deepcopy(chat_history) openai_chat_completion = OpenAIChatClient() @@ -360,7 +360,7 @@ async def test_get_streaming_no_stream( mock_chat_completion_response: ChatCompletion, # AsyncStream[ChatCompletionChunk]? ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(ChatMessage("user", ["hello world"])) openai_chat_completion = OpenAIChatClient() with pytest.raises(ServiceResponseException): diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py index a5bc8ac45e..55aa9fb8e3 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/core/tests/openai/test_openai_responses_client.py @@ -39,7 +39,6 @@ from agent_framework import ( HostedImageGenerationTool, HostedMCPTool, HostedWebSearchTool, - Role, tool, ) from agent_framework.exceptions import ( @@ -215,7 +214,7 @@ def test_get_response_with_all_parameters() -> None: with pytest.raises(ServiceResponseException): asyncio.run( client.get_response( - messages=[ChatMessage(role="user", text="Test message")], + messages=[ChatMessage("user", ["Test message"])], options={ "include": ["message.output_text.logprobs"], "instructions": "You are a helpful assistant", @@ -261,7 +260,7 @@ def test_web_search_tool_with_location() -> None: with pytest.raises(ServiceResponseException): asyncio.run( client.get_response( - messages=[ChatMessage(role="user", text="What's the weather?")], + messages=[ChatMessage("user", ["What's the weather?"])], options={"tools": [web_search_tool], "tool_choice": "auto"}, ) ) @@ -278,7 +277,7 @@ def test_file_search_tool_with_invalid_inputs() -> None: with pytest.raises(ValueError, match="HostedFileSearchTool requires inputs to be of type"): asyncio.run( client.get_response( - messages=[ChatMessage(role="user", text="Search files")], + messages=[ChatMessage("user", ["Search files"])], options={"tools": [file_search_tool]}, ) ) @@ -294,7 +293,7 @@ def test_code_interpreter_tool_variations() -> None: with pytest.raises(ServiceResponseException): asyncio.run( client.get_response( - messages=[ChatMessage(role="user", text="Run some code")], + messages=[ChatMessage("user", ["Run some code"])], options={"tools": [code_tool_empty]}, ) ) @@ -307,7 +306,7 @@ def test_code_interpreter_tool_variations() -> None: with pytest.raises(ServiceResponseException): asyncio.run( client.get_response( - messages=[ChatMessage(role="user", text="Process these files")], + messages=[ChatMessage("user", ["Process these files"])], options={"tools": [code_tool_with_files]}, ) ) @@ -327,7 +326,7 @@ def test_content_filter_exception() -> None: with patch.object(client.client.responses, "create", side_effect=mock_error): with pytest.raises(OpenAIContentFilterException) as exc_info: - asyncio.run(client.get_response(messages=[ChatMessage(role="user", text="Test message")])) + asyncio.run(client.get_response(messages=[ChatMessage("user", ["Test message"])])) assert "content error" in str(exc_info.value) @@ -343,7 +342,7 @@ def test_hosted_file_search_tool_validation() -> None: with pytest.raises((ValueError, ServiceInvalidRequestError)): asyncio.run( client.get_response( - messages=[ChatMessage(role="user", text="Test")], + messages=[ChatMessage("user", ["Test"])], options={"tools": [empty_file_search_tool]}, ) ) @@ -364,9 +363,9 @@ def test_chat_message_parsing_with_function_calls() -> None: function_result = Content.from_function_result(call_id="test-call-id", result="Function executed successfully") messages = [ - ChatMessage(role="user", text="Call a function"), - ChatMessage(role="assistant", contents=[function_call]), - ChatMessage(role="tool", contents=[function_result]), + ChatMessage("user", ["Call a function"]), + ChatMessage("assistant", [function_call]), + ChatMessage("tool", [function_result]), ] # This should exercise the message parsing logic - will fail due to invalid API key @@ -392,7 +391,7 @@ async def test_response_format_parse_path() -> None: with patch.object(client.client.responses, "parse", return_value=mock_parsed_response): response = await client.get_response( - messages=[ChatMessage(role="user", text="Test message")], + messages=[ChatMessage("user", ["Test message"])], options={"response_format": OutputStruct, "store": True}, ) assert response.response_id == "parsed_response_123" @@ -419,7 +418,7 @@ async def test_response_format_parse_path_with_conversation_id() -> None: with patch.object(client.client.responses, "parse", return_value=mock_parsed_response): response = await client.get_response( - messages=[ChatMessage(role="user", text="Test message")], + messages=[ChatMessage("user", ["Test message"])], options={"response_format": OutputStruct, "store": True}, ) assert response.response_id == "parsed_response_123" @@ -442,7 +441,7 @@ async def test_bad_request_error_non_content_filter() -> None: with patch.object(client.client.responses, "parse", side_effect=mock_error): with pytest.raises(ServiceResponseException) as exc_info: await client.get_response( - messages=[ChatMessage(role="user", text="Test message")], + messages=[ChatMessage("user", ["Test message"])], options={"response_format": OutputStruct}, ) @@ -463,7 +462,7 @@ async def test_streaming_content_filter_exception_handling() -> None: mock_create.side_effect.code = "content_filter" with pytest.raises(OpenAIContentFilterException, match="service encountered a content error"): - response_stream = client.get_streaming_response(messages=[ChatMessage(role="user", text="Test")]) + response_stream = client.get_streaming_response(messages=[ChatMessage("user", ["Test"])]) async for _ in response_stream: break @@ -658,7 +657,7 @@ def test_prepare_content_for_opentool_approval_response() -> None: function_call=function_call, ) - result = client._prepare_content_for_openai(Role.ASSISTANT, approval_response, {}) + result = client._prepare_content_for_openai("assistant", approval_response, {}) assert result["type"] == "mcp_approval_response" assert result["approval_request_id"] == "approval_001" @@ -675,7 +674,7 @@ def test_prepare_content_for_openai_error_content() -> None: error_details="Invalid parameter", ) - result = client._prepare_content_for_openai(Role.ASSISTANT, error_content, {}) + result = client._prepare_content_for_openai("assistant", error_content, {}) # ErrorContent should return empty dict (logged but not sent) assert result == {} @@ -693,7 +692,7 @@ def test_prepare_content_for_openai_usage_content() -> None: } ) - result = client._prepare_content_for_openai(Role.ASSISTANT, usage_content, {}) + result = client._prepare_content_for_openai("assistant", usage_content, {}) # UsageContent should return empty dict (logged but not sent) assert result == {} @@ -707,7 +706,7 @@ def test_prepare_content_for_openai_hosted_vector_store_content() -> None: vector_store_id="vs_123", ) - result = client._prepare_content_for_openai(Role.ASSISTANT, vector_store_content, {}) + result = client._prepare_content_for_openai("assistant", vector_store_content, {}) # HostedVectorStoreContent should return empty dict (logged but not sent) assert result == {} @@ -807,7 +806,7 @@ def test_prepare_message_for_openai_with_function_approval_response() -> None: function_call=function_call, ) - message = ChatMessage(role="user", contents=[approval_response]) + message = ChatMessage("user", [approval_response]) call_id_to_id: dict[str, str] = {} result = client._prepare_message_for_openai(message, call_id_to_id) @@ -829,7 +828,7 @@ def test_chat_message_with_error_content() -> None: error_code="TEST_ERR", ) - message = ChatMessage(role="assistant", contents=[error_content]) + message = ChatMessage("assistant", [error_content]) call_id_to_id: dict[str, str] = {} result = client._prepare_message_for_openai(message, call_id_to_id) @@ -854,7 +853,7 @@ def test_chat_message_with_usage_content() -> None: } ) - message = ChatMessage(role="assistant", contents=[usage_content]) + message = ChatMessage("assistant", [usage_content]) call_id_to_id: dict[str, str] = {} result = client._prepare_message_for_openai(message, call_id_to_id) @@ -877,7 +876,7 @@ def test_hosted_file_content_preparation() -> None: name="document.pdf", ) - result = client._prepare_content_for_openai(Role.USER, hosted_file, {}) + result = client._prepare_content_for_openai("user", hosted_file, {}) assert result["type"] == "input_file" assert result["file_id"] == "file_abc123" @@ -900,7 +899,7 @@ def test_function_approval_response_with_mcp_tool_call() -> None: function_call=mcp_call, ) - result = client._prepare_content_for_openai(Role.ASSISTANT, approval_response, {}) + result = client._prepare_content_for_openai("assistant", approval_response, {}) assert result["type"] == "mcp_approval_response" assert result["approval_request_id"] == "approval_mcp_001" @@ -1358,14 +1357,14 @@ async def test_end_to_end_mcp_approval_flow(span_exporter) -> None: # Patch the create call to return the two mocked responses in sequence with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: # First call: get the approval request - response = await client.get_response(messages=[ChatMessage(role="user", text="Trigger approval")]) + response = await client.get_response(messages=[ChatMessage("user", ["Trigger approval"])]) assert response.messages[0].contents[0].type == "function_approval_request" req = response.messages[0].contents[0] assert req.id == "approval-1" # Build a user approval and send it (include required function_call) approval = Content.from_function_approval_response(approved=True, id=req.id, function_call=req.function_call) - approval_message = ChatMessage(role="user", contents=[approval]) + approval_message = ChatMessage("user", [approval]) _ = await client.get_response(messages=[approval_message]) # Ensure two calls were made and the second includes the mcp_approval_response @@ -1469,7 +1468,7 @@ def test_streaming_response_basic_structure() -> None: # Should get a valid ChatResponseUpdate structure assert isinstance(response, ChatResponseUpdate) - assert response.role == Role.ASSISTANT + assert response.role == "assistant" assert response.model_id == "test-model" assert isinstance(response.contents, list) assert response.raw_representation is mock_event @@ -1620,7 +1619,7 @@ def test_streaming_annotation_added_with_unknown_type() -> None: def test_service_response_exception_includes_original_error_details() -> None: """Test that ServiceResponseException messages include original error details in the new format.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - messages = [ChatMessage(role="user", text="test message")] + messages = [ChatMessage("user", ["test message"])] mock_response = MagicMock() original_error_message = "Request rate limit exceeded" @@ -1645,7 +1644,7 @@ def test_service_response_exception_includes_original_error_details() -> None: def test_get_streaming_response_with_response_format() -> None: """Test get_streaming_response with response_format.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - messages = [ChatMessage(role="user", text="Test streaming with format")] + messages = [ChatMessage("user", ["Test streaming with format"])] # It will fail due to invalid API key, but exercises the code path with pytest.raises(ServiceResponseException): @@ -1667,7 +1666,7 @@ def test_prepare_content_for_openai_image_content() -> None: media_type="image/jpeg", additional_properties={"detail": "high", "file_id": "file_123"}, ) - result = client._prepare_content_for_openai(Role.USER, image_content_with_detail, {}) # type: ignore + result = client._prepare_content_for_openai("user", image_content_with_detail, {}) # type: ignore assert result["type"] == "input_image" assert result["image_url"] == "https://example.com/image.jpg" assert result["detail"] == "high" @@ -1675,7 +1674,7 @@ def test_prepare_content_for_openai_image_content() -> None: # Test image content without additional properties (defaults) image_content_basic = Content.from_uri(uri="https://example.com/basic.png", media_type="image/png") - result = client._prepare_content_for_openai(Role.USER, image_content_basic, {}) # type: ignore + result = client._prepare_content_for_openai("user", image_content_basic, {}) # type: ignore assert result["type"] == "input_image" assert result["detail"] == "auto" assert result["file_id"] is None @@ -1687,14 +1686,14 @@ def test_prepare_content_for_openai_audio_content() -> None: # Test WAV audio content wav_content = Content.from_uri(uri="data:audio/wav;base64,abc123", media_type="audio/wav") - result = client._prepare_content_for_openai(Role.USER, wav_content, {}) # type: ignore + result = client._prepare_content_for_openai("user", wav_content, {}) # type: ignore assert result["type"] == "input_audio" assert result["input_audio"]["data"] == "data:audio/wav;base64,abc123" assert result["input_audio"]["format"] == "wav" # Test MP3 audio content mp3_content = Content.from_uri(uri="data:audio/mp3;base64,def456", media_type="audio/mp3") - result = client._prepare_content_for_openai(Role.USER, mp3_content, {}) # type: ignore + result = client._prepare_content_for_openai("user", mp3_content, {}) # type: ignore assert result["type"] == "input_audio" assert result["input_audio"]["format"] == "mp3" @@ -1705,12 +1704,12 @@ def test_prepare_content_for_openai_unsupported_content() -> None: # Test unsupported audio format unsupported_audio = Content.from_uri(uri="data:audio/ogg;base64,ghi789", media_type="audio/ogg") - result = client._prepare_content_for_openai(Role.USER, unsupported_audio, {}) # type: ignore + result = client._prepare_content_for_openai("user", unsupported_audio, {}) # type: ignore assert result == {} # Test non-media content text_uri_content = Content.from_uri(uri="https://example.com/document.txt", media_type="text/plain") - result = client._prepare_content_for_openai(Role.USER, text_uri_content, {}) # type: ignore + result = client._prepare_content_for_openai("user", text_uri_content, {}) # type: ignore assert result == {} @@ -1775,7 +1774,7 @@ def test_prepare_content_for_openai_text_reasoning_comprehensive() -> None: "encrypted_content": "secure_data_456", }, ) - result = client._prepare_content_for_openai(Role.ASSISTANT, comprehensive_reasoning, {}) # type: ignore + result = client._prepare_content_for_openai("assistant", comprehensive_reasoning, {}) # type: ignore assert result["type"] == "reasoning" assert result["summary"]["text"] == "Comprehensive reasoning summary" assert result["status"] == "in_progress" @@ -2091,7 +2090,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") - messages = [ChatMessage(role="user", text="Test message")] + messages = [ChatMessage("user", ["Test message"])] test_conversation_id = "test-conversation-123" chat_options = ChatOptions(store=True, conversation_id=test_conversation_id) @@ -2117,7 +2116,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") - messages = [ChatMessage(role="user", text="Hello")] + messages = [ChatMessage("user", ["Hello"])] # options has a stale response id, kwargs carries the freshest one opts = {"conversation_id": "resp_old_123"} @@ -2224,14 +2223,14 @@ async def test_integration_options( # Prepare test message if option_name.startswith("tools") or option_name.startswith("tool_choice"): # Use weather-related prompt for tool tests - messages = [ChatMessage(role="user", text="What is the weather in Seattle?")] + messages = [ChatMessage("user", ["What is the weather in Seattle?"])] elif option_name.startswith("response_format"): # Use prompt that works well with structured output - messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")] - messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) + messages = [ChatMessage("user", ["The weather in Seattle is sunny"])] + messages.append(ChatMessage("user", ["What is the weather in Seattle?"])) else: # Generic prompt for simple options - messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")] + messages = [ChatMessage("user", ["Say 'Hello World' briefly."])] # Build options dict options: dict[str, Any] = {option_name: option_value} @@ -2248,7 +2247,7 @@ async def test_integration_options( ) output_format = option_value if option_name.startswith("response_format") else None - response = await ChatResponse.from_chat_response_generator(response_gen, output_format_type=output_format) + response = await ChatResponse.from_update_generator(response_gen, output_format_type=output_format) else: # Test non-streaming mode response = await openai_responses_client.get_response( @@ -2296,7 +2295,7 @@ async def test_integration_web_search() -> None: }, } if streaming: - response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content)) + response = await ChatResponse.from_update_generator(client.get_streaming_response(**content)) else: response = await client.get_response(**content) @@ -2321,7 +2320,7 @@ async def test_integration_web_search() -> None: }, } if streaming: - response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content)) + response = await ChatResponse.from_update_generator(client.get_streaming_response(**content)) else: response = await client.get_response(**content) assert response.text is not None diff --git a/python/packages/core/tests/workflow/test_agent_executor.py b/python/packages/core/tests/workflow/test_agent_executor.py index 0fa2bfd952..0d4912bae1 100644 --- a/python/packages/core/tests/workflow/test_agent_executor.py +++ b/python/packages/core/tests/workflow/test_agent_executor.py @@ -12,7 +12,6 @@ from agent_framework import ( ChatMessage, ChatMessageStore, Content, - Role, SequentialBuilder, WorkflowOutputEvent, WorkflowRunState, @@ -37,9 +36,7 @@ class _CountingAgent(BaseAgent): **kwargs: Any, ) -> AgentResponse: self.call_count += 1 - return AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text=f"Response #{self.call_count}: {self.name}")] - ) + return AgentResponse(messages=[ChatMessage("assistant", [f"Response #{self.call_count}: {self.name}"])]) async def run_stream( # type: ignore[override] self, @@ -62,8 +59,8 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None: # Add some initial messages to the thread to verify thread state persistence initial_messages = [ - ChatMessage(role=Role.USER, text="Initial message 1"), - ChatMessage(role=Role.ASSISTANT, text="Initial response 1"), + ChatMessage("user", ["Initial message 1"]), + ChatMessage("assistant", ["Initial response 1"]), ] await initial_thread.on_new_messages(initial_messages) @@ -166,9 +163,9 @@ async def test_agent_executor_save_and_restore_state_directly() -> None: # Add messages to thread thread_messages = [ - ChatMessage(role=Role.USER, text="Message in thread 1"), - ChatMessage(role=Role.ASSISTANT, text="Thread response 1"), - ChatMessage(role=Role.USER, text="Message in thread 2"), + ChatMessage("user", ["Message in thread 1"]), + ChatMessage("assistant", ["Thread response 1"]), + ChatMessage("user", ["Message in thread 2"]), ] await thread.on_new_messages(thread_messages) @@ -176,8 +173,8 @@ async def test_agent_executor_save_and_restore_state_directly() -> None: # Add messages to executor cache cache_messages = [ - ChatMessage(role=Role.USER, text="Cached user message"), - ChatMessage(role=Role.ASSISTANT, text="Cached assistant response"), + ChatMessage("user", ["Cached user message"]), + ChatMessage("assistant", ["Cached assistant response"]), ] executor._cache = list(cache_messages) # type: ignore[reportPrivateUsage] diff --git a/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py b/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py index 874f73fa5b..2b1f11423b 100644 --- a/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py +++ b/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py @@ -21,7 +21,6 @@ from agent_framework import ( ChatResponseUpdate, Content, RequestInfoEvent, - Role, WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, @@ -45,7 +44,7 @@ class _ToolCallingAgent(BaseAgent): **kwargs: Any, ) -> AgentResponse: """Non-streaming run - not used in this test.""" - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="done")]) + return AgentResponse(messages=[ChatMessage("assistant", ["done"])]) async def run_stream( self, @@ -58,7 +57,7 @@ class _ToolCallingAgent(BaseAgent): # First update: some text yield AgentResponseUpdate( contents=[Content.from_text(text="Let me search for that...")], - role=Role.ASSISTANT, + role="assistant", ) # Second update: tool call (no text!) @@ -70,7 +69,7 @@ class _ToolCallingAgent(BaseAgent): arguments={"query": "weather"}, ) ], - role=Role.ASSISTANT, + role="assistant", ) # Third update: tool result (no text!) @@ -81,13 +80,13 @@ class _ToolCallingAgent(BaseAgent): result={"temperature": 72, "condition": "sunny"}, ) ], - role=Role.TOOL, + role="tool", ) # Fourth update: final text response yield AgentResponseUpdate( contents=[Content.from_text(text="The weather is sunny, 72°F.")], - role=Role.ASSISTANT, + role="assistant", ) @@ -179,7 +178,7 @@ class MockChatClient: ) ) else: - response = ChatResponse(messages=ChatMessage(role="assistant", text="Tool executed successfully.")) + response = ChatResponse(messages=ChatMessage("assistant", ["Tool executed successfully."])) self._iteration += 1 return response @@ -212,7 +211,7 @@ class MockChatClient: role="assistant", ) else: - yield ChatResponseUpdate(text=Content.from_text(text="Tool executed "), role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(text="Tool executed ")], role="assistant") yield ChatResponseUpdate(contents=[Content.from_text(text="successfully.")], role="assistant") self._iteration += 1 diff --git a/python/packages/core/tests/workflow/test_agent_run_event_typing.py b/python/packages/core/tests/workflow/test_agent_run_event_typing.py index e5071a7c96..4ba1328fc1 100644 --- a/python/packages/core/tests/workflow/test_agent_run_event_typing.py +++ b/python/packages/core/tests/workflow/test_agent_run_event_typing.py @@ -2,13 +2,13 @@ """Tests for AgentRunEvent and AgentRunUpdateEvent type annotations.""" -from agent_framework import AgentResponse, AgentResponseUpdate, ChatMessage, Role +from agent_framework import AgentResponse, AgentResponseUpdate, ChatMessage from agent_framework._workflows._events import AgentRunEvent, AgentRunUpdateEvent def test_agent_run_event_data_type() -> None: """Verify AgentRunEvent.data is typed as AgentResponse | None.""" - response = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Hello")]) + response = AgentResponse(messages=[ChatMessage("assistant", ["Hello"])]) event = AgentRunEvent(executor_id="test", data=response) # This assignment should pass type checking without a cast diff --git a/python/packages/core/tests/workflow/test_concurrent.py b/python/packages/core/tests/workflow/test_concurrent.py index a0c03c7720..d1fee3684e 100644 --- a/python/packages/core/tests/workflow/test_concurrent.py +++ b/python/packages/core/tests/workflow/test_concurrent.py @@ -12,7 +12,6 @@ from agent_framework import ( ChatMessage, ConcurrentBuilder, Executor, - Role, WorkflowContext, WorkflowOutputEvent, WorkflowRunState, @@ -36,7 +35,7 @@ class _FakeAgentExec(Executor): @handler async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExecutorResponse]) -> None: - response = AgentResponse(messages=ChatMessage(Role.ASSISTANT, text=self._reply_text)) + response = AgentResponse(messages=ChatMessage("assistant", text=self._reply_text)) full_conversation = list(request.messages) + list(response.messages) await ctx.send_message(AgentExecutorResponse(self.id, response, full_conversation=full_conversation)) @@ -126,12 +125,12 @@ async def test_concurrent_default_aggregator_emits_single_user_and_assistants() # Expect one user message + one assistant message per participant assert len(messages) == 1 + 3 - assert messages[0].role == Role.USER + assert messages[0].role == "user" assert "hello world" in messages[0].text assistant_texts = {m.text for m in messages[1:]} assert assistant_texts == {"Alpha", "Beta", "Gamma"} - assert all(m.role == Role.ASSISTANT for m in messages[1:]) + assert all(m.role == "assistant" for m in messages[1:]) async def test_concurrent_custom_aggregator_callback_is_used() -> None: @@ -543,9 +542,9 @@ async def test_concurrent_with_register_participants() -> None: # Expect one user message + one assistant message per participant assert len(messages) == 1 + 3 - assert messages[0].role == Role.USER + assert messages[0].role == "user" assert "test prompt" in messages[0].text assistant_texts = {m.text for m in messages[1:]} assert assistant_texts == {"Alpha", "Beta", "Gamma"} - assert all(m.role == Role.ASSISTANT for m in messages[1:]) + assert all(m.role == "assistant" for m in messages[1:]) diff --git a/python/packages/core/tests/workflow/test_executor.py b/python/packages/core/tests/workflow/test_executor.py index e7c2a31aec..d4e950d62d 100644 --- a/python/packages/core/tests/workflow/test_executor.py +++ b/python/packages/core/tests/workflow/test_executor.py @@ -537,7 +537,7 @@ async def test_executor_invoked_event_data_not_mutated_by_handler(): async def mutator(messages: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None: # The handler mutates the input list by appending new messages original_len = len(messages) - messages.append(ChatMessage(role="assistant", text="Added by executor")) + messages.append(ChatMessage("assistant", ["Added by executor"])) await ctx.send_message(messages) # Verify mutation happened assert len(messages) == original_len + 1 @@ -545,7 +545,7 @@ async def test_executor_invoked_event_data_not_mutated_by_handler(): workflow = WorkflowBuilder().set_start_executor(mutator).build() # Run with a single user message - input_messages = [ChatMessage(role="user", text="hello")] + input_messages = [ChatMessage("user", ["hello"])] events = await workflow.run(input_messages) # Find the invoked event for the Mutator executor diff --git a/python/packages/core/tests/workflow/test_full_conversation.py b/python/packages/core/tests/workflow/test_full_conversation.py index 9a8f4bd9c9..1c84e04494 100644 --- a/python/packages/core/tests/workflow/test_full_conversation.py +++ b/python/packages/core/tests/workflow/test_full_conversation.py @@ -16,7 +16,6 @@ from agent_framework import ( ChatMessage, Content, Executor, - Role, SequentialBuilder, WorkflowBuilder, WorkflowContext, @@ -40,7 +39,7 @@ class _SimpleAgent(BaseAgent): thread: AgentThread | None = None, **kwargs: Any, ) -> AgentResponse: - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=self._reply_text)]) + return AgentResponse(messages=[ChatMessage("assistant", [self._reply_text])]) async def run_stream( # type: ignore[override] self, @@ -89,8 +88,8 @@ async def test_agent_executor_populates_full_conversation_non_streaming() -> Non # Assert: full_conversation contains [user("hello world"), assistant("agent-reply")] assert isinstance(payload, dict) assert payload["length"] == 2 - assert payload["roles"][0] == Role.USER and "hello world" in (payload["texts"][0] or "") - assert payload["roles"][1] == Role.ASSISTANT and "agent-reply" in (payload["texts"][1] or "") + assert payload["roles"][0] == "user" and "hello world" in (payload["texts"][0] or "") + assert payload["roles"][1] == "assistant" and "agent-reply" in (payload["texts"][1] or "") class _CaptureAgent(BaseAgent): @@ -116,9 +115,9 @@ class _CaptureAgent(BaseAgent): if isinstance(m, ChatMessage): norm.append(m) elif isinstance(m, str): - norm.append(ChatMessage(role=Role.USER, text=m)) + norm.append(ChatMessage("user", [m])) self._last_messages = norm - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=self._reply_text)]) + return AgentResponse(messages=[ChatMessage("assistant", [self._reply_text])]) async def run_stream( # type: ignore[override] self, @@ -134,7 +133,7 @@ class _CaptureAgent(BaseAgent): if isinstance(m, ChatMessage): norm.append(m) elif isinstance(m, str): - norm.append(ChatMessage(role=Role.USER, text=m)) + norm.append(ChatMessage("user", [m])) self._last_messages = norm yield AgentResponseUpdate(contents=[Content.from_text(text=self._reply_text)]) @@ -154,5 +153,5 @@ async def test_sequential_adapter_uses_full_conversation() -> None: # Assert: second agent should have seen the user prompt and A1's assistant reply seen = a2._last_messages # pyright: ignore[reportPrivateUsage] assert len(seen) == 2 - assert seen[0].role == Role.USER and "hello seq" in (seen[0].text or "") - assert seen[1].role == Role.ASSISTANT and "A1 reply" in (seen[1].text or "") + assert seen[0].role == "user" and "hello seq" in (seen[0].text or "") + assert seen[1].role == "assistant" and "A1 reply" in (seen[1].text or "") diff --git a/python/packages/core/tests/workflow/test_group_chat.py b/python/packages/core/tests/workflow/test_group_chat.py index e75bdfd638..21f1e567d3 100644 --- a/python/packages/core/tests/workflow/test_group_chat.py +++ b/python/packages/core/tests/workflow/test_group_chat.py @@ -25,7 +25,6 @@ from agent_framework import ( MagenticProgressLedger, MagenticProgressLedgerItem, RequestInfoEvent, - Role, WorkflowOutputEvent, WorkflowRunState, WorkflowStatusEvent, @@ -45,7 +44,7 @@ class StubAgent(BaseAgent): thread: AgentThread | None = None, **kwargs: Any, ) -> AgentResponse: - response = ChatMessage(role=Role.ASSISTANT, text=self._reply_text, author_name=self.name) + response = ChatMessage("assistant", [self._reply_text], author_name=self.name) return AgentResponse(messages=[response]) def run_stream( # type: ignore[override] @@ -57,7 +56,7 @@ class StubAgent(BaseAgent): ) -> AsyncIterable[AgentResponseUpdate]: async def _stream() -> AsyncIterable[AgentResponseUpdate]: yield AgentResponseUpdate( - contents=[Content.from_text(text=self._reply_text)], role=Role.ASSISTANT, author_name=self.name + contents=[Content.from_text(text=self._reply_text)], role="assistant", author_name=self.name ) return _stream() @@ -94,7 +93,7 @@ class StubManagerAgent(ChatAgent): return AgentResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", text=( '{"terminate": false, "reason": "Selecting agent", ' '"next_speaker": "agent", "final_message": null}' @@ -115,7 +114,7 @@ class StubManagerAgent(ChatAgent): return AgentResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", text=( '{"terminate": true, "reason": "Task complete", ' '"next_speaker": null, "final_message": "agent manager final"}' @@ -146,7 +145,7 @@ class StubManagerAgent(ChatAgent): ) ) ], - role=Role.ASSISTANT, + role="assistant", author_name=self.name, ) @@ -162,7 +161,7 @@ class StubManagerAgent(ChatAgent): ) ) ], - role=Role.ASSISTANT, + role="assistant", author_name=self.name, ) @@ -192,7 +191,7 @@ class StubMagenticManager(MagenticManagerBase): self._round = 0 async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="plan", author_name="magentic_manager") + return ChatMessage("assistant", ["plan"], author_name="magentic_manager") async def replan(self, magentic_context: MagenticContext) -> ChatMessage: return await self.plan(magentic_context) @@ -218,7 +217,7 @@ class StubMagenticManager(MagenticManagerBase): ) async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="final", author_name="magentic_manager") + return ChatMessage("assistant", ["final"], author_name="magentic_manager") async def test_group_chat_builder_basic_flow() -> None: @@ -263,8 +262,8 @@ async def test_group_chat_as_agent_accepts_conversation() -> None: agent = workflow.as_agent(name="group-chat-agent") conversation = [ - ChatMessage(role=Role.USER, text="kickoff", author_name="user"), - ChatMessage(role=Role.ASSISTANT, text="noted", author_name="alpha"), + ChatMessage("user", ["kickoff"], author_name="user"), + ChatMessage("assistant", ["noted"], author_name="alpha"), ] response = await agent.run(conversation) @@ -425,7 +424,7 @@ class TestGroupChatWorkflow: return "agent" def termination_condition(conversation: list[ChatMessage]) -> bool: - replies = [msg for msg in conversation if msg.role == Role.ASSISTANT and msg.author_name == "agent"] + replies = [msg for msg in conversation if msg.role == "assistant" and msg.author_name == "agent"] return len(replies) >= 2 agent = StubAgent("agent", "response") @@ -447,7 +446,7 @@ class TestGroupChatWorkflow: assert outputs, "Expected termination to yield output" conversation = outputs[-1] - agent_replies = [msg for msg in conversation if msg.author_name == "agent" and msg.role == Role.ASSISTANT] + agent_replies = [msg for msg in conversation if msg.author_name == "agent" and msg.role == "assistant"] assert len(agent_replies) == 2 final_output = conversation[-1] # The orchestrator uses its ID as author_name by default @@ -553,7 +552,7 @@ class TestConversationHandling: def selector(state: GroupChatState) -> str: # Verify the conversation has the user message assert len(state.conversation) > 0 - assert state.conversation[0].role == Role.USER + assert state.conversation[0].role == "user" assert state.conversation[0].text == "test string" return "agent" @@ -578,7 +577,7 @@ class TestConversationHandling: async def test_handle_chat_message_input(self) -> None: """Test handling ChatMessage input directly.""" - task_message = ChatMessage(role=Role.USER, text="test message") + task_message = ChatMessage("user", ["test message"]) def selector(state: GroupChatState) -> str: # Verify the task message was preserved in conversation @@ -608,8 +607,8 @@ class TestConversationHandling: async def test_handle_conversation_list_input(self) -> None: """Test handling conversation list preserves context.""" conversation = [ - ChatMessage(role=Role.SYSTEM, text="system message"), - ChatMessage(role=Role.USER, text="user message"), + ChatMessage("system", ["system message"]), + ChatMessage("user", ["user message"]), ] def selector(state: GroupChatState) -> str: @@ -1118,7 +1117,7 @@ async def test_group_chat_with_orchestrator_factory_returning_chat_agent(): return AgentResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", text=( '{"terminate": false, "reason": "Selecting alpha", ' '"next_speaker": "alpha", "final_message": null}' @@ -1138,7 +1137,7 @@ async def test_group_chat_with_orchestrator_factory_returning_chat_agent(): return AgentResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", text=( '{"terminate": true, "reason": "Task complete", ' '"next_speaker": null, "final_message": "dynamic manager final"}' diff --git a/python/packages/core/tests/workflow/test_handoff.py b/python/packages/core/tests/workflow/test_handoff.py index 130bacb0ed..962ab88f16 100644 --- a/python/packages/core/tests/workflow/test_handoff.py +++ b/python/packages/core/tests/workflow/test_handoff.py @@ -15,7 +15,6 @@ from agent_framework import ( HandoffAgentUserRequest, HandoffBuilder, RequestInfoEvent, - Role, WorkflowEvent, WorkflowOutputEvent, resolve_agent_id, @@ -49,7 +48,7 @@ class MockChatClient: async def get_response(self, messages: Any, **kwargs: Any) -> ChatResponse: contents = _build_reply_contents(self._name, self._handoff_to, self._next_call_id()) reply = ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=contents, ) return ChatResponse(messages=reply, response_id="mock_response") @@ -57,7 +56,7 @@ class MockChatClient: def get_streaming_response(self, messages: Any, **kwargs: Any) -> AsyncIterable[ChatResponseUpdate]: async def _stream() -> AsyncIterable[ChatResponseUpdate]: contents = _build_reply_contents(self._name, self._handoff_to, self._next_call_id()) - yield ChatResponseUpdate(contents=contents, role=Role.ASSISTANT) + yield ChatResponseUpdate(contents=contents, role="assistant") return _stream() @@ -123,7 +122,7 @@ async def test_handoff(): workflow = ( HandoffBuilder(participants=[triage, specialist, escalation]) .with_start_agent(triage) - .with_termination_condition(lambda conv: sum(1 for m in conv if m.role == Role.USER) >= 2) + .with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2) .build() ) @@ -174,9 +173,7 @@ async def test_autonomous_mode_yields_output_without_user_request(): final_conversation = outputs[-1].data assert isinstance(final_conversation, list) conversation_list = cast(list[ChatMessage], final_conversation) - assert any( - msg.role == Role.ASSISTANT and (msg.text or "").startswith("specialist reply") for msg in conversation_list - ) + assert any(msg.role == "assistant" and (msg.text or "").startswith("specialist reply") for msg in conversation_list) async def test_autonomous_mode_resumes_user_input_on_turn_limit(): @@ -222,7 +219,7 @@ async def test_handoff_async_termination_condition() -> None: async def async_termination(conv: list[ChatMessage]) -> bool: nonlocal termination_call_count termination_call_count += 1 - user_count = sum(1 for msg in conv if msg.role == Role.USER) + user_count = sum(1 for msg in conv if msg.role == "user") return user_count >= 2 coordinator = MockHandoffAgent(name="coordinator", handoff_to="worker") @@ -240,9 +237,7 @@ async def test_handoff_async_termination_condition() -> None: assert requests events = await _drain( - workflow.send_responses_streaming({ - requests[-1].request_id: [ChatMessage(role=Role.USER, text="Second user message")] - }) + workflow.send_responses_streaming({requests[-1].request_id: [ChatMessage("user", ["Second user message"])]}) ) outputs = [ev for ev in events if isinstance(ev, WorkflowOutputEvent)] assert len(outputs) == 1 @@ -250,7 +245,7 @@ async def test_handoff_async_termination_condition() -> None: final_conversation = outputs[0].data assert isinstance(final_conversation, list) final_conv_list = cast(list[ChatMessage], final_conversation) - user_messages = [msg for msg in final_conv_list if msg.role == Role.USER] + user_messages = [msg for msg in final_conv_list if msg.role == "user"] assert len(user_messages) == 2 assert termination_call_count > 0 @@ -264,7 +259,7 @@ async def test_tool_choice_preserved_from_agent_config(): if options: recorded_tool_choices.append(options.get("tool_choice")) return ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Response")], + messages=[ChatMessage("assistant", ["Response"])], response_id="test_response", ) @@ -480,7 +475,7 @@ async def test_handoff_with_participant_factories(): workflow = ( HandoffBuilder(participant_factories={"triage": create_triage, "specialist": create_specialist}) .with_start_agent("triage") - .with_termination_condition(lambda conv: sum(1 for m in conv if m.role == Role.USER) >= 2) + .with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2) .build() ) @@ -493,7 +488,7 @@ async def test_handoff_with_participant_factories(): # Follow-up message events = await _drain( - workflow.send_responses_streaming({requests[-1].request_id: [ChatMessage(role=Role.USER, text="More details")]}) + workflow.send_responses_streaming({requests[-1].request_id: [ChatMessage("user", ["More details"])]}) ) outputs = [ev for ev in events if isinstance(ev, WorkflowOutputEvent)] assert outputs @@ -553,7 +548,7 @@ async def test_handoff_with_participant_factories_and_add_handoff(): .with_start_agent("triage") .add_handoff("triage", ["specialist_a", "specialist_b"]) .add_handoff("specialist_a", ["specialist_b"]) - .with_termination_condition(lambda conv: sum(1 for m in conv if m.role == Role.USER) >= 3) + .with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 3) .build() ) @@ -567,9 +562,7 @@ async def test_handoff_with_participant_factories_and_add_handoff(): # Second user message - specialist_a hands off to specialist_b events = await _drain( - workflow.send_responses_streaming({ - requests[-1].request_id: [ChatMessage(role=Role.USER, text="Need escalation")] - }) + workflow.send_responses_streaming({requests[-1].request_id: [ChatMessage("user", ["Need escalation"])]}) ) requests = [ev for ev in events if isinstance(ev, RequestInfoEvent)] assert requests @@ -594,7 +587,7 @@ async def test_handoff_participant_factories_with_checkpointing(): HandoffBuilder(participant_factories={"triage": create_triage, "specialist": create_specialist}) .with_start_agent("triage") .with_checkpointing(storage) - .with_termination_condition(lambda conv: sum(1 for m in conv if m.role == Role.USER) >= 2) + .with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2) .build() ) @@ -604,7 +597,7 @@ async def test_handoff_participant_factories_with_checkpointing(): assert requests events = await _drain( - workflow.send_responses_streaming({requests[-1].request_id: [ChatMessage(role=Role.USER, text="follow up")]}) + workflow.send_responses_streaming({requests[-1].request_id: [ChatMessage("user", ["follow up"])]}) ) outputs = [ev for ev in events if isinstance(ev, WorkflowOutputEvent)] assert outputs, "Should have workflow output after termination condition is met" diff --git a/python/packages/core/tests/workflow/test_magentic.py b/python/packages/core/tests/workflow/test_magentic.py index 9c6a2521b1..8f116aa1ad 100644 --- a/python/packages/core/tests/workflow/test_magentic.py +++ b/python/packages/core/tests/workflow/test_magentic.py @@ -27,7 +27,6 @@ from agent_framework import ( MagenticProgressLedger, MagenticProgressLedgerItem, RequestInfoEvent, - Role, StandardMagenticManager, Workflow, WorkflowCheckpoint, @@ -53,7 +52,7 @@ def test_magentic_context_reset_behavior(): participant_descriptions={"Alice": "Researcher"}, ) # seed context state - ctx.chat_history.append(ChatMessage(role=Role.ASSISTANT, text="draft")) + ctx.chat_history.append(ChatMessage("assistant", ["draft"])) ctx.stall_count = 2 prev_reset = ctx.reset_count @@ -120,18 +119,18 @@ class FakeManager(MagenticManagerBase): pass async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - facts = ChatMessage(role=Role.ASSISTANT, text="GIVEN OR VERIFIED FACTS\n- A\n") - plan = ChatMessage(role=Role.ASSISTANT, text="- Do X\n- Do Y\n") + facts = ChatMessage("assistant", ["GIVEN OR VERIFIED FACTS\n- A\n"]) + plan = ChatMessage("assistant", ["- Do X\n- Do Y\n"]) self.task_ledger = _SimpleLedger(facts=facts, plan=plan) combined = f"Task: {magentic_context.task}\n\nFacts:\n{facts.text}\n\nPlan:\n{plan.text}" - return ChatMessage(role=Role.ASSISTANT, text=combined, author_name=self.name) + return ChatMessage("assistant", [combined], author_name=self.name) async def replan(self, magentic_context: MagenticContext) -> ChatMessage: - facts = ChatMessage(role=Role.ASSISTANT, text="GIVEN OR VERIFIED FACTS\n- A2\n") - plan = ChatMessage(role=Role.ASSISTANT, text="- Do Z\n") + facts = ChatMessage("assistant", ["GIVEN OR VERIFIED FACTS\n- A2\n"]) + plan = ChatMessage("assistant", ["- Do Z\n"]) self.task_ledger = _SimpleLedger(facts=facts, plan=plan) combined = f"Task: {magentic_context.task}\n\nFacts:\n{facts.text}\n\nPlan:\n{plan.text}" - return ChatMessage(role=Role.ASSISTANT, text=combined, author_name=self.name) + return ChatMessage("assistant", [combined], author_name=self.name) async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: # At least two messages in chat history means request is satisfied for testing @@ -145,7 +144,7 @@ class FakeManager(MagenticManagerBase): ) async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text=self.FINAL_ANSWER, author_name=self.name) + return ChatMessage("assistant", [self.FINAL_ANSWER], author_name=self.name) class StubAgent(BaseAgent): @@ -160,7 +159,7 @@ class StubAgent(BaseAgent): thread: AgentThread | None = None, **kwargs: Any, ) -> AgentResponse: - response = ChatMessage(role=Role.ASSISTANT, text=self._reply_text, author_name=self.name) + response = ChatMessage("assistant", [self._reply_text], author_name=self.name) return AgentResponse(messages=[response]) def run_stream( # type: ignore[override] @@ -172,7 +171,7 @@ class StubAgent(BaseAgent): ) -> AsyncIterable[AgentResponseUpdate]: async def _stream() -> AsyncIterable[AgentResponseUpdate]: yield AgentResponseUpdate( - contents=[Content.from_text(text=self._reply_text)], role=Role.ASSISTANT, author_name=self.name + contents=[Content.from_text(text=self._reply_text)], role="assistant", author_name=self.name ) return _stream() @@ -223,8 +222,8 @@ async def test_magentic_as_agent_does_not_accept_conversation() -> None: agent = workflow.as_agent(name="magentic-agent") conversation = [ - ChatMessage(role=Role.SYSTEM, text="Guidelines", author_name="system"), - ChatMessage(role=Role.USER, text="Summarize the findings", author_name="requester"), + ChatMessage("system", ["Guidelines"], author_name="system"), + ChatMessage("user", ["Summarize the findings"], author_name="requester"), ] with pytest.raises(ValueError, match="Magentic only support a single task message to start the workflow."): await agent.run(conversation) @@ -238,7 +237,7 @@ async def test_standard_manager_plan_and_replan_combined_ledger(): ) first = await manager.plan(ctx.clone()) - assert first.role == Role.ASSISTANT and "Facts:" in first.text and "Plan:" in first.text + assert first.role == "assistant" and "Facts:" in first.text and "Plan:" in first.text assert manager.task_ledger is not None replanned = await manager.replan(ctx.clone()) @@ -352,7 +351,7 @@ async def test_magentic_orchestrator_round_limit_produces_partial_result(): data = output_event.data assert isinstance(data, list) assert len(data) > 0 # type: ignore - assert data[-1].role == Role.ASSISTANT # type: ignore + assert data[-1].role == "assistant" # type: ignore assert all(isinstance(msg, ChatMessage) for msg in data) # type: ignore @@ -427,7 +426,7 @@ class StubManagerAgent(BaseAgent): thread: Any = None, **kwargs: Any, ) -> AgentResponse: - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="ok")]) + return AgentResponse(messages=[ChatMessage("assistant", ["ok"])]) def run_stream( self, @@ -437,7 +436,7 @@ class StubManagerAgent(BaseAgent): **kwargs: Any, ) -> AsyncIterable[AgentResponseUpdate]: async def _gen() -> AsyncIterable[AgentResponseUpdate]: - yield AgentResponseUpdate(message_deltas=[ChatMessage(role=Role.ASSISTANT, text="ok")]) + yield AgentResponseUpdate(message_deltas=[ChatMessage("assistant", ["ok"])]) return _gen() @@ -448,8 +447,8 @@ async def test_standard_manager_plan_and_replan_via_complete_monkeypatch(): async def fake_complete_plan(messages: list[ChatMessage], **kwargs: Any) -> ChatMessage: # Return a different response depending on call order length if any("FACTS" in (m.text or "") for m in messages): - return ChatMessage(role=Role.ASSISTANT, text="- step A\n- step B") - return ChatMessage(role=Role.ASSISTANT, text="GIVEN OR VERIFIED FACTS\n- fact1") + return ChatMessage("assistant", ["- step A\n- step B"]) + return ChatMessage("assistant", ["GIVEN OR VERIFIED FACTS\n- fact1"]) # First, patch to produce facts then plan mgr._complete = fake_complete_plan # type: ignore[attr-defined] @@ -464,8 +463,8 @@ async def test_standard_manager_plan_and_replan_via_complete_monkeypatch(): # Now replan with new outputs async def fake_complete_replan(messages: list[ChatMessage], **kwargs: Any) -> ChatMessage: if any("Please briefly explain" in (m.text or "") for m in messages): - return ChatMessage(role=Role.ASSISTANT, text="- new step") - return ChatMessage(role=Role.ASSISTANT, text="GIVEN OR VERIFIED FACTS\n- updated") + return ChatMessage("assistant", ["- new step"]) + return ChatMessage("assistant", ["GIVEN OR VERIFIED FACTS\n- updated"]) mgr._complete = fake_complete_replan # type: ignore[attr-defined] combined2 = await mgr.replan(ctx.clone()) @@ -485,7 +484,7 @@ async def test_standard_manager_progress_ledger_success_and_error(): '"next_speaker": {"reason": "r", "answer": "alice"}, ' '"instruction_or_question": {"reason": "r", "answer": "do"}}' ) - return ChatMessage(role=Role.ASSISTANT, text=json_text) + return ChatMessage("assistant", [json_text]) mgr._complete = fake_complete_ok # type: ignore[attr-defined] ledger = await mgr.create_progress_ledger(ctx.clone()) @@ -493,7 +492,7 @@ async def test_standard_manager_progress_ledger_success_and_error(): # Error path: invalid JSON now raises to avoid emitting planner-oriented instructions to agents async def fake_complete_bad(messages: list[ChatMessage], **kwargs: Any) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="not-json") + return ChatMessage("assistant", ["not-json"]) mgr._complete = fake_complete_bad # type: ignore[attr-defined] with pytest.raises(RuntimeError): @@ -506,10 +505,10 @@ class InvokeOnceManager(MagenticManagerBase): self._invoked = False async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="ledger") + return ChatMessage("assistant", ["ledger"]) async def replan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="re-ledger") + return ChatMessage("assistant", ["re-ledger"]) async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: if not self._invoked: @@ -532,7 +531,7 @@ class InvokeOnceManager(MagenticManagerBase): ) async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="final") + return ChatMessage("assistant", ["final"]) class StubThreadAgent(BaseAgent): @@ -543,11 +542,11 @@ class StubThreadAgent(BaseAgent): yield AgentResponseUpdate( contents=[Content.from_text(text="thread-ok")], author_name=self.name, - role=Role.ASSISTANT, + role="assistant", ) async def run(self, messages=None, *, thread=None, **kwargs): # type: ignore[override] - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="thread-ok", author_name=self.name)]) + return AgentResponse(messages=[ChatMessage("assistant", ["thread-ok"], author_name=self.name)]) class StubAssistantsClient: @@ -565,11 +564,11 @@ class StubAssistantsAgent(BaseAgent): yield AgentResponseUpdate( contents=[Content.from_text(text="assistants-ok")], author_name=self.name, - role=Role.ASSISTANT, + role="assistant", ) async def run(self, messages=None, *, thread=None, **kwargs): # type: ignore[override] - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="assistants-ok", author_name=self.name)]) + return AgentResponse(messages=[ChatMessage("assistant", ["assistants-ok"], author_name=self.name)]) async def _collect_agent_responses_setup(participant: AgentProtocol) -> list[ChatMessage]: @@ -586,7 +585,7 @@ async def _collect_agent_responses_setup(participant: AgentProtocol) -> list[Cha if isinstance(ev, AgentRunUpdateEvent): captured.append( ChatMessage( - role=ev.data.role or Role.ASSISTANT, + role=ev.data.role or "assistant", text=ev.data.text or "", author_name=ev.data.author_name, ) @@ -738,10 +737,10 @@ class NotProgressingManager(MagenticManagerBase): """ async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="ledger") + return ChatMessage("assistant", ["ledger"]) async def replan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="re-ledger") + return ChatMessage("assistant", ["re-ledger"]) async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: return MagenticProgressLedger( @@ -753,7 +752,7 @@ class NotProgressingManager(MagenticManagerBase): ) async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="final") + return ChatMessage("assistant", ["final"]) async def test_magentic_stall_and_reset_reach_limits(): @@ -851,8 +850,8 @@ async def test_magentic_context_no_duplicate_on_reset(): ctx = MagenticContext(task="task", participant_descriptions={"Alice": "Researcher"}) # Add some history - ctx.chat_history.append(ChatMessage(role=Role.ASSISTANT, text="response1")) - ctx.chat_history.append(ChatMessage(role=Role.ASSISTANT, text="response2")) + ctx.chat_history.append(ChatMessage("assistant", ["response1"])) + ctx.chat_history.append(ChatMessage("assistant", ["response2"])) assert len(ctx.chat_history) == 2 # Reset @@ -862,7 +861,7 @@ async def test_magentic_context_no_duplicate_on_reset(): assert len(ctx.chat_history) == 0, "chat_history should be empty after reset" # Add new history - ctx.chat_history.append(ChatMessage(role=Role.ASSISTANT, text="new_response")) + ctx.chat_history.append(ChatMessage("assistant", ["new_response"])) assert len(ctx.chat_history) == 1, "Should have exactly 1 message after adding to reset context" @@ -881,7 +880,7 @@ async def test_magentic_checkpoint_restore_no_duplicate_history(): # Run with conversation history to create initial checkpoint conversation: list[ChatMessage] = [ - ChatMessage(role=Role.USER, text="task_msg"), + ChatMessage("user", ["task_msg"]), ] async for event in wf.run_stream(conversation): @@ -1248,8 +1247,8 @@ def test_magentic_agent_factory_with_standard_manager_options(): from agent_framework._workflows._magentic import _MagenticTaskLedger # type: ignore custom_task_ledger = _MagenticTaskLedger( - facts=ChatMessage(role=Role.ASSISTANT, text="Custom facts"), - plan=ChatMessage(role=Role.ASSISTANT, text="Custom plan"), + facts=ChatMessage("assistant", ["Custom facts"]), + plan=ChatMessage("assistant", ["Custom plan"]), ) participant = StubAgent("agentA", "reply from agentA") diff --git a/python/packages/core/tests/workflow/test_orchestration_request_info.py b/python/packages/core/tests/workflow/test_orchestration_request_info.py index 24b2239757..787a2c6642 100644 --- a/python/packages/core/tests/workflow/test_orchestration_request_info.py +++ b/python/packages/core/tests/workflow/test_orchestration_request_info.py @@ -14,7 +14,6 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, ChatMessage, - Role, ) from agent_framework._workflows._agent_executor import AgentExecutorRequest, AgentExecutorResponse from agent_framework._workflows._orchestration_request_info import ( @@ -73,7 +72,7 @@ class TestAgentRequestInfoResponse: def test_create_response_with_messages(self): """Test creating an AgentRequestInfoResponse with messages.""" - messages = [ChatMessage(role=Role.USER, text="Additional info")] + messages = [ChatMessage("user", ["Additional info"])] response = AgentRequestInfoResponse(messages=messages) assert response.messages == messages @@ -81,8 +80,8 @@ class TestAgentRequestInfoResponse: def test_from_messages_factory(self): """Test creating response from ChatMessage list.""" messages = [ - ChatMessage(role=Role.USER, text="Message 1"), - ChatMessage(role=Role.USER, text="Message 2"), + ChatMessage("user", ["Message 1"]), + ChatMessage("user", ["Message 2"]), ] response = AgentRequestInfoResponse.from_messages(messages) @@ -94,9 +93,9 @@ class TestAgentRequestInfoResponse: response = AgentRequestInfoResponse.from_strings(texts) assert len(response.messages) == 2 - assert response.messages[0].role == Role.USER + assert response.messages[0].role == "user" assert response.messages[0].text == "First message" - assert response.messages[1].role == Role.USER + assert response.messages[1].role == "user" assert response.messages[1].text == "Second message" def test_approve_factory(self): @@ -114,7 +113,7 @@ class TestAgentRequestInfoExecutor: """Test that request_info handler calls ctx.request_info.""" executor = AgentRequestInfoExecutor(id="test_executor") - agent_response = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Agent response")]) + agent_response = AgentResponse(messages=[ChatMessage("assistant", ["Agent response"])]) agent_response = AgentExecutorResponse( executor_id="test_agent", agent_response=agent_response, @@ -132,7 +131,7 @@ class TestAgentRequestInfoExecutor: """Test response handler when user provides additional messages.""" executor = AgentRequestInfoExecutor(id="test_executor") - agent_response = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Original")]) + agent_response = AgentResponse(messages=[ChatMessage("assistant", ["Original"])]) original_request = AgentExecutorResponse( executor_id="test_agent", agent_response=agent_response, @@ -158,7 +157,7 @@ class TestAgentRequestInfoExecutor: """Test response handler when user approves (no additional messages).""" executor = AgentRequestInfoExecutor(id="test_executor") - agent_response = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Original")]) + agent_response = AgentResponse(messages=[ChatMessage("assistant", ["Original"])]) original_request = AgentExecutorResponse( executor_id="test_agent", agent_response=agent_response, @@ -207,7 +206,7 @@ class _TestAgent: **kwargs: Any, ) -> AgentResponse: """Dummy run method.""" - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Test response")]) + return AgentResponse(messages=[ChatMessage("assistant", ["Test response"])]) def run_stream( self, @@ -219,7 +218,7 @@ class _TestAgent: """Dummy run_stream method.""" async def generator(): - yield AgentResponseUpdate(messages=[ChatMessage(role=Role.ASSISTANT, text="Test response stream")]) + yield AgentResponseUpdate(messages=[ChatMessage("assistant", ["Test response stream"])]) return generator() diff --git a/python/packages/core/tests/workflow/test_sequential.py b/python/packages/core/tests/workflow/test_sequential.py index a685db73db..e5b55ae081 100644 --- a/python/packages/core/tests/workflow/test_sequential.py +++ b/python/packages/core/tests/workflow/test_sequential.py @@ -14,7 +14,6 @@ from agent_framework import ( ChatMessage, Content, Executor, - Role, SequentialBuilder, TypeCompatibilityError, WorkflowContext, @@ -36,7 +35,7 @@ class _EchoAgent(BaseAgent): thread: AgentThread | None = None, **kwargs: Any, ) -> AgentResponse: - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.name} reply")]) + return AgentResponse(messages=[ChatMessage("assistant", [f"{self.name} reply"])]) async def run_stream( # type: ignore[override] self, @@ -55,9 +54,9 @@ class _SummarizerExec(Executor): @handler async def summarize(self, agent_response: AgentExecutorResponse, ctx: WorkflowContext[list[ChatMessage]]) -> None: conversation = agent_response.full_conversation or [] - user_texts = [m.text for m in conversation if m.role == Role.USER] - agents = [m.author_name or m.role for m in conversation if m.role == Role.ASSISTANT] - summary = ChatMessage(role=Role.ASSISTANT, text=f"Summary of users:{len(user_texts)} agents:{len(agents)}") + user_texts = [m.text for m in conversation if m.role == "user"] + agents = [m.author_name or m.role for m in conversation if m.role == "assistant"] + summary = ChatMessage("assistant", [f"Summary of users:{len(user_texts)} agents:{len(agents)}"]) await ctx.send_message(list(conversation) + [summary]) @@ -119,9 +118,9 @@ async def test_sequential_agents_append_to_context() -> None: assert isinstance(output, list) msgs: list[ChatMessage] = output assert len(msgs) == 3 - assert msgs[0].role == Role.USER and "hello sequential" in msgs[0].text - assert msgs[1].role == Role.ASSISTANT and (msgs[1].author_name == "A1" or True) - assert msgs[2].role == Role.ASSISTANT and (msgs[2].author_name == "A2" or True) + assert msgs[0].role == "user" and "hello sequential" in msgs[0].text + assert msgs[1].role == "assistant" and (msgs[1].author_name == "A1" or True) + assert msgs[2].role == "assistant" and (msgs[2].author_name == "A2" or True) assert "A1 reply" in msgs[1].text assert "A2 reply" in msgs[2].text @@ -152,9 +151,9 @@ async def test_sequential_register_participants_with_agent_factories() -> None: assert isinstance(output, list) msgs: list[ChatMessage] = output assert len(msgs) == 3 - assert msgs[0].role == Role.USER and "hello factories" in msgs[0].text - assert msgs[1].role == Role.ASSISTANT and "A1 reply" in msgs[1].text - assert msgs[2].role == Role.ASSISTANT and "A2 reply" in msgs[2].text + assert msgs[0].role == "user" and "hello factories" in msgs[0].text + assert msgs[1].role == "assistant" and "A1 reply" in msgs[1].text + assert msgs[2].role == "assistant" and "A2 reply" in msgs[2].text async def test_sequential_with_custom_executor_summary() -> None: @@ -178,9 +177,9 @@ async def test_sequential_with_custom_executor_summary() -> None: msgs: list[ChatMessage] = output # Expect: [user, A1 reply, summary] assert len(msgs) == 3 - assert msgs[0].role == Role.USER - assert msgs[1].role == Role.ASSISTANT and "A1 reply" in msgs[1].text - assert msgs[2].role == Role.ASSISTANT and msgs[2].text.startswith("Summary of users:") + assert msgs[0].role == "user" + assert msgs[1].role == "assistant" and "A1 reply" in msgs[1].text + assert msgs[2].role == "assistant" and msgs[2].text.startswith("Summary of users:") async def test_sequential_register_participants_mixed_agents_and_executors() -> None: @@ -209,9 +208,9 @@ async def test_sequential_register_participants_mixed_agents_and_executors() -> msgs: list[ChatMessage] = output # Expect: [user, A1 reply, summary] assert len(msgs) == 3 - assert msgs[0].role == Role.USER and "topic Y" in msgs[0].text - assert msgs[1].role == Role.ASSISTANT and "A1 reply" in msgs[1].text - assert msgs[2].role == Role.ASSISTANT and msgs[2].text.startswith("Summary of users:") + assert msgs[0].role == "user" and "topic Y" in msgs[0].text + assert msgs[1].role == "assistant" and "A1 reply" in msgs[1].text + assert msgs[2].role == "assistant" and msgs[2].text.startswith("Summary of users:") async def test_sequential_checkpoint_resume_round_trip() -> None: diff --git a/python/packages/core/tests/workflow/test_workflow.py b/python/packages/core/tests/workflow/test_workflow.py index 6b08b7b22a..1bca73b565 100644 --- a/python/packages/core/tests/workflow/test_workflow.py +++ b/python/packages/core/tests/workflow/test_workflow.py @@ -23,7 +23,6 @@ from agent_framework import ( FileCheckpointStorage, Message, RequestInfoEvent, - Role, WorkflowBuilder, WorkflowCheckpointException, WorkflowContext, @@ -869,7 +868,7 @@ class _StreamingTestAgent(BaseAgent): **kwargs: Any, ) -> AgentResponse: """Non-streaming run - returns complete response.""" - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=self._reply_text)]) + return AgentResponse(messages=[ChatMessage("assistant", [self._reply_text])]) async def run_stream( self, diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index 9514efdf74..b12c916d84 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -16,7 +16,6 @@ from agent_framework import ( ChatMessageStore, Content, Executor, - Role, UsageDetails, WorkflowAgent, WorkflowBuilder, @@ -41,11 +40,11 @@ class SimpleExecutor(Executor): response_text = f"{self.response_text}: {input_text}" # Create response message for both streaming and non-streaming cases - response_message = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)]) + response_message = ChatMessage("assistant", [Content.from_text(text=response_text)]) # Emit update event. streaming_update = AgentResponseUpdate( - contents=[Content.from_text(text=response_text)], role=Role.ASSISTANT, message_id=str(uuid.uuid4()) + contents=[Content.from_text(text=response_text)], role="assistant", message_id=str(uuid.uuid4()) ) await ctx.add_event(AgentRunUpdateEvent(executor_id=self.id, data=streaming_update)) @@ -68,7 +67,7 @@ class RequestingExecutor(Executor): # Handle the response and emit completion response update = AgentResponseUpdate( contents=[Content.from_text(text="Request completed successfully")], - role=Role.ASSISTANT, + role="assistant", message_id=str(uuid.uuid4()), ) await ctx.add_event(AgentRunUpdateEvent(executor_id=self.id, data=update)) @@ -90,10 +89,10 @@ class ConversationHistoryCapturingExecutor(Executor): message_count = len(messages) response_text = f"Received {message_count} messages" - response_message = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)]) + response_message = ChatMessage("assistant", [Content.from_text(text=response_text)]) streaming_update = AgentResponseUpdate( - contents=[Content.from_text(text=response_text)], role=Role.ASSISTANT, message_id=str(uuid.uuid4()) + contents=[Content.from_text(text=response_text)], role="assistant", message_id=str(uuid.uuid4()) ) await ctx.add_event(AgentRunUpdateEvent(executor_id=self.id, data=streaming_update)) await ctx.send_message([response_message]) @@ -232,7 +231,7 @@ class TestWorkflowAgent: ), ) - response_message = ChatMessage(role=Role.USER, contents=[approval_response]) + response_message = ChatMessage("user", [approval_response]) # Continue the workflow with the response continuation_result = await agent.run(response_message) @@ -295,7 +294,7 @@ class TestWorkflowAgent: workflow = WorkflowBuilder().set_start_executor(yielding_executor).build() # Run directly - should return WorkflowOutputEvent in result - direct_result = await workflow.run([ChatMessage(role=Role.USER, contents=[Content.from_text(text="hello")])]) + direct_result = await workflow.run([ChatMessage("user", [Content.from_text(text="hello")])]) direct_outputs = direct_result.get_outputs() assert len(direct_outputs) == 1 assert direct_outputs[0] == "processed: hello" @@ -362,7 +361,7 @@ class TestWorkflowAgent: @executor async def chat_message_executor(messages: list[ChatMessage], ctx: WorkflowContext) -> None: msg = ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(text="response text")], author_name="custom-author", ) @@ -374,7 +373,7 @@ class TestWorkflowAgent: result = await agent.run("test") assert len(result.messages) == 1 - assert result.messages[0].role == Role.ASSISTANT + assert result.messages[0].role == "assistant" assert result.messages[0].text == "response text" assert result.messages[0].author_name == "custom-author" @@ -425,10 +424,10 @@ class TestWorkflowAgent: async def list_yielding_executor(messages: list[ChatMessage], ctx: WorkflowContext) -> None: # Yield a list of ChatMessages (as SequentialBuilder does) msg_list = [ - ChatMessage(role=Role.USER, contents=[Content.from_text(text="first message")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="second message")]), + ChatMessage("user", [Content.from_text(text="first message")]), + ChatMessage("assistant", [Content.from_text(text="second message")]), ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(text="third"), Content.from_text(text="fourth")], ), ] @@ -469,8 +468,8 @@ class TestWorkflowAgent: # Create a thread with existing conversation history history_messages = [ - ChatMessage(role=Role.USER, text="Previous user message"), - ChatMessage(role=Role.ASSISTANT, text="Previous assistant response"), + ChatMessage("user", ["Previous user message"]), + ChatMessage("assistant", ["Previous assistant response"]), ] message_store = ChatMessageStore(messages=history_messages) thread = AgentThread(message_store=message_store) @@ -499,9 +498,9 @@ class TestWorkflowAgent: # Create a thread with existing conversation history history_messages = [ - ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant"), - ChatMessage(role=Role.USER, text="Hello"), - ChatMessage(role=Role.ASSISTANT, text="Hi there!"), + ChatMessage("system", ["You are a helpful assistant"]), + ChatMessage("user", ["Hello"]), + ChatMessage("assistant", ["Hi there!"]), ] message_store = ChatMessageStore(messages=history_messages) thread = AgentThread(message_store=message_store) @@ -579,7 +578,7 @@ class TestWorkflowAgent: async def run(self, messages: Any, *, thread: AgentThread | None = None, **kwargs: Any) -> AgentResponse: return AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text=self._response_text)], + messages=[ChatMessage("assistant", [self._response_text])], text=self._response_text, ) @@ -589,7 +588,7 @@ class TestWorkflowAgent: for word in self._response_text.split(): yield AgentResponseUpdate( contents=[Content.from_text(text=word + " ")], - role=Role.ASSISTANT, + role="assistant", author_name=self._name, ) @@ -653,7 +652,7 @@ class TestWorkflowAgent: async def run(self, messages: Any, *, thread: AgentThread | None = None, **kwargs: Any) -> AgentResponse: return AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text=self._response_text)], + messages=[ChatMessage("assistant", [self._response_text])], text=self._response_text, ) @@ -662,7 +661,7 @@ class TestWorkflowAgent: ) -> AsyncIterable[AgentResponseUpdate]: yield AgentResponseUpdate( contents=[Content.from_text(text=self._response_text)], - role=Role.ASSISTANT, + role="assistant", author_name=self._name, ) @@ -728,7 +727,7 @@ class TestWorkflowAgentAuthorName: # Emit update with explicit author_name update = AgentResponseUpdate( contents=[Content.from_text(text="Response with author")], - role=Role.ASSISTANT, + role="assistant", author_name="custom_author_name", # Explicitly set message_id=str(uuid.uuid4()), ) @@ -780,7 +779,7 @@ class TestWorkflowAgentMergeUpdates: # Response B, Message 2 (latest in resp B) AgentResponseUpdate( contents=[Content.from_text(text="RespB-Msg2")], - role=Role.ASSISTANT, + role="assistant", response_id="resp-b", message_id="msg-2", created_at="2024-01-01T12:02:00Z", @@ -788,7 +787,7 @@ class TestWorkflowAgentMergeUpdates: # Response A, Message 1 (earliest overall) AgentResponseUpdate( contents=[Content.from_text(text="RespA-Msg1")], - role=Role.ASSISTANT, + role="assistant", response_id="resp-a", message_id="msg-1", created_at="2024-01-01T12:00:00Z", @@ -796,7 +795,7 @@ class TestWorkflowAgentMergeUpdates: # Response B, Message 1 (earlier in resp B) AgentResponseUpdate( contents=[Content.from_text(text="RespB-Msg1")], - role=Role.ASSISTANT, + role="assistant", response_id="resp-b", message_id="msg-1", created_at="2024-01-01T12:01:00Z", @@ -804,7 +803,7 @@ class TestWorkflowAgentMergeUpdates: # Response A, Message 2 (later in resp A) AgentResponseUpdate( contents=[Content.from_text(text="RespA-Msg2")], - role=Role.ASSISTANT, + role="assistant", response_id="resp-a", message_id="msg-2", created_at="2024-01-01T12:00:30Z", @@ -812,7 +811,7 @@ class TestWorkflowAgentMergeUpdates: # Global dangling update (no response_id) - should go at end AgentResponseUpdate( contents=[Content.from_text(text="Global-Dangling")], - role=Role.ASSISTANT, + role="assistant", response_id=None, message_id="msg-global", created_at="2024-01-01T11:59:00Z", # Earliest timestamp but should be last @@ -886,7 +885,7 @@ class TestWorkflowAgentMergeUpdates: usage_details={"input_token_count": 10, "output_token_count": 5, "total_token_count": 15} ), ], - role=Role.ASSISTANT, + role="assistant", response_id="resp-1", message_id="msg-1", created_at="2024-01-01T12:00:00Z", @@ -899,7 +898,7 @@ class TestWorkflowAgentMergeUpdates: usage_details={"input_token_count": 20, "output_token_count": 8, "total_token_count": 28} ), ], - role=Role.ASSISTANT, + role="assistant", response_id="resp-2", message_id="msg-2", created_at="2024-01-01T12:01:00Z", # Later timestamp @@ -912,7 +911,7 @@ class TestWorkflowAgentMergeUpdates: usage_details={"input_token_count": 5, "output_token_count": 3, "total_token_count": 8} ), ], - role=Role.ASSISTANT, + role="assistant", response_id="resp-1", # Same response_id as first message_id="msg-3", created_at="2024-01-01T11:59:00Z", # Earlier timestamp @@ -975,7 +974,7 @@ class TestWorkflowAgentMergeUpdates: # User question AgentResponseUpdate( contents=[Content.from_text(text="What is the weather?")], - role=Role.USER, + role="user", response_id="resp-1", message_id="msg-1", created_at="2024-01-01T12:00:00Z", @@ -985,7 +984,7 @@ class TestWorkflowAgentMergeUpdates: contents=[ Content.from_function_call(call_id=call_id, name="get_weather", arguments='{"location": "NYC"}') ], - role=Role.ASSISTANT, + role="assistant", response_id="resp-1", message_id="msg-2", created_at="2024-01-01T12:00:01Z", @@ -994,7 +993,7 @@ class TestWorkflowAgentMergeUpdates: # and be placed at the end (the bug); fix now correctly associates via call_id AgentResponseUpdate( contents=[Content.from_function_result(call_id=call_id, result="Sunny, 72F")], - role=Role.TOOL, + role="tool", response_id=None, message_id="msg-3", created_at="2024-01-01T12:00:02Z", @@ -1002,7 +1001,7 @@ class TestWorkflowAgentMergeUpdates: # Final assistant answer AgentResponseUpdate( contents=[Content.from_text(text="The weather in NYC is sunny and 72F.")], - role=Role.ASSISTANT, + role="assistant", response_id="resp-1", message_id="msg-4", created_at="2024-01-01T12:00:03Z", @@ -1026,10 +1025,10 @@ class TestWorkflowAgentMergeUpdates: # Verify correct ordering: user -> function_call -> function_result -> assistant_answer expected_sequence = [ - ("text", Role.USER), - ("function_call", Role.ASSISTANT), - ("function_result", Role.TOOL), - ("text", Role.ASSISTANT), + ("text", "user"), + ("function_call", "assistant"), + ("function_result", "tool"), + ("text", "assistant"), ] assert content_sequence == expected_sequence, ( @@ -1073,7 +1072,7 @@ class TestWorkflowAgentMergeUpdates: # User question AgentResponseUpdate( contents=[Content.from_text(text="What's the weather and time?")], - role=Role.USER, + role="user", response_id="resp-1", message_id="msg-1", created_at="2024-01-01T12:00:00Z", @@ -1083,7 +1082,7 @@ class TestWorkflowAgentMergeUpdates: contents=[ Content.from_function_call(call_id=call_id_1, name="get_weather", arguments='{"location": "NYC"}') ], - role=Role.ASSISTANT, + role="assistant", response_id="resp-1", message_id="msg-2", created_at="2024-01-01T12:00:01Z", @@ -1093,7 +1092,7 @@ class TestWorkflowAgentMergeUpdates: contents=[ Content.from_function_call(call_id=call_id_2, name="get_time", arguments='{"timezone": "EST"}') ], - role=Role.ASSISTANT, + role="assistant", response_id="resp-1", message_id="msg-3", created_at="2024-01-01T12:00:02Z", @@ -1101,7 +1100,7 @@ class TestWorkflowAgentMergeUpdates: # Second function result arrives first (no response_id) AgentResponseUpdate( contents=[Content.from_function_result(call_id=call_id_2, result="3:00 PM EST")], - role=Role.TOOL, + role="tool", response_id=None, message_id="msg-4", created_at="2024-01-01T12:00:03Z", @@ -1109,7 +1108,7 @@ class TestWorkflowAgentMergeUpdates: # First function result arrives second (no response_id) AgentResponseUpdate( contents=[Content.from_function_result(call_id=call_id_1, result="Sunny, 72F")], - role=Role.TOOL, + role="tool", response_id=None, message_id="msg-5", created_at="2024-01-01T12:00:04Z", @@ -1117,7 +1116,7 @@ class TestWorkflowAgentMergeUpdates: # Final assistant answer AgentResponseUpdate( contents=[Content.from_text(text="It's sunny (72F) and 3 PM in NYC.")], - role=Role.ASSISTANT, + role="assistant", response_id="resp-1", message_id="msg-6", created_at="2024-01-01T12:00:05Z", @@ -1168,7 +1167,7 @@ class TestWorkflowAgentMergeUpdates: updates = [ AgentResponseUpdate( contents=[Content.from_text(text="Hello")], - role=Role.USER, + role="user", response_id="resp-1", message_id="msg-1", created_at="2024-01-01T12:00:00Z", @@ -1176,14 +1175,14 @@ class TestWorkflowAgentMergeUpdates: # Function result with no matching call AgentResponseUpdate( contents=[Content.from_function_result(call_id="orphan_call_id", result="orphan result")], - role=Role.TOOL, + role="tool", response_id=None, message_id="msg-2", created_at="2024-01-01T12:00:01Z", ), AgentResponseUpdate( contents=[Content.from_text(text="Goodbye")], - role=Role.ASSISTANT, + role="assistant", response_id="resp-1", message_id="msg-3", created_at="2024-01-01T12:00:02Z", diff --git a/python/packages/core/tests/workflow/test_workflow_builder.py b/python/packages/core/tests/workflow/test_workflow_builder.py index ef572ba82b..26bee34f6c 100644 --- a/python/packages/core/tests/workflow/test_workflow_builder.py +++ b/python/packages/core/tests/workflow/test_workflow_builder.py @@ -13,7 +13,6 @@ from agent_framework import ( BaseAgent, ChatMessage, Executor, - Role, WorkflowBuilder, WorkflowContext, handler, @@ -28,7 +27,7 @@ class DummyAgent(BaseAgent): if isinstance(m, ChatMessage): norm.append(m) elif isinstance(m, str): - norm.append(ChatMessage(role=Role.USER, text=m)) + norm.append(ChatMessage("user", [m])) return AgentResponse(messages=norm) async def run_stream(self, messages=None, *, thread: AgentThread | None = None, **kwargs): # type: ignore[override] diff --git a/python/packages/core/tests/workflow/test_workflow_kwargs.py b/python/packages/core/tests/workflow/test_workflow_kwargs.py index 79aa009f57..763a911351 100644 --- a/python/packages/core/tests/workflow/test_workflow_kwargs.py +++ b/python/packages/core/tests/workflow/test_workflow_kwargs.py @@ -16,7 +16,6 @@ from agent_framework import ( GroupChatBuilder, GroupChatState, HandoffBuilder, - Role, SequentialBuilder, WorkflowRunState, WorkflowStatusEvent, @@ -57,7 +56,7 @@ class _KwargsCapturingAgent(BaseAgent): **kwargs: Any, ) -> AgentResponse: self.captured_kwargs.append(dict(kwargs)) - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.name} response")]) + return AgentResponse(messages=[ChatMessage("assistant", [f"{self.name} response"])]) async def run_stream( self, @@ -387,10 +386,10 @@ async def test_magentic_kwargs_flow_to_agents() -> None: self.task_ledger = None async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="Plan: Test task", author_name="manager") + return ChatMessage("assistant", ["Plan: Test task"], author_name="manager") async def replan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="Replan: Test task", author_name="manager") + return ChatMessage("assistant", ["Replan: Test task"], author_name="manager") async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: # Return completed on first call @@ -403,7 +402,7 @@ async def test_magentic_kwargs_flow_to_agents() -> None: ) async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="Final answer", author_name="manager") + return ChatMessage("assistant", ["Final answer"], author_name="manager") agent = _KwargsCapturingAgent(name="agent1") manager = _MockManager() @@ -437,10 +436,10 @@ async def test_magentic_kwargs_stored_in_shared_state() -> None: self.task_ledger = None async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="Plan", author_name="manager") + return ChatMessage("assistant", ["Plan"], author_name="manager") async def replan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="Replan", author_name="manager") + return ChatMessage("assistant", ["Replan"], author_name="manager") async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: return MagenticProgressLedger( @@ -452,7 +451,7 @@ async def test_magentic_kwargs_stored_in_shared_state() -> None: ) async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role=Role.ASSISTANT, text="Final", author_name="manager") + return ChatMessage("assistant", ["Final"], author_name="manager") agent = _KwargsCapturingAgent(name="agent1") manager = _MockManager() diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_actions_agents.py b/python/packages/declarative/agent_framework_declarative/_workflows/_actions_agents.py index 9d610d057d..390eb0a991 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_actions_agents.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_actions_agents.py @@ -285,11 +285,11 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl evaluated_input = ctx.state.eval_if_expression(input_messages) if evaluated_input: if isinstance(evaluated_input, str): - messages.append(ChatMessage(role="user", text=evaluated_input)) + messages.append(ChatMessage("user", [evaluated_input])) elif isinstance(evaluated_input, list): for msg_item in evaluated_input: # type: ignore if isinstance(msg_item, str): - messages.append(ChatMessage(role="user", text=msg_item)) + messages.append(ChatMessage("user", [msg_item])) elif isinstance(msg_item, ChatMessage): messages.append(msg_item) elif isinstance(msg_item, dict) and "content" in msg_item: @@ -297,11 +297,11 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl role: str = str(item_dict.get("role", "user")) content: str = str(item_dict.get("content", "")) if role == "user": - messages.append(ChatMessage(role="user", text=content)) + messages.append(ChatMessage("user", [content])) elif role == "assistant": - messages.append(ChatMessage(role="assistant", text=content)) + messages.append(ChatMessage("assistant", [content])) elif role == "system": - messages.append(ChatMessage(role="system", text=content)) + messages.append(ChatMessage("system", [content])) # Evaluate and include input arguments evaluated_args: dict[str, Any] = {} @@ -348,7 +348,7 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl tool_calls.extend(chunk.tool_calls) # Build consolidated response from updates - response = AgentResponse.from_agent_run_response_updates(updates) + response = AgentResponse.from_updates(updates) text = response.text response_messages = response.messages @@ -361,7 +361,7 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl # Add to conversation history if text: - ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text)) + ctx.state.add_conversation_message(ChatMessage("assistant", [text])) # Store in output variables (.NET style) if output_messages_var: @@ -414,7 +414,7 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl # Add to conversation history if text: - ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text)) + ctx.state.add_conversation_message(ChatMessage("assistant", [text])) # Store in output variables (.NET style) if output_messages_var: @@ -560,7 +560,7 @@ async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[Workf # Add input as user message if provided if input_value: if isinstance(input_value, str): - messages.append(ChatMessage(role="user", text=input_value)) + messages.append(ChatMessage("user", [input_value])) elif isinstance(input_value, ChatMessage): messages.append(input_value) @@ -581,14 +581,14 @@ async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[Workf ) # Build consolidated response from updates - response = AgentResponse.from_agent_run_response_updates(updates) + response = AgentResponse.from_updates(updates) text = response.text response_messages = response.messages ctx.state.set_agent_result(text=text, messages=response_messages) if text: - ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text)) + ctx.state.add_conversation_message(ChatMessage("assistant", [text])) if output_path: ctx.state.set(output_path, text) @@ -607,7 +607,7 @@ async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[Workf ctx.state.set_agent_result(text=text, messages=response_messages) if text: - ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text)) + ctx.state.add_conversation_message(ChatMessage("assistant", [text])) if output_path: ctx.state.set(output_path, text) diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py index 18685ef401..d75c62e807 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py @@ -642,7 +642,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor): # Add user input to conversation history first (via state.append only) if input_text: - user_message = ChatMessage(role="user", text=input_text) + user_message = ChatMessage("user", [input_text]) await state.append(messages_path, user_message) # Get conversation history from state AFTER adding user message @@ -711,7 +711,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor): "Agent '%s': No messages in response, creating simple assistant message", agent_name, ) - assistant_message = ChatMessage(role="assistant", text=accumulated_response) + assistant_message = ChatMessage("assistant", [accumulated_response]) await state.append(messages_path, assistant_message) # Store results in state - support both schema formats: diff --git a/python/packages/devui/agent_framework_devui/_conversations.py b/python/packages/devui/agent_framework_devui/_conversations.py index 868ca3e162..8321e6a6aa 100644 --- a/python/packages/devui/agent_framework_devui/_conversations.py +++ b/python/packages/devui/agent_framework_devui/_conversations.py @@ -303,7 +303,7 @@ class InMemoryConversationStore(ConversationStore): content = item.get("content", []) text = content[0].get("text", "") if content else "" - chat_msg = ChatMessage(role=role, contents=[{"type": "text", "text": text}]) + chat_msg = ChatMessage(role, [{"type": "text", "text": text}]) chat_messages.append(chat_msg) # Add messages to AgentThread @@ -315,7 +315,7 @@ class InMemoryConversationStore(ConversationStore): item_id = f"item_{uuid.uuid4().hex}" # Extract role - handle both string and enum - role_str = msg.role.value if hasattr(msg.role, "value") else str(msg.role) + role_str = msg.role if hasattr(msg.role, "value") else str(msg.role) role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles # Convert ChatMessage contents to OpenAI TextContent format @@ -373,7 +373,7 @@ class InMemoryConversationStore(ConversationStore): # Convert each AgentFramework ChatMessage to appropriate ConversationItem type(s) for i, msg in enumerate(af_messages): item_id = f"item_{i}" - role_str = msg.role.value if hasattr(msg.role, "value") else str(msg.role) + role_str = msg.role if hasattr(msg.role, "value") else str(msg.role) role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles # Process each content item in the message diff --git a/python/packages/devui/agent_framework_devui/_executor.py b/python/packages/devui/agent_framework_devui/_executor.py index cf4fa0066f..9f60678386 100644 --- a/python/packages/devui/agent_framework_devui/_executor.py +++ b/python/packages/devui/agent_framework_devui/_executor.py @@ -760,7 +760,7 @@ class AgentFrameworkExecutor: if not contents: contents.append(Content.from_text(text="")) - chat_message = ChatMessage(role=Role.USER, contents=contents) + chat_message = ChatMessage("user", contents) logger.info(f"Created ChatMessage with {len(contents)} contents:") for idx, content in enumerate(contents): diff --git a/python/packages/devui/tests/test_cleanup_hooks.py b/python/packages/devui/tests/test_cleanup_hooks.py index e821779686..68c8ff6af2 100644 --- a/python/packages/devui/tests/test_cleanup_hooks.py +++ b/python/packages/devui/tests/test_cleanup_hooks.py @@ -7,7 +7,7 @@ import tempfile from pathlib import Path import pytest -from agent_framework import AgentResponse, ChatMessage, Content, Role +from agent_framework import AgentResponse, ChatMessage, Content from agent_framework_devui import register_cleanup from agent_framework_devui._discovery import EntityDiscovery @@ -36,7 +36,7 @@ class MockAgent: async def run_stream(self, messages=None, *, thread=None, **kwargs): """Mock streaming run method.""" yield AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Test response")])], + messages=[ChatMessage("assistant", [Content.from_text(text="Test response")])], ) @@ -279,7 +279,7 @@ class TestAgent: async def run_stream(self, messages=None, *, thread=None, **kwargs): yield AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, content=[Content.from_text(text="Test")])], + messages=[ChatMessage("assistant", [Content.from_text(text="Test")])], inner_messages=[], ) diff --git a/python/packages/devui/tests/test_conversations.py b/python/packages/devui/tests/test_conversations.py index eb2d6f3c76..cd1451f79b 100644 --- a/python/packages/devui/tests/test_conversations.py +++ b/python/packages/devui/tests/test_conversations.py @@ -199,7 +199,7 @@ async def test_list_items_pagination(): @pytest.mark.asyncio async def test_list_items_converts_function_calls(): """Test that list_items properly converts function calls to ResponseFunctionToolCallItem.""" - from agent_framework import ChatMessage, ChatMessageStore, Role + from agent_framework import ChatMessage, ChatMessageStore store = InMemoryConversationStore() @@ -216,9 +216,9 @@ async def test_list_items_converts_function_calls(): # Simulate messages from agent execution with function calls messages = [ - ChatMessage(role=Role.USER, contents=[{"type": "text", "text": "What's the weather in SF?"}]), + ChatMessage("user", [{"type": "text", "text": "What's the weather in SF?"}]), ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ { "type": "function_call", @@ -229,7 +229,7 @@ async def test_list_items_converts_function_calls(): ], ), ChatMessage( - role=Role.TOOL, + role="tool", contents=[ { "type": "function_result", @@ -238,7 +238,7 @@ async def test_list_items_converts_function_calls(): } ], ), - ChatMessage(role=Role.ASSISTANT, contents=[{"type": "text", "text": "The weather is sunny, 65°F"}]), + ChatMessage("assistant", [{"type": "text", "text": "The weather is sunny, 65°F"}]), ] # Add messages to thread @@ -284,7 +284,7 @@ async def test_list_items_converts_function_calls(): @pytest.mark.asyncio async def test_list_items_handles_images_and_files(): """Test that list_items properly converts data content (images/files) to OpenAI types.""" - from agent_framework import ChatMessage, ChatMessageStore, Role + from agent_framework import ChatMessage, ChatMessageStore store = InMemoryConversationStore() @@ -301,7 +301,7 @@ async def test_list_items_handles_images_and_files(): # Simulate message with image and file messages = [ ChatMessage( - role=Role.USER, + role="user", contents=[ {"type": "text", "text": "Check this image and PDF"}, {"type": "data", "uri": "data:image/png;base64,iVBORw0KGgo=", "media_type": "image/png"}, diff --git a/python/packages/devui/tests/test_discovery.py b/python/packages/devui/tests/test_discovery.py index f2b321d75c..8b0cf9fb3a 100644 --- a/python/packages/devui/tests/test_discovery.py +++ b/python/packages/devui/tests/test_discovery.py @@ -94,7 +94,7 @@ class NonStreamingAgent: async def run(self, messages=None, *, thread=None, **kwargs): return AgentResponse( messages=[ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(text="response")] )], response_id="test" @@ -210,7 +210,7 @@ class TestAgent: async def run(self, messages=None, *, thread=None, **kwargs): return AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="test")])], + messages=[ChatMessage("assistant", [Content.from_text(text="test")])], response_id="test" ) diff --git a/python/packages/devui/tests/test_execution.py b/python/packages/devui/tests/test_execution.py index e367782597..ce763d227e 100644 --- a/python/packages/devui/tests/test_execution.py +++ b/python/packages/devui/tests/test_execution.py @@ -566,7 +566,7 @@ def test_extract_workflow_hil_responses_handles_stringified_json(): async def test_executor_handles_non_streaming_agent(): """Test executor can handle agents with only run() method (no run_stream).""" - from agent_framework import AgentResponse, AgentThread, ChatMessage, Content, Role + from agent_framework import AgentResponse, AgentThread, ChatMessage, Content class NonStreamingAgent: """Agent with only run() method - does NOT satisfy full AgentProtocol.""" @@ -577,9 +577,7 @@ async def test_executor_handles_non_streaming_agent(): async def run(self, messages=None, *, thread=None, **kwargs): return AgentResponse( - messages=[ - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=f"Processed: {messages}")]) - ], + messages=[ChatMessage("assistant", [Content.from_text(text=f"Processed: {messages}")])], response_id="test_123", ) diff --git a/python/packages/devui/tests/test_helpers.py b/python/packages/devui/tests/test_helpers.py index 130ab475d9..d0d9b36b6e 100644 --- a/python/packages/devui/tests/test_helpers.py +++ b/python/packages/devui/tests/test_helpers.py @@ -29,7 +29,6 @@ from agent_framework import ( ChatResponseUpdate, ConcurrentBuilder, Content, - Role, SequentialBuilder, use_chat_middleware, ) @@ -79,7 +78,7 @@ class MockChatClient: self.call_count += 1 if self.responses: return self.responses.pop(0) - return ChatResponse(messages=ChatMessage(role="assistant", text="test response")) + return ChatResponse(messages=ChatMessage("assistant", ["test response"])) async def get_streaming_response( self, @@ -91,7 +90,7 @@ class MockChatClient: for update in self.streaming_responses.pop(0): yield update else: - yield ChatResponseUpdate(text=Content.from_text(text="test streaming response"), role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(text="test streaming response")], role="assistant") @use_chat_middleware @@ -122,7 +121,7 @@ class MockBaseChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]): self.received_messages.append(list(messages)) if self.run_responses: return self.run_responses.pop(0) - return ChatResponse(messages=ChatMessage(role="assistant", text="Mock response from ChatAgent")) + return ChatResponse(messages=ChatMessage("assistant", ["Mock response from ChatAgent"])) @override async def _inner_get_streaming_response( @@ -139,10 +138,10 @@ class MockBaseChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]): yield update else: # Simulate realistic streaming chunks - yield ChatResponseUpdate(text=Content.from_text(text="Mock "), role="assistant") - yield ChatResponseUpdate(text=Content.from_text(text="streaming "), role="assistant") - yield ChatResponseUpdate(text=Content.from_text(text="response "), role="assistant") - yield ChatResponseUpdate(text=Content.from_text(text="from ChatAgent"), role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(text="Mock ")], role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(text="streaming ")], role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(text="response ")], role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(text="from ChatAgent")], role="assistant") # ============================================================================= @@ -172,9 +171,7 @@ class MockAgent(BaseAgent): **kwargs: Any, ) -> AgentResponse: self.call_count += 1 - return AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=self.response_text)])] - ) + return AgentResponse(messages=[ChatMessage("assistant", [Content.from_text(text=self.response_text)])]) async def run_stream( self, @@ -185,7 +182,7 @@ class MockAgent(BaseAgent): ) -> AsyncIterable[AgentResponseUpdate]: self.call_count += 1 for chunk in self.streaming_chunks: - yield AgentResponseUpdate(contents=[Content.from_text(text=chunk)], role=Role.ASSISTANT) + yield AgentResponseUpdate(contents=[Content.from_text(text=chunk)], role="assistant") class MockToolCallingAgent(BaseAgent): @@ -203,7 +200,7 @@ class MockToolCallingAgent(BaseAgent): **kwargs: Any, ) -> AgentResponse: self.call_count += 1 - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="done")]) + return AgentResponse(messages=[ChatMessage("assistant", ["done"])]) async def run_stream( self, @@ -216,7 +213,7 @@ class MockToolCallingAgent(BaseAgent): # First: text yield AgentResponseUpdate( contents=[Content.from_text(text="Let me search for that...")], - role=Role.ASSISTANT, + role="assistant", ) # Second: tool call yield AgentResponseUpdate( @@ -227,7 +224,7 @@ class MockToolCallingAgent(BaseAgent): arguments={"query": "weather"}, ) ], - role=Role.ASSISTANT, + role="assistant", ) # Third: tool result yield AgentResponseUpdate( @@ -237,12 +234,12 @@ class MockToolCallingAgent(BaseAgent): result={"temperature": 72, "condition": "sunny"}, ) ], - role=Role.TOOL, + role="tool", ) # Fourth: final text yield AgentResponseUpdate( contents=[Content.from_text(text="The weather is sunny, 72°F.")], - role=Role.ASSISTANT, + role="assistant", ) @@ -295,7 +292,7 @@ def create_mock_tool_agent(id: str = "tool_agent", name: str = "ToolAgent") -> M def create_agent_run_response(text: str = "Test response") -> AgentResponse: """Create an AgentResponse with the given text.""" - return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=text)])]) + return AgentResponse(messages=[ChatMessage("assistant", [Content.from_text(text=text)])]) def create_agent_executor_response( @@ -308,8 +305,8 @@ def create_agent_executor_response( executor_id=executor_id, agent_response=agent_response, full_conversation=[ - ChatMessage(role=Role.USER, contents=[Content.from_text(text="User input")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)]), + ChatMessage("user", [Content.from_text(text="User input")]), + ChatMessage("assistant", [Content.from_text(text=response_text)]), ], ) @@ -391,8 +388,8 @@ async def create_sequential_workflow() -> tuple[AgentFrameworkExecutor, str, Moc """ mock_client = MockBaseChatClient() mock_client.run_responses = [ - ChatResponse(messages=ChatMessage(role=Role.ASSISTANT, text="Here's the draft content about the topic.")), - ChatResponse(messages=ChatMessage(role=Role.ASSISTANT, text="Review: Content is clear and well-structured.")), + ChatResponse(messages=ChatMessage("assistant", ["Here's the draft content about the topic."])), + ChatResponse(messages=ChatMessage("assistant", ["Review: Content is clear and well-structured."])), ] writer = ChatAgent( @@ -434,9 +431,9 @@ async def create_concurrent_workflow() -> tuple[AgentFrameworkExecutor, str, Moc """ mock_client = MockBaseChatClient() mock_client.run_responses = [ - ChatResponse(messages=ChatMessage(role=Role.ASSISTANT, text="Research findings: Key data points identified.")), - ChatResponse(messages=ChatMessage(role=Role.ASSISTANT, text="Analysis: Trends indicate positive growth.")), - ChatResponse(messages=ChatMessage(role=Role.ASSISTANT, text="Summary: Overall outlook is favorable.")), + ChatResponse(messages=ChatMessage("assistant", ["Research findings: Key data points identified."])), + ChatResponse(messages=ChatMessage("assistant", ["Analysis: Trends indicate positive growth."])), + ChatResponse(messages=ChatMessage("assistant", ["Summary: Overall outlook is favorable."])), ] researcher = ChatAgent( diff --git a/python/packages/devui/tests/test_mapper.py b/python/packages/devui/tests/test_mapper.py index 4ea3ba9333..70bf44b773 100644 --- a/python/packages/devui/tests/test_mapper.py +++ b/python/packages/devui/tests/test_mapper.py @@ -15,7 +15,6 @@ import pytest from agent_framework._types import ( AgentResponseUpdate, Content, - Role, ) # Import real workflow event classes - NOT mocks! @@ -84,7 +83,7 @@ def create_test_content(content_type: str, **kwargs: Any) -> Any: def create_test_agent_update(contents: list[Any]) -> AgentResponseUpdate: """Create test AgentResponseUpdate.""" - return AgentResponseUpdate(contents=contents, role=Role.ASSISTANT, message_id="test_msg", response_id="test_resp") + return AgentResponseUpdate(contents=contents, role="assistant", message_id="test_msg", response_id="test_resp") # ============================================================================= @@ -450,13 +449,13 @@ async def test_magentic_agent_run_update_event_with_agent_delta_metadata( This tests the ACTUAL event format Magentic emits - not a fake MagenticAgentDeltaEvent class. Magentic uses AgentRunUpdateEvent with additional_properties containing magentic_event_type. """ - from agent_framework._types import AgentResponseUpdate, Role + from agent_framework._types import AgentResponseUpdate from agent_framework._workflows._events import AgentRunUpdateEvent # Create the REAL event format that Magentic emits update = AgentResponseUpdate( contents=[Content.from_text(text="Hello from agent")], - role=Role.ASSISTANT, + role="assistant", author_name="Writer", additional_properties={ "magentic_event_type": "agent_delta", @@ -481,13 +480,13 @@ async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_r Magentic emits orchestrator planning/instruction messages using AgentRunUpdateEvent with additional_properties containing magentic_event_type='orchestrator_message'. """ - from agent_framework._types import AgentResponseUpdate, Role + from agent_framework._types import AgentResponseUpdate from agent_framework._workflows._events import AgentRunUpdateEvent # Create orchestrator message event (REAL format from Magentic) update = AgentResponseUpdate( contents=[Content.from_text(text="Planning: First, the writer will create content...")], - role=Role.ASSISTANT, + role="assistant", author_name="Orchestrator", additional_properties={ "magentic_event_type": "orchestrator_message", @@ -517,21 +516,21 @@ async def test_magentic_events_use_same_event_class_as_other_workflows( additional_properties. Any mapper code checking for 'MagenticAgentDeltaEvent' class names is dead code. """ - from agent_framework._types import AgentResponseUpdate, Role + from agent_framework._types import AgentResponseUpdate from agent_framework._workflows._events import AgentRunUpdateEvent # Create events the way different workflows do it # 1. Regular workflow (no additional_properties) regular_update = AgentResponseUpdate( contents=[Content.from_text(text="Regular workflow response")], - role=Role.ASSISTANT, + role="assistant", ) regular_event = AgentRunUpdateEvent(executor_id="regular_executor", data=regular_update) # 2. Magentic workflow (with additional_properties) magentic_update = AgentResponseUpdate( contents=[Content.from_text(text="Magentic workflow response")], - role=Role.ASSISTANT, + role="assistant", additional_properties={"magentic_event_type": "agent_delta"}, ) magentic_event = AgentRunUpdateEvent(executor_id="magentic_executor", data=magentic_update) @@ -598,13 +597,13 @@ async def test_workflow_output_event(mapper: MessageMapper, test_request: AgentF async def test_workflow_output_event_with_list_data(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None: """Test WorkflowOutputEvent with list data (common for sequential/concurrent workflows).""" - from agent_framework import ChatMessage, Role + from agent_framework import ChatMessage from agent_framework._workflows._events import WorkflowOutputEvent # Sequential/Concurrent workflows often output list[ChatMessage] messages = [ - ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="World")]), + ChatMessage("user", [Content.from_text(text="Hello")]), + ChatMessage("assistant", [Content.from_text(text="World")]), ] event = WorkflowOutputEvent(data=messages, executor_id="complete") events = await mapper.convert_event(event, test_request) diff --git a/python/packages/devui/tests/test_multimodal_workflow.py b/python/packages/devui/tests/test_multimodal_workflow.py index b962fccd7b..dbd4c4dfae 100644 --- a/python/packages/devui/tests/test_multimodal_workflow.py +++ b/python/packages/devui/tests/test_multimodal_workflow.py @@ -49,7 +49,7 @@ class TestMultimodalWorkflowInput: def test_convert_openai_input_to_chat_message_with_image(self): """Test that OpenAI format with image is converted to ChatMessage with DataContent.""" - from agent_framework import ChatMessage, Role + from agent_framework import ChatMessage discovery = MagicMock(spec=EntityDiscovery) mapper = MagicMock(spec=MessageMapper) @@ -72,7 +72,7 @@ class TestMultimodalWorkflowInput: # Verify result is ChatMessage assert isinstance(result, ChatMessage), f"Expected ChatMessage, got {type(result)}" - assert result.role == Role.USER + assert result.role == "user" # Verify contents assert len(result.contents) == 2, f"Expected 2 contents, got {len(result.contents)}" diff --git a/python/packages/durabletask/agent_framework_durabletask/_durable_agent_state.py b/python/packages/durabletask/agent_framework_durabletask/_durable_agent_state.py index a72f3fb07f..aabfa4bf08 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_durable_agent_state.py +++ b/python/packages/durabletask/agent_framework_durabletask/_durable_agent_state.py @@ -797,7 +797,7 @@ class DurableAgentStateMessage: DurableAgentStateMessage with converted content items and metadata """ return DurableAgentStateMessage( - role=request.role.value, + role=request.role, contents=[DurableAgentStateTextContent(text=request.message)], created_at=_parse_created_at(request.created_at) if request.created_at else None, ) @@ -817,7 +817,7 @@ class DurableAgentStateMessage: ] return DurableAgentStateMessage( - role=chat_message.role.value, + role=chat_message.role, contents=contents_list, author_name=chat_message.author_name, extension_data=dict(chat_message.additional_properties) if chat_message.additional_properties else None, diff --git a/python/packages/durabletask/agent_framework_durabletask/_entities.py b/python/packages/durabletask/agent_framework_durabletask/_entities.py index 1f816b6b9d..c842d58fe7 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_entities.py +++ b/python/packages/durabletask/agent_framework_durabletask/_entities.py @@ -14,7 +14,6 @@ from agent_framework import ( AgentResponseUpdate, ChatMessage, Content, - Role, get_logger, ) from durabletask.entities import DurableEntity @@ -176,7 +175,7 @@ class AgentEntity: logger.exception("[AgentEntity.run] Agent execution failed.") error_message = ChatMessage( - role=Role.ASSISTANT, contents=[Content.from_error(message=str(exc), error_code=type(exc).__name__)] + role="assistant", contents=[Content.from_error(message=str(exc), error_code=type(exc).__name__)] ) error_response = AgentResponse(messages=[error_message]) @@ -247,7 +246,7 @@ class AgentEntity: await self._notify_stream_update(update, callback_context) if updates: - response = AgentResponse.from_agent_run_response_updates(updates) + response = AgentResponse.from_updates(updates) else: logger.debug("[AgentEntity] No streaming updates received; creating empty response") response = AgentResponse(messages=[]) diff --git a/python/packages/durabletask/agent_framework_durabletask/_executors.py b/python/packages/durabletask/agent_framework_durabletask/_executors.py index 15bbb4ecb3..226d9dff6c 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_executors.py +++ b/python/packages/durabletask/agent_framework_durabletask/_executors.py @@ -16,7 +16,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timezone from typing import Any, Generic, TypeVar -from agent_framework import AgentResponse, AgentThread, ChatMessage, Content, Role, get_logger +from agent_framework import AgentResponse, AgentThread, ChatMessage, Content, get_logger from durabletask.client import TaskHubGrpcClient from durabletask.entities import EntityInstanceId from durabletask.task import CompletableTask, CompositeTask, OrchestrationContext, Task @@ -180,7 +180,7 @@ class DurableAgentExecutor(ABC, Generic[TaskT]): AgentResponse: Acceptance response with correlation ID """ acceptance_message = ChatMessage( - role=Role.SYSTEM, + role="system", contents=[ Content.from_text( f"Request accepted for processing (correlation_id: {correlation_id}). " @@ -361,7 +361,7 @@ class ClientAgentExecutor(DurableAgentExecutor[AgentResponse]): correlation_id, ) error_message = ChatMessage( - role=Role.SYSTEM, + role="system", contents=[ Content.from_error( message=f"Error processing agent response: {e}", @@ -376,7 +376,7 @@ class ClientAgentExecutor(DurableAgentExecutor[AgentResponse]): correlation_id, ) error_message = ChatMessage( - role=Role.SYSTEM, + role="system", contents=[ Content.from_error( message=f"Timeout waiting for agent response after {self.max_poll_retries} attempts", diff --git a/python/packages/durabletask/agent_framework_durabletask/_models.py b/python/packages/durabletask/agent_framework_durabletask/_models.py index 971aad8e54..3d20828fc7 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_models.py +++ b/python/packages/durabletask/agent_framework_durabletask/_models.py @@ -16,7 +16,7 @@ from datetime import datetime, timezone from importlib import import_module from typing import TYPE_CHECKING, Any, cast -from agent_framework import AgentThread, Role +from agent_framework import AgentThread from ._constants import REQUEST_RESPONSE_FORMAT_TEXT @@ -115,7 +115,7 @@ class RunRequest: message: str request_response_format: str correlation_id: str - role: Role = Role.USER + role: str = "user" response_format: type[BaseModel] | None = None enable_tool_calls: bool = True wait_for_response: bool = True @@ -128,7 +128,7 @@ class RunRequest: message: str, correlation_id: str, request_response_format: str = REQUEST_RESPONSE_FORMAT_TEXT, - role: Role | str | None = Role.USER, + role: str | None = "user", response_format: type[BaseModel] | None = None, enable_tool_calls: bool = True, wait_for_response: bool = True, @@ -148,16 +148,14 @@ class RunRequest: self.options = options if options is not None else {} @staticmethod - def coerce_role(value: Role | str | None) -> Role: - """Normalize various role representations into a Role instance.""" - if isinstance(value, Role): - return value + def coerce_role(value: str | None) -> str: + """Normalize various role representations into a role string.""" if isinstance(value, str): normalized = value.strip() if not normalized: - return Role.USER - return Role(value=normalized.lower()) - return Role.USER + return "user" + return normalized.lower() + return "user" def to_dict(self) -> dict[str, Any]: """Convert to dictionary for JSON serialization.""" @@ -165,7 +163,7 @@ class RunRequest: "message": self.message, "enable_tool_calls": self.enable_tool_calls, "wait_for_response": self.wait_for_response, - "role": self.role.value, + "role": self.role, "request_response_format": self.request_response_format, "correlationId": self.correlation_id, "options": self.options, diff --git a/python/packages/durabletask/agent_framework_durabletask/_response_utils.py b/python/packages/durabletask/agent_framework_durabletask/_response_utils.py index fd622d9b35..1085d4b51d 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_response_utils.py +++ b/python/packages/durabletask/agent_framework_durabletask/_response_utils.py @@ -55,9 +55,12 @@ def ensure_response_format( Raises: ValueError: If response_format is specified but response.value cannot be parsed """ - if response_format is not None and not isinstance(response.value, response_format): - response.try_parse_value(response_format) + if response_format is not None: + # Set the response format on the response so .value knows how to parse + response._response_format = response_format + response._value_parsed = False # Reset to allow re-parsing with new format + # Access response.value to trigger parsing (may raise ValidationError) # Validate that parsing succeeded if not isinstance(response.value, response_format): raise ValueError( diff --git a/python/packages/durabletask/tests/test_durable_entities.py b/python/packages/durabletask/tests/test_durable_entities.py index 35babc44c0..acebcd8492 100644 --- a/python/packages/durabletask/tests/test_durable_entities.py +++ b/python/packages/durabletask/tests/test_durable_entities.py @@ -11,7 +11,7 @@ from typing import Any, TypeVar from unittest.mock import AsyncMock, Mock import pytest -from agent_framework import AgentResponse, AgentResponseUpdate, ChatMessage, Content, Role +from agent_framework import AgentResponse, AgentResponseUpdate, ChatMessage, Content from pydantic import BaseModel from agent_framework_durabletask import ( @@ -81,9 +81,7 @@ def _role_value(chat_message: DurableAgentStateMessage) -> str: def _agent_response(text: str | None) -> AgentResponse: """Create an AgentResponse with a single assistant message.""" - message = ( - ChatMessage(role="assistant", text=text) if text is not None else ChatMessage(role="assistant", contents=[]) - ) + message = ChatMessage("assistant", [text]) if text is not None else ChatMessage("assistant", []) return AgentResponse(messages=[message]) @@ -222,8 +220,8 @@ class TestAgentEntityRunAgent: async def test_run_agent_streaming_callbacks_invoked(self) -> None: """Ensure streaming updates trigger callbacks and run() is not used.""" updates = [ - AgentResponseUpdate(text="Hello"), - AgentResponseUpdate(text=" world"), + AgentResponseUpdate(contents=[Content.from_text(text="Hello")]), + AgentResponseUpdate(contents=[Content.from_text(text=" world")]), ] async def update_generator() -> AsyncIterator[AgentResponseUpdate]: @@ -595,7 +593,7 @@ class TestRunRequestSupport: request = RunRequest( message="Test message", - role=Role.USER, + role="user", enable_tool_calls=True, correlation_id="corr-runreq-1", ) @@ -644,7 +642,7 @@ class TestRunRequestSupport: # Send as system role request = RunRequest( message="System message", - role=Role.SYSTEM, + role="system", correlation_id="corr-runreq-3", ) diff --git a/python/packages/durabletask/tests/test_executors.py b/python/packages/durabletask/tests/test_executors.py index 46fe8bbdbc..802007541f 100644 --- a/python/packages/durabletask/tests/test_executors.py +++ b/python/packages/durabletask/tests/test_executors.py @@ -11,7 +11,7 @@ from typing import Any from unittest.mock import Mock import pytest -from agent_framework import AgentResponse, Role +from agent_framework import AgentResponse from durabletask.entities import EntityInstanceId from durabletask.task import Task from pydantic import BaseModel @@ -241,7 +241,7 @@ class TestClientAgentExecutorFireAndForget: # Verify it contains an acceptance message assert isinstance(result, AgentResponse) assert len(result.messages) == 1 - assert result.messages[0].role == Role.SYSTEM + assert result.messages[0].role == "system" # Check message contains key information message_text = result.messages[0].text assert "accepted" in message_text.lower() @@ -294,7 +294,7 @@ class TestOrchestrationAgentExecutorFireAndForget: response = result.get_result() assert isinstance(response, AgentResponse) assert len(response.messages) == 1 - assert response.messages[0].role == Role.SYSTEM + assert response.messages[0].role == "system" assert "test-789" in response.messages[0].text def test_orchestration_blocking_mode_calls_call_entity(self, mock_orchestration_context: Mock) -> None: @@ -392,7 +392,7 @@ class TestDurableAgentTask: result = task.get_result() assert isinstance(result, AgentResponse) assert len(result.messages) == 1 - assert result.messages[0].role == Role.ASSISTANT + assert result.messages[0].role == "assistant" def test_durable_agent_task_propagates_failure(self, configure_failed_entity_task: Any) -> None: """Verify DurableAgentTask propagates task failures.""" @@ -519,8 +519,8 @@ class TestDurableAgentTask: result = task.get_result() assert isinstance(result, AgentResponse) assert len(result.messages) == 2 - assert result.messages[0].role == Role.ASSISTANT - assert result.messages[1].role == Role.ASSISTANT + assert result.messages[0].role == "assistant" + assert result.messages[1].role == "assistant" def test_durable_agent_task_is_not_complete_initially(self, mock_entity_task: Mock) -> None: """Verify DurableAgentTask is not complete when first created.""" diff --git a/python/packages/durabletask/tests/test_models.py b/python/packages/durabletask/tests/test_models.py index 0f6a24293d..40097fd43d 100644 --- a/python/packages/durabletask/tests/test_models.py +++ b/python/packages/durabletask/tests/test_models.py @@ -3,7 +3,6 @@ """Unit tests for data models (RunRequest).""" import pytest -from agent_framework import Role from pydantic import BaseModel from agent_framework_durabletask._models import RunRequest @@ -22,7 +21,7 @@ class TestRunRequest: assert request.message == "Hello" assert request.correlation_id == "corr-001" - assert request.role == Role.USER + assert request.role == "user" assert request.response_format is None assert request.enable_tool_calls is True assert request.wait_for_response is True @@ -33,7 +32,7 @@ class TestRunRequest: request = RunRequest( message="Hello", correlation_id="corr-002", - role=Role.SYSTEM, + role="system", response_format=schema, enable_tool_calls=False, wait_for_response=False, @@ -41,7 +40,7 @@ class TestRunRequest: assert request.message == "Hello" assert request.correlation_id == "corr-002" - assert request.role == Role.SYSTEM + assert request.role == "system" assert request.response_format is schema assert request.enable_tool_calls is False assert request.wait_for_response is False @@ -50,7 +49,7 @@ class TestRunRequest: """Ensure string role values are coerced into Role instances.""" request = RunRequest(message="Hello", correlation_id="corr-003", role="system") # type: ignore[arg-type] - assert request.role == Role.SYSTEM + assert request.role == "system" def test_to_dict_with_defaults(self) -> None: """Test to_dict with default values.""" @@ -71,7 +70,7 @@ class TestRunRequest: request = RunRequest( message="Hello", correlation_id="corr-005", - role=Role.ASSISTANT, + role="assistant", response_format=schema, enable_tool_calls=False, wait_for_response=False, @@ -95,7 +94,7 @@ class TestRunRequest: assert request.message == "Hello" assert request.correlation_id == "corr-006" - assert request.role == Role.USER + assert request.role == "user" assert request.enable_tool_calls is True assert request.wait_for_response is True @@ -122,7 +121,7 @@ class TestRunRequest: assert request.message == "Test" assert request.correlation_id == "corr-008" - assert request.role == Role.SYSTEM + assert request.role == "system" assert request.response_format is ModuleStructuredResponse assert request.enable_tool_calls is False @@ -131,8 +130,8 @@ class TestRunRequest: data = {"message": "Test", "correlationId": "corr-009", "role": "reviewer"} request = RunRequest.from_dict(data) - assert request.role.value == "reviewer" - assert request.role != Role.USER + assert request.role == "reviewer" + assert request.role != "user" def test_from_dict_empty_message(self) -> None: """Test from_dict with empty message.""" @@ -140,7 +139,7 @@ class TestRunRequest: assert request.message == "" assert request.correlation_id == "corr-010" - assert request.role == Role.USER + assert request.role == "user" def test_from_dict_missing_correlation_id_raises(self) -> None: """Test from_dict raises when correlationId is missing.""" @@ -152,7 +151,7 @@ class TestRunRequest: original = RunRequest( message="Test message", correlation_id="corr-011", - role=Role.SYSTEM, + role="system", response_format=ModuleStructuredResponse, enable_tool_calls=False, ) @@ -232,7 +231,7 @@ class TestRunRequest: """Test round-trip to_dict and from_dict with correlationId.""" original = RunRequest( message="Test message", - role=Role.SYSTEM, + role="system", correlation_id="corr-124", ) @@ -292,7 +291,7 @@ class TestRunRequest: """Test round-trip to_dict and from_dict with orchestration_id.""" original = RunRequest( message="Test message", - role=Role.SYSTEM, + role="system", correlation_id="corr-129", orchestration_id="orch-123", ) diff --git a/python/packages/durabletask/tests/test_shim.py b/python/packages/durabletask/tests/test_shim.py index 26988edca4..d1b0cf2cab 100644 --- a/python/packages/durabletask/tests/test_shim.py +++ b/python/packages/durabletask/tests/test_shim.py @@ -77,7 +77,7 @@ class TestDurableAIAgentMessageNormalization: def test_run_accepts_chat_message(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None: """Verify run accepts and normalizes ChatMessage objects.""" - chat_msg = ChatMessage(role="user", text="Test message") + chat_msg = ChatMessage("user", ["Test message"]) test_agent.run(chat_msg) mock_executor.run_durable_agent.assert_called_once() @@ -95,8 +95,8 @@ class TestDurableAIAgentMessageNormalization: def test_run_accepts_list_of_chat_messages(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None: """Verify run accepts and joins list of ChatMessage objects.""" messages = [ - ChatMessage(role="user", text="Message 1"), - ChatMessage(role="assistant", text="Message 2"), + ChatMessage("user", ["Message 1"]), + ChatMessage("assistant", ["Message 2"]), ] test_agent.run(messages) diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 90655ae055..778a340039 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -16,7 +16,6 @@ from agent_framework import ( ChatMessage, Content, ContextProvider, - Role, normalize_messages, ) from agent_framework._tools import FunctionTool, ToolProtocol @@ -330,7 +329,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): if response_event.data.content: response_messages.append( ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(response_event.data.content)], message_id=message_id, raw_representation=response_event, @@ -385,7 +384,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]): if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: if event.data.delta_content: update = AgentResponseUpdate( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(event.data.delta_content)], response_id=event.data.message_id, message_id=event.data.message_id, diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index e68b58c243..37707465cb 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -13,7 +13,6 @@ from agent_framework import ( AgentThread, ChatMessage, Content, - Role, ) from agent_framework.exceptions import ServiceException from copilot.generated.session_events import Data, SessionEvent, SessionEventType @@ -282,7 +281,7 @@ class TestGitHubCopilotAgentRun: assert isinstance(response, AgentResponse) assert len(response.messages) == 1 - assert response.messages[0].role == Role.ASSISTANT + assert response.messages[0].role == "assistant" assert response.messages[0].contents[0].text == "Test response" async def test_run_chat_message( @@ -295,7 +294,7 @@ class TestGitHubCopilotAgentRun: mock_session.send_and_wait.return_value = assistant_message_event agent = GitHubCopilotAgent(client=mock_client) - chat_message = ChatMessage(role=Role.USER, contents=[Content.from_text("Hello")]) + chat_message = ChatMessage("user", [Content.from_text("Hello")]) response = await agent.run(chat_message) assert isinstance(response, AgentResponse) @@ -390,7 +389,7 @@ class TestGitHubCopilotAgentRunStream: assert len(responses) == 1 assert isinstance(responses[0], AgentResponseUpdate) - assert responses[0].role == Role.ASSISTANT + assert responses[0].role == "assistant" assert responses[0].contents[0].text == "Hello" async def test_run_stream_with_thread( diff --git a/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py b/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py index 2da621a21a..4fd5e21fb7 100644 --- a/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py +++ b/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework._types import ChatMessage, Content, Role +from agent_framework._types import ChatMessage, Content from loguru import logger @@ -18,25 +18,25 @@ def flip_messages(messages: list[ChatMessage]) -> list[ChatMessage]: flipped_messages = [] for msg in messages: - if msg.role == Role.ASSISTANT: + if msg.role == "assistant": # Flip assistant to user contents = filter_out_function_calls(msg.contents) if contents: flipped_msg = ChatMessage( - role=Role.USER, + role="user", # The function calls will cause 400 when role is user contents=contents, author_name=msg.author_name, message_id=msg.message_id, ) flipped_messages.append(flipped_msg) - elif msg.role == Role.USER: + elif msg.role == "user": # Flip user to assistant flipped_msg = ChatMessage( - role=Role.ASSISTANT, contents=msg.contents, author_name=msg.author_name, message_id=msg.message_id + role="assistant", contents=msg.contents, author_name=msg.author_name, message_id=msg.message_id ) flipped_messages.append(flipped_msg) - elif msg.role == Role.TOOL: + elif msg.role == "tool": # Skip tool messages pass else: @@ -59,16 +59,16 @@ def log_messages(messages: list[ChatMessage]) -> None: if hasattr(content, "type"): if content.type == "text": escape_text = content.text.replace("<", r"\<") # type: ignore[union-attr] - if msg.role == Role.SYSTEM: + if msg.role == "system": logger_.info(f"[SYSTEM] {escape_text}") - elif msg.role == Role.USER: + elif msg.role == "user": logger_.info(f"[USER] {escape_text}") - elif msg.role == Role.ASSISTANT: + elif msg.role == "assistant": logger_.info(f"[ASSISTANT] {escape_text}") - elif msg.role == Role.TOOL: + elif msg.role == "tool": logger_.info(f"[TOOL] {escape_text}") else: - logger_.info(f"[{msg.role.value.upper()}] {escape_text}") + logger_.info(f"[{msg.role.upper()}] {escape_text}") elif content.type == "function_call": function_call_text = f"{content.name}({content.arguments})" function_call_text = function_call_text.replace("<", r"\<") @@ -79,34 +79,34 @@ def log_messages(messages: list[ChatMessage]) -> None: logger_.info(f"[TOOL_RESULT] 🔨 {function_result_text}") else: content_text = str(content).replace("<", r"\<") - logger_.info(f"[{msg.role.value.upper()}] ({content.type}) {content_text}") + logger_.info(f"[{msg.role.upper()}] ({content.type}) {content_text}") else: # Fallback for content without type text_content = str(content).replace("<", r"\<") - if msg.role == Role.SYSTEM: + if msg.role == "system": logger_.info(f"[SYSTEM] {text_content}") - elif msg.role == Role.USER: + elif msg.role == "user": logger_.info(f"[USER] {text_content}") - elif msg.role == Role.ASSISTANT: + elif msg.role == "assistant": logger_.info(f"[ASSISTANT] {text_content}") - elif msg.role == Role.TOOL: + elif msg.role == "tool": logger_.info(f"[TOOL] {text_content}") else: - logger_.info(f"[{msg.role.value.upper()}] {text_content}") + logger_.info(f"[{msg.role.upper()}] {text_content}") elif hasattr(msg, "text") and msg.text: # Handle simple text messages text_content = msg.text.replace("<", r"\<") - if msg.role == Role.SYSTEM: + if msg.role == "system": logger_.info(f"[SYSTEM] {text_content}") - elif msg.role == Role.USER: + elif msg.role == "user": logger_.info(f"[USER] {text_content}") - elif msg.role == Role.ASSISTANT: + elif msg.role == "assistant": logger_.info(f"[ASSISTANT] {text_content}") - elif msg.role == Role.TOOL: + elif msg.role == "tool": logger_.info(f"[TOOL] {text_content}") else: - logger_.info(f"[{msg.role.value.upper()}] {text_content}") + logger_.info(f"[{msg.role.upper()}] {text_content}") else: # Fallback for other message formats text_content = str(msg).replace("<", r"\<") - logger_.info(f"[{msg.role.value.upper()}] {text_content}") + logger_.info(f"[{msg.role.upper()}] {text_content}") diff --git a/python/packages/lab/tau2/agent_framework_lab_tau2/_sliding_window.py b/python/packages/lab/tau2/agent_framework_lab_tau2/_sliding_window.py index e34d9f48a4..cec984272f 100644 --- a/python/packages/lab/tau2/agent_framework_lab_tau2/_sliding_window.py +++ b/python/packages/lab/tau2/agent_framework_lab_tau2/_sliding_window.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from typing import Any import tiktoken -from agent_framework import ChatMessage, ChatMessageStore, Role +from agent_framework import ChatMessage, ChatMessageStore from loguru import logger @@ -51,7 +51,7 @@ class SlidingWindowChatMessageStore(ChatMessageStore): logger.warning("Messages exceed max tokens. Truncating oldest message.") self.truncated_messages.pop(0) # Remove leading tool messages - while len(self.truncated_messages) > 0 and self.truncated_messages[0].role == Role.TOOL: + while len(self.truncated_messages) > 0 and self.truncated_messages[0].role == "tool": logger.warning("Removing leading tool message because tool result cannot be the first message.") self.truncated_messages.pop(0) diff --git a/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py b/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py index dddf7088b5..0e63f4085e 100644 --- a/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py +++ b/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py @@ -12,7 +12,6 @@ from agent_framework import ( ChatClientProtocol, ChatMessage, FunctionExecutor, - Role, Workflow, WorkflowBuilder, WorkflowContext, @@ -339,11 +338,11 @@ class TaskRunner: # Matches tau2's expected conversation start pattern logger.info(f"Starting workflow with hardcoded greeting: '{DEFAULT_FIRST_AGENT_MESSAGE}'") - first_message = ChatMessage(Role.ASSISTANT, text=DEFAULT_FIRST_AGENT_MESSAGE) + first_message = ChatMessage("assistant", text=DEFAULT_FIRST_AGENT_MESSAGE) initial_greeting = AgentExecutorResponse( executor_id=ASSISTANT_AGENT_ID, agent_response=AgentResponse(messages=[first_message]), - full_conversation=[ChatMessage(Role.ASSISTANT, text=DEFAULT_FIRST_AGENT_MESSAGE)], + full_conversation=[ChatMessage("assistant", text=DEFAULT_FIRST_AGENT_MESSAGE)], ) # STEP 4: Execute the workflow and collect results diff --git a/python/packages/lab/tau2/tests/test_message_utils.py b/python/packages/lab/tau2/tests/test_message_utils.py index 255f0a96ae..33b705db3a 100644 --- a/python/packages/lab/tau2/tests/test_message_utils.py +++ b/python/packages/lab/tau2/tests/test_message_utils.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from agent_framework._types import ChatMessage, Content, Role +from agent_framework._types import ChatMessage, Content from agent_framework_lab_tau2._message_utils import flip_messages, log_messages @@ -10,7 +10,7 @@ def test_flip_messages_user_to_assistant(): """Test flipping user message to assistant.""" messages = [ ChatMessage( - role=Role.USER, + role="user", contents=[Content.from_text(text="Hello assistant")], author_name="User1", message_id="msg_001", @@ -20,7 +20,7 @@ def test_flip_messages_user_to_assistant(): flipped = flip_messages(messages) assert len(flipped) == 1 - assert flipped[0].role == Role.ASSISTANT + assert flipped[0].role == "assistant" assert flipped[0].text == "Hello assistant" assert flipped[0].author_name == "User1" assert flipped[0].message_id == "msg_001" @@ -30,7 +30,7 @@ def test_flip_messages_assistant_to_user(): """Test flipping assistant message to user.""" messages = [ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(text="Hello user")], author_name="Assistant1", message_id="msg_002", @@ -40,7 +40,7 @@ def test_flip_messages_assistant_to_user(): flipped = flip_messages(messages) assert len(flipped) == 1 - assert flipped[0].role == Role.USER + assert flipped[0].role == "user" assert flipped[0].text == "Hello user" assert flipped[0].author_name == "Assistant1" assert flipped[0].message_id == "msg_002" @@ -52,7 +52,7 @@ def test_flip_messages_assistant_with_function_calls_filtered(): messages = [ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_text(text="I'll call a function"), function_call, @@ -65,7 +65,7 @@ def test_flip_messages_assistant_with_function_calls_filtered(): flipped = flip_messages(messages) assert len(flipped) == 1 - assert flipped[0].role == Role.USER + assert flipped[0].role == "user" # Function call should be filtered out assert len(flipped[0].contents) == 2 assert all(content.type == "text" for content in flipped[0].contents) @@ -78,7 +78,7 @@ def test_flip_messages_assistant_with_only_function_calls_skipped(): function_call = Content.from_function_call(call_id="call_456", name="another_function", arguments={"key": "value"}) messages = [ - ChatMessage(role=Role.ASSISTANT, contents=[function_call], message_id="msg_004") # Only function call, no text + ChatMessage("assistant", [function_call], message_id="msg_004") # Only function call, no text ] flipped = flip_messages(messages) @@ -91,7 +91,7 @@ def test_flip_messages_tool_messages_skipped(): """Test that tool messages are skipped.""" function_result = Content.from_function_result(call_id="call_789", result={"success": True}) - messages = [ChatMessage(role=Role.TOOL, contents=[function_result])] + messages = [ChatMessage("tool", [function_result])] flipped = flip_messages(messages) @@ -101,14 +101,12 @@ def test_flip_messages_tool_messages_skipped(): def test_flip_messages_system_messages_preserved(): """Test that system messages are preserved as-is.""" - messages = [ - ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="System instruction")], message_id="sys_001") - ] + messages = [ChatMessage("system", [Content.from_text(text="System instruction")], message_id="sys_001")] flipped = flip_messages(messages) assert len(flipped) == 1 - assert flipped[0].role == Role.SYSTEM + assert flipped[0].role == "system" assert flipped[0].text == "System instruction" assert flipped[0].message_id == "sys_001" @@ -120,11 +118,11 @@ def test_flip_messages_mixed_conversation(): function_result = Content.from_function_result(call_id="call_mixed", result="function result") messages = [ - ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="System prompt")]), - ChatMessage(role=Role.USER, contents=[Content.from_text(text="User question")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Assistant response"), function_call]), - ChatMessage(role=Role.TOOL, contents=[function_result]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Final response")]), + ChatMessage("system", [Content.from_text(text="System prompt")]), + ChatMessage("user", [Content.from_text(text="User question")]), + ChatMessage("assistant", [Content.from_text(text="Assistant response"), function_call]), + ChatMessage("tool", [function_result]), + ChatMessage("assistant", [Content.from_text(text="Final response")]), ] flipped = flip_messages(messages) @@ -134,18 +132,18 @@ def test_flip_messages_mixed_conversation(): assert len(flipped) == 4 # Check each flipped message - assert flipped[0].role == Role.SYSTEM + assert flipped[0].role == "system" assert flipped[0].text == "System prompt" - assert flipped[1].role == Role.ASSISTANT + assert flipped[1].role == "assistant" assert flipped[1].text == "User question" - assert flipped[2].role == Role.USER + assert flipped[2].role == "user" assert flipped[2].text == "Assistant response" # Function call filtered out # Tool message skipped - assert flipped[3].role == Role.USER + assert flipped[3].role == "user" assert flipped[3].text == "Final response" @@ -160,7 +158,7 @@ def test_flip_messages_preserves_metadata(): """Test that message metadata is preserved during flipping.""" messages = [ ChatMessage( - role=Role.USER, + role="user", contents=[Content.from_text(text="Test message")], author_name="TestUser", message_id="test_123", @@ -178,8 +176,8 @@ def test_flip_messages_preserves_metadata(): def test_log_messages_text_content(mock_logger): """Test logging messages with text content.""" messages = [ - ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Hi there!")]), + ChatMessage("user", [Content.from_text(text="Hello")]), + ChatMessage("assistant", [Content.from_text(text="Hi there!")]), ] log_messages(messages) @@ -193,7 +191,7 @@ def test_log_messages_function_call(mock_logger): """Test logging messages with function calls.""" function_call = Content.from_function_call(call_id="call_log", name="log_function", arguments={"param": "value"}) - messages = [ChatMessage(role=Role.ASSISTANT, contents=[function_call])] + messages = [ChatMessage("assistant", [function_call])] log_messages(messages) @@ -209,7 +207,7 @@ def test_log_messages_function_result(mock_logger): """Test logging messages with function results.""" function_result = Content.from_function_result(call_id="call_result", result="success") - messages = [ChatMessage(role=Role.TOOL, contents=[function_result])] + messages = [ChatMessage("tool", [function_result])] log_messages(messages) @@ -223,10 +221,10 @@ def test_log_messages_function_result(mock_logger): def test_log_messages_different_roles(mock_logger): """Test logging messages with different roles get different colors.""" messages = [ - ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="System")]), - ChatMessage(role=Role.USER, contents=[Content.from_text(text="User")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Assistant")]), - ChatMessage(role=Role.TOOL, contents=[Content.from_text(text="Tool")]), + ChatMessage("system", [Content.from_text(text="System")]), + ChatMessage("user", [Content.from_text(text="User")]), + ChatMessage("assistant", [Content.from_text(text="Assistant")]), + ChatMessage("tool", [Content.from_text(text="Tool")]), ] log_messages(messages) @@ -250,7 +248,7 @@ def test_log_messages_different_roles(mock_logger): @patch("agent_framework_lab_tau2._message_utils.logger") def test_log_messages_escapes_html(mock_logger): """Test that HTML-like characters are properly escaped in log output.""" - messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Message with content")])] + messages = [ChatMessage("user", [Content.from_text(text="Message with content")])] log_messages(messages) @@ -267,7 +265,7 @@ def test_log_messages_mixed_content_types(mock_logger): messages = [ ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(text="I'll call a function"), function_call, Content.from_text(text="Done!")], ) ] diff --git a/python/packages/lab/tau2/tests/test_sliding_window.py b/python/packages/lab/tau2/tests/test_sliding_window.py index 030b57a750..971a391882 100644 --- a/python/packages/lab/tau2/tests/test_sliding_window.py +++ b/python/packages/lab/tau2/tests/test_sliding_window.py @@ -4,7 +4,7 @@ from unittest.mock import patch -from agent_framework._types import ChatMessage, Content, Role +from agent_framework._types import ChatMessage, Content from agent_framework_lab_tau2._sliding_window import SlidingWindowChatMessageStore @@ -36,8 +36,8 @@ def test_initialization_with_parameters(): def test_initialization_with_messages(): """Test initializing with existing messages.""" messages = [ - ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Hi there!")]), + ChatMessage("user", [Content.from_text(text="Hello")]), + ChatMessage("assistant", [Content.from_text(text="Hi there!")]), ] sliding_window = SlidingWindowChatMessageStore(messages=messages, max_tokens=1000) @@ -51,8 +51,8 @@ async def test_add_messages_simple(): sliding_window = SlidingWindowChatMessageStore(max_tokens=10000) # Large limit new_messages = [ - ChatMessage(role=Role.USER, contents=[Content.from_text(text="What's the weather?")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="I can help with that.")]), + ChatMessage("user", [Content.from_text(text="What's the weather?")]), + ChatMessage("assistant", [Content.from_text(text="I can help with that.")]), ] await sliding_window.add_messages(new_messages) @@ -68,10 +68,7 @@ async def test_list_all_messages_vs_list_messages(): sliding_window = SlidingWindowChatMessageStore(max_tokens=50) # Small limit to force truncation # Add many messages to trigger truncation - messages = [ - ChatMessage(role=Role.USER, contents=[Content.from_text(text=f"Message {i} with some content")]) - for i in range(10) - ] + messages = [ChatMessage("user", [Content.from_text(text=f"Message {i} with some content")]) for i in range(10)] await sliding_window.add_messages(messages) @@ -88,7 +85,7 @@ async def test_list_all_messages_vs_list_messages(): def test_get_token_count_basic(): """Test basic token counting.""" sliding_window = SlidingWindowChatMessageStore(max_tokens=1000) - sliding_window.truncated_messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])] + sliding_window.truncated_messages = [ChatMessage("user", [Content.from_text(text="Hello")])] token_count = sliding_window.get_token_count() @@ -105,7 +102,7 @@ def test_get_token_count_with_system_message(): token_count_empty = sliding_window.get_token_count() # Add a message - sliding_window.truncated_messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])] + sliding_window.truncated_messages = [ChatMessage("user", [Content.from_text(text="Hello")])] token_count_with_message = sliding_window.get_token_count() # With message should be more tokens @@ -118,7 +115,7 @@ def test_get_token_count_function_call(): function_call = Content.from_function_call(call_id="call_123", name="test_function", arguments={"param": "value"}) sliding_window = SlidingWindowChatMessageStore(max_tokens=1000) - sliding_window.truncated_messages = [ChatMessage(role=Role.ASSISTANT, contents=[function_call])] + sliding_window.truncated_messages = [ChatMessage("assistant", [function_call])] token_count = sliding_window.get_token_count() assert token_count > 0 @@ -129,7 +126,7 @@ def test_get_token_count_function_result(): function_result = Content.from_function_result(call_id="call_123", result={"success": True, "data": "result"}) sliding_window = SlidingWindowChatMessageStore(max_tokens=1000) - sliding_window.truncated_messages = [ChatMessage(role=Role.TOOL, contents=[function_result])] + sliding_window.truncated_messages = [ChatMessage("tool", [function_result])] token_count = sliding_window.get_token_count() assert token_count > 0 @@ -143,16 +140,16 @@ def test_truncate_messages_removes_old_messages(mock_logger): # Create messages that will exceed the limit messages = [ ChatMessage( - role=Role.USER, + role="user", contents=[Content.from_text(text="This is a very long message that should exceed the token limit")], ), ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_text(text="This is another very long message that should also exceed the token limit") ], ), - ChatMessage(role=Role.USER, contents=[Content.from_text(text="Short msg")]), + ChatMessage("user", [Content.from_text(text="Short msg")]), ] sliding_window.truncated_messages = messages.copy() @@ -172,16 +169,16 @@ def test_truncate_messages_removes_leading_tool_messages(mock_logger): # Create messages starting with tool message tool_message = ChatMessage( - role=Role.TOOL, contents=[Content.from_function_result(call_id="call_123", result="result")] + role="tool", contents=[Content.from_function_result(call_id="call_123", result="result")] ) - user_message = ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")]) + user_message = ChatMessage("user", [Content.from_text(text="Hello")]) sliding_window.truncated_messages = [tool_message, user_message] sliding_window.truncate_messages() # Tool message should be removed from the beginning assert len(sliding_window.truncated_messages) == 1 - assert sliding_window.truncated_messages[0].role == Role.USER + assert sliding_window.truncated_messages[0].role == "user" # Should have logged warning about removing tool message mock_logger.warning.assert_called() @@ -232,14 +229,14 @@ async def test_real_world_scenario(): # Simulate a conversation conversation = [ - ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello, how are you?")]), + ChatMessage("user", [Content.from_text(text="Hello, how are you?")]), ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(text="I'm doing well, thank you! How can I help you today?")], ), - ChatMessage(role=Role.USER, contents=[Content.from_text(text="Can you tell me about the weather?")]), + ChatMessage("user", [Content.from_text(text="Can you tell me about the weather?")]), ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_text( text="I'd be happy to help with weather information, " @@ -247,9 +244,9 @@ async def test_real_world_scenario(): ) ], ), - ChatMessage(role=Role.USER, contents=[Content.from_text(text="What about telling me a joke instead?")]), + ChatMessage("user", [Content.from_text(text="What about telling me a joke instead?")]), ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[ Content.from_text(text="Sure! Why don't scientists trust atoms? Because they make up everything!") ], diff --git a/python/packages/lab/tau2/tests/test_tau2_utils.py b/python/packages/lab/tau2/tests/test_tau2_utils.py index 8811952bbe..29520bda42 100644 --- a/python/packages/lab/tau2/tests/test_tau2_utils.py +++ b/python/packages/lab/tau2/tests/test_tau2_utils.py @@ -6,7 +6,7 @@ import urllib.request from pathlib import Path import pytest -from agent_framework import ChatMessage, Content, FunctionTool, Role +from agent_framework import ChatMessage, Content, FunctionTool from agent_framework_lab_tau2._tau2_utils import ( convert_agent_framework_messages_to_tau2_messages, convert_tau2_tool_to_function_tool, @@ -91,7 +91,7 @@ def test_convert_tau2_tool_to_function_tool_multiple_tools(tau2_airline_environm def test_convert_agent_framework_messages_to_tau2_messages_system(): """Test converting system message.""" - messages = [ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="System instruction")])] + messages = [ChatMessage("system", [Content.from_text(text="System instruction")])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -103,7 +103,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_system(): def test_convert_agent_framework_messages_to_tau2_messages_user(): """Test converting user message.""" - messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello assistant")])] + messages = [ChatMessage("user", [Content.from_text(text="Hello assistant")])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -116,7 +116,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_user(): def test_convert_agent_framework_messages_to_tau2_messages_assistant(): """Test converting assistant message.""" - messages = [ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello user")])] + messages = [ChatMessage("assistant", [Content.from_text(text="Hello user")])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -131,9 +131,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_with_function_call(): """Test converting message with function call.""" function_call = Content.from_function_call(call_id="call_123", name="test_function", arguments={"param": "value"}) - messages = [ - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="I'll call a function"), function_call]) - ] + messages = [ChatMessage("assistant", [Content.from_text(text="I'll call a function"), function_call])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -155,7 +153,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_with_function_result( """Test converting message with function result.""" function_result = Content.from_function_result(call_id="call_123", result={"success": True, "data": "result data"}) - messages = [ChatMessage(role=Role.TOOL, contents=[function_result])] + messages = [ChatMessage("tool", [function_result])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -175,7 +173,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_with_error(): call_id="call_456", result="Error occurred", exception=Exception("Test error") ) - messages = [ChatMessage(role=Role.TOOL, contents=[function_result])] + messages = [ChatMessage("tool", [function_result])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -186,11 +184,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_with_error(): def test_convert_agent_framework_messages_to_tau2_messages_multiple_text_contents(): """Test converting message with multiple text contents.""" - messages = [ - ChatMessage( - role=Role.USER, contents=[Content.from_text(text="First part"), Content.from_text(text="Second part")] - ) - ] + messages = [ChatMessage("user", [Content.from_text(text="First part"), Content.from_text(text="Second part")])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -206,11 +200,11 @@ def test_convert_agent_framework_messages_to_tau2_messages_complex_scenario(): function_result = Content.from_function_result(call_id="call_789", result={"output": "tool result"}) messages = [ - ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="System prompt")]), - ChatMessage(role=Role.USER, contents=[Content.from_text(text="User request")]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="I'll help you"), function_call]), - ChatMessage(role=Role.TOOL, contents=[function_result]), - ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Based on the result...")]), + ChatMessage("system", [Content.from_text(text="System prompt")]), + ChatMessage("user", [Content.from_text(text="User request")]), + ChatMessage("assistant", [Content.from_text(text="I'll help you"), function_call]), + ChatMessage("tool", [function_result]), + ChatMessage("assistant", [Content.from_text(text="Based on the result...")]), ] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) diff --git a/python/packages/mem0/agent_framework_mem0/_provider.py b/python/packages/mem0/agent_framework_mem0/_provider.py index 8ab9192d1a..ac37cc1a2c 100644 --- a/python/packages/mem0/agent_framework_mem0/_provider.py +++ b/python/packages/mem0/agent_framework_mem0/_provider.py @@ -121,9 +121,9 @@ class Mem0Provider(ContextProvider): messages_list = [*request_messages_list, *response_messages_list] messages: list[dict[str, str]] = [ - {"role": message.role.value, "content": message.text} + {"role": message.role, "content": message.text} for message in messages_list - if message.role.value in {"user", "assistant", "system"} and message.text and message.text.strip() + if message.role in {"user", "assistant", "system"} and message.text and message.text.strip() ] if messages: @@ -176,7 +176,7 @@ class Mem0Provider(ContextProvider): line_separated_memories = "\n".join(memory.get("memory", "") for memory in memories) return Context( - messages=[ChatMessage(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")] + messages=[ChatMessage("user", [f"{self.context_prompt}\n{line_separated_memories}"])] if line_separated_memories else None ) diff --git a/python/packages/mem0/tests/test_mem0_context_provider.py b/python/packages/mem0/tests/test_mem0_context_provider.py index 85779b6ccf..0b39c7b043 100644 --- a/python/packages/mem0/tests/test_mem0_context_provider.py +++ b/python/packages/mem0/tests/test_mem0_context_provider.py @@ -7,7 +7,7 @@ import sys from unittest.mock import AsyncMock, patch import pytest -from agent_framework import ChatMessage, Content, Context, Role +from agent_framework import ChatMessage, Content, Context from agent_framework.exceptions import ServiceInitializationError from agent_framework.mem0 import Mem0Provider @@ -36,9 +36,9 @@ def mock_mem0_client() -> AsyncMock: def sample_messages() -> list[ChatMessage]: """Create sample chat messages for testing.""" return [ - ChatMessage(role=Role.USER, text="Hello, how are you?"), - ChatMessage(role=Role.ASSISTANT, text="I'm doing well, thank you!"), - ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant"), + ChatMessage("user", ["Hello, how are you?"]), + ChatMessage("assistant", ["I'm doing well, thank you!"]), + ChatMessage("system", ["You are a helpful assistant"]), ] @@ -191,7 +191,7 @@ class TestMem0ProviderMessagesAdding: async def test_messages_adding_fails_without_filters(self, mock_mem0_client: AsyncMock) -> None: """Test that invoked fails when no filters are provided.""" provider = Mem0Provider(mem0_client=mock_mem0_client) - message = ChatMessage(role=Role.USER, text="Hello!") + message = ChatMessage("user", ["Hello!"]) with pytest.raises(ServiceInitializationError) as exc_info: await provider.invoked(message) @@ -201,7 +201,7 @@ class TestMem0ProviderMessagesAdding: async def test_messages_adding_single_message(self, mock_mem0_client: AsyncMock) -> None: """Test adding a single message.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) - message = ChatMessage(role=Role.USER, text="Hello!") + message = ChatMessage("user", ["Hello!"]) await provider.invoked(message) @@ -288,9 +288,9 @@ class TestMem0ProviderMessagesAdding: """Test that empty or invalid messages are filtered out.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) messages = [ - ChatMessage(role=Role.USER, text=""), # Empty text - ChatMessage(role=Role.USER, text=" "), # Whitespace only - ChatMessage(role=Role.USER, text="Valid message"), + ChatMessage("user", [""]), # Empty text + ChatMessage("user", [" "]), # Whitespace only + ChatMessage("user", ["Valid message"]), ] await provider.invoked(messages) @@ -303,8 +303,8 @@ class TestMem0ProviderMessagesAdding: """Test that mem0 client is not called when no valid messages exist.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) messages = [ - ChatMessage(role=Role.USER, text=""), - ChatMessage(role=Role.USER, text=" "), + ChatMessage("user", [""]), + ChatMessage("user", [" "]), ] await provider.invoked(messages) @@ -318,7 +318,7 @@ class TestMem0ProviderModelInvoking: async def test_model_invoking_fails_without_filters(self, mock_mem0_client: AsyncMock) -> None: """Test that invoking fails when no filters are provided.""" provider = Mem0Provider(mem0_client=mock_mem0_client) - message = ChatMessage(role=Role.USER, text="What's the weather?") + message = ChatMessage("user", ["What's the weather?"]) with pytest.raises(ServiceInitializationError) as exc_info: await provider.invoking(message) @@ -328,7 +328,7 @@ class TestMem0ProviderModelInvoking: async def test_model_invoking_single_message(self, mock_mem0_client: AsyncMock) -> None: """Test invoking with a single message.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) - message = ChatMessage(role=Role.USER, text="What's the weather?") + message = ChatMessage("user", ["What's the weather?"]) # Mock search results mock_mem0_client.search.return_value = [ @@ -369,7 +369,7 @@ class TestMem0ProviderModelInvoking: async def test_model_invoking_with_agent_id(self, mock_mem0_client: AsyncMock) -> None: """Test invoking with agent_id.""" provider = Mem0Provider(agent_id="agent123", mem0_client=mock_mem0_client) - message = ChatMessage(role=Role.USER, text="Hello") + message = ChatMessage("user", ["Hello"]) mock_mem0_client.search.return_value = [] @@ -387,7 +387,7 @@ class TestMem0ProviderModelInvoking: mem0_client=mock_mem0_client, ) provider._per_operation_thread_id = "operation_thread" - message = ChatMessage(role=Role.USER, text="Hello") + message = ChatMessage("user", ["Hello"]) mock_mem0_client.search.return_value = [] @@ -399,7 +399,7 @@ class TestMem0ProviderModelInvoking: async def test_model_invoking_no_memories_returns_none_instructions(self, mock_mem0_client: AsyncMock) -> None: """Test that no memories returns context with None instructions.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) - message = ChatMessage(role=Role.USER, text="Hello") + message = ChatMessage("user", ["Hello"]) mock_mem0_client.search.return_value = [] @@ -416,7 +416,7 @@ class TestMem0ProviderModelInvoking: provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) function_call = Content.from_function_call(call_id="1", name="test_func", arguments='{"arg1": "value1"}') message = ChatMessage( - role=Role.USER, + role="user", contents=[ Content.from_function_approval_response( id="approval_1", @@ -437,9 +437,9 @@ class TestMem0ProviderModelInvoking: """Test that empty message text is filtered out from query.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) messages = [ - ChatMessage(role=Role.USER, text=""), - ChatMessage(role=Role.USER, text="Valid message"), - ChatMessage(role=Role.USER, text=" "), + ChatMessage("user", [""]), + ChatMessage("user", ["Valid message"]), + ChatMessage("user", [" "]), ] mock_mem0_client.search.return_value = [] @@ -457,7 +457,7 @@ class TestMem0ProviderModelInvoking: context_prompt=custom_prompt, mem0_client=mock_mem0_client, ) - message = ChatMessage(role=Role.USER, text="Hello") + message = ChatMessage("user", ["Hello"]) mock_mem0_client.search.return_value = [{"memory": "Test memory"}] diff --git a/python/packages/ollama/agent_framework_ollama/_chat_client.py b/python/packages/ollama/agent_framework_ollama/_chat_client.py index ead729b8e2..2891ab5bcb 100644 --- a/python/packages/ollama/agent_framework_ollama/_chat_client.py +++ b/python/packages/ollama/agent_framework_ollama/_chat_client.py @@ -21,7 +21,6 @@ from agent_framework import ( ChatResponseUpdate, Content, FunctionTool, - Role, ToolProtocol, UsageDetails, get_logger, @@ -442,12 +441,12 @@ class OllamaChatClient(BaseChatClient[TOllamaChatOptions], Generic[TOllamaChatOp def _prepare_message_for_ollama(self, message: ChatMessage) -> list[OllamaMessage]: message_converters: dict[str, Callable[[ChatMessage], list[OllamaMessage]]] = { - Role.SYSTEM.value: self._format_system_message, - Role.USER.value: self._format_user_message, - Role.ASSISTANT.value: self._format_assistant_message, - Role.TOOL.value: self._format_tool_message, + "system": self._format_system_message, + "user": self._format_user_message, + "assistant": self._format_assistant_message, + "tool": self._format_tool_message, } - return message_converters[message.role.value](message) + return message_converters[message.role](message) def _format_system_message(self, message: ChatMessage) -> list[OllamaMessage]: return [OllamaMessage(role="system", content=message.text)] @@ -516,8 +515,8 @@ class OllamaChatClient(BaseChatClient[TOllamaChatOptions], Generic[TOllamaChatOp contents = self._parse_contents_from_ollama(response) return ChatResponseUpdate( contents=contents, - role=Role.ASSISTANT, - ai_model_id=response.model, + role="assistant", + model_id=response.model, created_at=response.created_at, ) @@ -525,7 +524,7 @@ class OllamaChatClient(BaseChatClient[TOllamaChatOptions], Generic[TOllamaChatOp contents = self._parse_contents_from_ollama(response) return ChatResponse( - messages=[ChatMessage(role=Role.ASSISTANT, contents=contents)], + messages=[ChatMessage("assistant", contents)], model_id=response.model, created_at=response.created_at, usage_details=UsageDetails( diff --git a/python/packages/purview/README.md b/python/packages/purview/README.md index ad3d1867d0..b016f00c8b 100644 --- a/python/packages/purview/README.md +++ b/python/packages/purview/README.md @@ -72,7 +72,7 @@ async def main(): middleware=[purview_middleware] ) - response = await agent.run(ChatMessage(role=Role.USER, text="Summarize zero trust in one sentence.")) + response = await agent.run(ChatMessage("user", ["Summarize zero trust in one sentence."])) print(response) asyncio.run(main()) diff --git a/python/packages/purview/agent_framework_purview/_middleware.py b/python/packages/purview/agent_framework_purview/_middleware.py index 7839f2f968..a0cce1bd55 100644 --- a/python/packages/purview/agent_framework_purview/_middleware.py +++ b/python/packages/purview/agent_framework_purview/_middleware.py @@ -57,10 +57,10 @@ class PurviewPolicyMiddleware(AgentMiddleware): context.messages, Activity.UPLOAD_TEXT ) if should_block_prompt: - from agent_framework import AgentResponse, ChatMessage, Role + from agent_framework import AgentResponse, ChatMessage context.result = AgentResponse( - messages=[ChatMessage(role=Role.SYSTEM, text=self._settings.blocked_prompt_message)] + messages=[ChatMessage("system", [self._settings.blocked_prompt_message])] ) context.terminate = True return @@ -85,10 +85,10 @@ class PurviewPolicyMiddleware(AgentMiddleware): user_id=resolved_user_id, ) if should_block_response: - from agent_framework import AgentResponse, ChatMessage, Role + from agent_framework import AgentResponse, ChatMessage context.result = AgentResponse( - messages=[ChatMessage(role=Role.SYSTEM, text=self._settings.blocked_response_message)] + messages=[ChatMessage("system", [self._settings.blocked_response_message])] ) else: # Streaming responses are not supported for post-checks @@ -149,7 +149,7 @@ class PurviewChatPolicyMiddleware(ChatMiddleware): if should_block_prompt: from agent_framework import ChatMessage, ChatResponse - blocked_message = ChatMessage(role="system", text=self._settings.blocked_prompt_message) + blocked_message = ChatMessage("system", [self._settings.blocked_prompt_message]) context.result = ChatResponse(messages=[blocked_message]) context.terminate = True return @@ -177,7 +177,7 @@ class PurviewChatPolicyMiddleware(ChatMiddleware): if should_block_response: from agent_framework import ChatMessage, ChatResponse - blocked_message = ChatMessage(role="system", text=self._settings.blocked_response_message) + blocked_message = ChatMessage("system", [self._settings.blocked_response_message]) context.result = ChatResponse(messages=[blocked_message]) else: logger.debug("Streaming responses are not supported for Purview policy post-checks") diff --git a/python/packages/purview/tests/test_chat_middleware.py b/python/packages/purview/tests/test_chat_middleware.py index 3f9595e721..763a54ac67 100644 --- a/python/packages/purview/tests/test_chat_middleware.py +++ b/python/packages/purview/tests/test_chat_middleware.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import ChatContext, ChatMessage, Role +from agent_framework import ChatContext, ChatMessage from azure.core.credentials import AccessToken from agent_framework_purview import PurviewChatPolicyMiddleware, PurviewSettings @@ -36,9 +36,7 @@ class TestPurviewChatPolicyMiddleware: chat_client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - return ChatContext( - chat_client=chat_client, messages=[ChatMessage(role=Role.USER, text="Hello")], options=chat_options - ) + return ChatContext(chat_client=chat_client, messages=[ChatMessage("user", ["Hello"])], options=chat_options) async def test_initialization(self, middleware: PurviewChatPolicyMiddleware) -> None: assert middleware._client is not None @@ -56,14 +54,14 @@ class TestPurviewChatPolicyMiddleware: class Result: def __init__(self): - self.messages = [ChatMessage(role=Role.ASSISTANT, text="Hi there")] + self.messages = [ChatMessage("assistant", ["Hi there"])] ctx.result = Result() await middleware.process(chat_context, mock_next) assert next_called assert mock_proc.call_count == 2 - assert chat_context.result.messages[0].role == Role.ASSISTANT + assert chat_context.result.messages[0].role == "assistant" async def test_blocks_prompt(self, middleware: PurviewChatPolicyMiddleware, chat_context: ChatContext) -> None: with patch.object(middleware._processor, "process_messages", return_value=(True, "user-123")): @@ -76,7 +74,7 @@ class TestPurviewChatPolicyMiddleware: assert chat_context.result assert hasattr(chat_context.result, "messages") msg = chat_context.result.messages[0] - assert msg.role in ("system", Role.SYSTEM) + assert msg.role in ("system", "system") assert "blocked" in msg.text.lower() async def test_blocks_response(self, middleware: PurviewChatPolicyMiddleware, chat_context: ChatContext) -> None: @@ -92,7 +90,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: class Result: def __init__(self): - self.messages = [ChatMessage(role=Role.ASSISTANT, text="Sensitive output")] # pragma: no cover + self.messages = [ChatMessage("assistant", ["Sensitive output"])] # pragma: no cover ctx.result = Result() @@ -100,7 +98,7 @@ class TestPurviewChatPolicyMiddleware: assert call_state["count"] == 2 msgs = getattr(chat_context.result, "messages", None) or chat_context.result first_msg = msgs[0] - assert first_msg.role in ("system", Role.SYSTEM) + assert first_msg.role in ("system", "system") assert "blocked" in first_msg.text.lower() async def test_streaming_skips_post_check(self, middleware: PurviewChatPolicyMiddleware) -> None: @@ -109,7 +107,7 @@ class TestPurviewChatPolicyMiddleware: chat_options.model = "test-model" streaming_context = ChatContext( chat_client=chat_client, - messages=[ChatMessage(role=Role.USER, text="Hello")], + messages=[ChatMessage("user", ["Hello"])], options=chat_options, is_streaming=True, ) @@ -141,7 +139,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role=Role.ASSISTANT, text="Response")] + result.messages = [ChatMessage("assistant", ["Response"])] ctx.result = result await middleware.process(chat_context, mock_next) @@ -165,7 +163,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role=Role.ASSISTANT, text="Response")] + result.messages = [ChatMessage("assistant", ["Response"])] ctx.result = result await middleware.process(chat_context, mock_next) @@ -188,9 +186,7 @@ class TestPurviewChatPolicyMiddleware: chat_client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role=Role.USER, text="Hello")], options=chat_options - ) + context = ChatContext(chat_client=chat_client, messages=[ChatMessage("user", ["Hello"])], options=chat_options) async def mock_process_messages(*args, **kwargs): raise PurviewPaymentRequiredError("Payment required") @@ -214,9 +210,7 @@ class TestPurviewChatPolicyMiddleware: chat_client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role=Role.USER, text="Hello")], options=chat_options - ) + context = ChatContext(chat_client=chat_client, messages=[ChatMessage("user", ["Hello"])], options=chat_options) call_count = 0 @@ -231,7 +225,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role=Role.ASSISTANT, text="OK")] + result.messages = [ChatMessage("assistant", ["OK"])] ctx.result = result with pytest.raises(PurviewPaymentRequiredError): @@ -247,9 +241,7 @@ class TestPurviewChatPolicyMiddleware: chat_client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role=Role.USER, text="Hello")], options=chat_options - ) + context = ChatContext(chat_client=chat_client, messages=[ChatMessage("user", ["Hello"])], options=chat_options) async def mock_process_messages(*args, **kwargs): raise PurviewPaymentRequiredError("Payment required") @@ -258,7 +250,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role=Role.ASSISTANT, text="Response")] + result.messages = [ChatMessage("assistant", ["Response"])] context.result = result # Should not raise, just log @@ -289,9 +281,7 @@ class TestPurviewChatPolicyMiddleware: chat_client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role=Role.USER, text="Hello")], options=chat_options - ) + context = ChatContext(chat_client=chat_client, messages=[ChatMessage("user", ["Hello"])], options=chat_options) async def mock_process_messages(*args, **kwargs): raise ValueError("Some error") @@ -300,7 +290,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role=Role.ASSISTANT, text="Response")] + result.messages = [ChatMessage("assistant", ["Response"])] context.result = result # Should not raise, just log @@ -318,9 +308,7 @@ class TestPurviewChatPolicyMiddleware: chat_client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role=Role.USER, text="Hello")], options=chat_options - ) + context = ChatContext(chat_client=chat_client, messages=[ChatMessage("user", ["Hello"])], options=chat_options) with patch.object(middleware._processor, "process_messages", side_effect=ValueError("boom")): @@ -340,9 +328,7 @@ class TestPurviewChatPolicyMiddleware: chat_client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role=Role.USER, text="Hello")], options=chat_options - ) + context = ChatContext(chat_client=chat_client, messages=[ChatMessage("user", ["Hello"])], options=chat_options) call_count = 0 @@ -357,7 +343,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role=Role.ASSISTANT, text="OK")] + result.messages = [ChatMessage("assistant", ["OK"])] ctx.result = result with pytest.raises(ValueError, match="post"): diff --git a/python/packages/purview/tests/test_middleware.py b/python/packages/purview/tests/test_middleware.py index b973e8ea34..32f712b0b9 100644 --- a/python/packages/purview/tests/test_middleware.py +++ b/python/packages/purview/tests/test_middleware.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import AgentResponse, AgentRunContext, ChatMessage, Role +from agent_framework import AgentResponse, AgentRunContext, ChatMessage from azure.core.credentials import AccessToken from agent_framework_purview import PurviewPolicyMiddleware, PurviewSettings @@ -49,7 +49,7 @@ class TestPurviewPolicyMiddleware: self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock ) -> None: """Test middleware allows prompt that passes policy check.""" - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello, how are you?")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Hello, how are you?"])]) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")): next_called = False @@ -57,7 +57,7 @@ class TestPurviewPolicyMiddleware: async def mock_next(ctx: AgentRunContext) -> None: nonlocal next_called next_called = True - ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="I'm good, thanks!")]) + ctx.result = AgentResponse(messages=[ChatMessage("assistant", ["I'm good, thanks!"])]) await middleware.process(context, mock_next) @@ -69,9 +69,7 @@ class TestPurviewPolicyMiddleware: self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock ) -> None: """Test middleware blocks prompt that violates policy.""" - context = AgentRunContext( - agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Sensitive information")] - ) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Sensitive information"])]) with patch.object(middleware._processor, "process_messages", return_value=(True, "user-123")): next_called = False @@ -86,12 +84,12 @@ class TestPurviewPolicyMiddleware: assert context.result is not None assert context.terminate assert len(context.result.messages) == 1 - assert context.result.messages[0].role == Role.SYSTEM + assert context.result.messages[0].role == "system" assert "blocked by policy" in context.result.messages[0].text.lower() async def test_middleware_checks_response(self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock) -> None: """Test middleware checks agent response for policy violations.""" - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Hello"])]) call_count = 0 @@ -104,16 +102,14 @@ class TestPurviewPolicyMiddleware: with patch.object(middleware._processor, "process_messages", side_effect=mock_process_messages): async def mock_next(ctx: AgentRunContext) -> None: - ctx.result = AgentResponse( - messages=[ChatMessage(role=Role.ASSISTANT, text="Here's some sensitive information")] - ) + ctx.result = AgentResponse(messages=[ChatMessage("assistant", ["Here's some sensitive information"])]) await middleware.process(context, mock_next) assert call_count == 2 assert context.result is not None assert len(context.result.messages) == 1 - assert context.result.messages[0].role == Role.SYSTEM + assert context.result.messages[0].role == "system" assert "blocked by policy" in context.result.messages[0].text.lower() async def test_middleware_handles_result_without_messages( @@ -123,7 +119,7 @@ class TestPurviewPolicyMiddleware: # Set ignore_exceptions to True so AttributeError is caught and logged middleware._settings.ignore_exceptions = True - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Hello"])]) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")): @@ -140,12 +136,12 @@ class TestPurviewPolicyMiddleware: """Test middleware passes correct activity type to processor.""" from agent_framework_purview._models import Activity - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Test")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Test"])]) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_process: async def mock_next(ctx: AgentRunContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Response")]) + ctx.result = AgentResponse(messages=[ChatMessage("assistant", ["Response"])]) await middleware.process(context, mock_next) @@ -157,13 +153,13 @@ class TestPurviewPolicyMiddleware: self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock ) -> None: """Test that streaming results skip post-check evaluation.""" - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Hello"])]) context.is_streaming = True with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: async def mock_next(ctx: AgentRunContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="streaming")]) + ctx.result = AgentResponse(messages=[ChatMessage("assistant", ["streaming"])]) await middleware.process(context, mock_next) @@ -175,7 +171,7 @@ class TestPurviewPolicyMiddleware: """Test that 402 in pre-check is raised when ignore_payment_required=False.""" from agent_framework_purview._exceptions import PurviewPaymentRequiredError - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Hello"])]) with patch.object( middleware._processor, @@ -195,7 +191,7 @@ class TestPurviewPolicyMiddleware: """Test that 402 in post-check is raised when ignore_payment_required=False.""" from agent_framework_purview._exceptions import PurviewPaymentRequiredError - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Hello"])]) call_count = 0 @@ -209,7 +205,7 @@ class TestPurviewPolicyMiddleware: with patch.object(middleware._processor, "process_messages", side_effect=side_effect): async def mock_next(ctx: AgentRunContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="OK")]) + ctx.result = AgentResponse(messages=[ChatMessage("assistant", ["OK"])]) with pytest.raises(PurviewPaymentRequiredError): await middleware.process(context, mock_next) @@ -220,7 +216,7 @@ class TestPurviewPolicyMiddleware: """Test that post-check exceptions are propagated when ignore_exceptions=False.""" middleware._settings.ignore_exceptions = False - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Hello")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Hello"])]) call_count = 0 @@ -234,7 +230,7 @@ class TestPurviewPolicyMiddleware: with patch.object(middleware._processor, "process_messages", side_effect=side_effect): async def mock_next(ctx: AgentRunContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="OK")]) + ctx.result = AgentResponse(messages=[ChatMessage("assistant", ["OK"])]) with pytest.raises(ValueError, match="Post-check blew up"): await middleware.process(context, mock_next) @@ -246,14 +242,14 @@ class TestPurviewPolicyMiddleware: # Set ignore_exceptions to True middleware._settings.ignore_exceptions = True - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Test")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Test"])]) with patch.object( middleware._processor, "process_messages", side_effect=Exception("Pre-check error") ) as mock_process: async def mock_next(ctx: AgentRunContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Response")]) + ctx.result = AgentResponse(messages=[ChatMessage("assistant", ["Response"])]) await middleware.process(context, mock_next) @@ -271,7 +267,7 @@ class TestPurviewPolicyMiddleware: # Set ignore_exceptions to True middleware._settings.ignore_exceptions = True - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Test")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Test"])]) call_count = 0 @@ -285,7 +281,7 @@ class TestPurviewPolicyMiddleware: with patch.object(middleware._processor, "process_messages", side_effect=mock_process_messages): async def mock_next(ctx: AgentRunContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Response")]) + ctx.result = AgentResponse(messages=[ChatMessage("assistant", ["Response"])]) await middleware.process(context, mock_next) @@ -302,7 +298,7 @@ class TestPurviewPolicyMiddleware: mock_agent = MagicMock() mock_agent.name = "test-agent" - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Test")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Test"])]) # Mock processor to raise an exception async def mock_process_messages(*args, **kwargs): @@ -311,7 +307,7 @@ class TestPurviewPolicyMiddleware: with patch.object(middleware._processor, "process_messages", side_effect=mock_process_messages): async def mock_next(ctx): - ctx.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="Response")]) + ctx.result = AgentResponse(messages=[ChatMessage("assistant", ["Response"])]) # Should not raise, just log await middleware.process(context, mock_next) @@ -326,7 +322,7 @@ class TestPurviewPolicyMiddleware: mock_agent = MagicMock() mock_agent.name = "test-agent" - context = AgentRunContext(agent=mock_agent, messages=[ChatMessage(role=Role.USER, text="Test")]) + context = AgentRunContext(agent=mock_agent, messages=[ChatMessage("user", ["Test"])]) # Mock processor to raise an exception async def mock_process_messages(*args, **kwargs): diff --git a/python/packages/purview/tests/test_processor.py b/python/packages/purview/tests/test_processor.py index 11f48ed199..3dfd78d981 100644 --- a/python/packages/purview/tests/test_processor.py +++ b/python/packages/purview/tests/test_processor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import ChatMessage, Role +from agent_framework import ChatMessage from agent_framework_purview import PurviewAppLocation, PurviewLocationType, PurviewSettings from agent_framework_purview._models import ( @@ -83,8 +83,8 @@ class TestScopedContentProcessor: async def test_process_messages_with_defaults(self, processor: ScopedContentProcessor) -> None: """Test process_messages with settings that have defaults.""" messages = [ - ChatMessage(role=Role.USER, text="Hello"), - ChatMessage(role=Role.ASSISTANT, text="Hi there"), + ChatMessage("user", ["Hello"]), + ChatMessage("assistant", ["Hi there"]), ] with patch.object(processor, "_map_messages", return_value=([], None)) as mock_map: @@ -98,7 +98,7 @@ class TestScopedContentProcessor: self, processor: ScopedContentProcessor, process_content_request_factory ) -> None: """Test process_messages returns True when content should be blocked.""" - messages = [ChatMessage(role=Role.USER, text="Sensitive content")] + messages = [ChatMessage("user", ["Sensitive content"])] mock_request = process_content_request_factory("Sensitive content") @@ -121,7 +121,7 @@ class TestScopedContentProcessor: """Test _map_messages creates ProcessContentRequest objects.""" messages = [ ChatMessage( - role=Role.USER, + role="user", text="Test message", message_id="msg-123", author_name="12345678-1234-1234-1234-123456789012", @@ -139,7 +139,7 @@ class TestScopedContentProcessor: """Test _map_messages gets token info when settings lack some defaults.""" settings = PurviewSettings(app_name="Test App", tenant_id="12345678-1234-1234-1234-123456789012") processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role=Role.USER, text="Test", message_id="msg-123")] + messages = [ChatMessage("user", ["Test"], message_id="msg-123")] requests, user_id = await processor._map_messages(messages, Activity.UPLOAD_TEXT) @@ -156,7 +156,7 @@ class TestScopedContentProcessor: return_value={"user_id": "test-user", "client_id": "test-client"} ) - messages = [ChatMessage(role=Role.USER, text="Test", message_id="msg-123")] + messages = [ChatMessage("user", ["Test"], message_id="msg-123")] with pytest.raises(ValueError, match="Tenant id required"): await processor._map_messages(messages, Activity.UPLOAD_TEXT) @@ -332,7 +332,7 @@ class TestScopedContentProcessor: messages = [ ChatMessage( - role=Role.USER, + role="user", text="Test message", additional_properties={"user_id": "22345678-1234-1234-1234-123456789012"}, ), @@ -355,7 +355,7 @@ class TestScopedContentProcessor: ) processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role=Role.USER, text="Test message")] + messages = [ChatMessage("user", ["Test message"])] requests, user_id = await processor._map_messages( messages, Activity.UPLOAD_TEXT, provided_user_id="32345678-1234-1234-1234-123456789012" @@ -376,7 +376,7 @@ class TestScopedContentProcessor: ) processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role=Role.USER, text="Test message")] + messages = [ChatMessage("user", ["Test message"])] requests, user_id = await processor._map_messages(messages, Activity.UPLOAD_TEXT) @@ -479,7 +479,7 @@ class TestUserIdResolution: settings = PurviewSettings(app_name="Test App") # No tenant_id or app_location processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] requests, user_id = await processor._map_messages(messages, Activity.UPLOAD_TEXT) @@ -494,7 +494,7 @@ class TestUserIdResolution: messages = [ ChatMessage( - role=Role.USER, + role="user", text="Test", additional_properties={"user_id": "22222222-2222-2222-2222-222222222222"}, ) @@ -514,7 +514,7 @@ class TestUserIdResolution: messages = [ ChatMessage( - role=Role.USER, + role="user", text="Test", author_name="33333333-3333-3333-3333-333333333333", ) @@ -532,7 +532,7 @@ class TestUserIdResolution: messages = [ ChatMessage( - role=Role.USER, + role="user", text="Test", author_name="John Doe", # Not a GUID ) @@ -550,7 +550,7 @@ class TestUserIdResolution: """Test provided_user_id parameter is used as last resort.""" processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] requests, user_id = await processor._map_messages( messages, Activity.UPLOAD_TEXT, provided_user_id="44444444-4444-4444-4444-444444444444" @@ -562,7 +562,7 @@ class TestUserIdResolution: """Test invalid provided_user_id is ignored.""" processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] requests, user_id = await processor._map_messages(messages, Activity.UPLOAD_TEXT, provided_user_id="not-a-guid") @@ -575,10 +575,10 @@ class TestUserIdResolution: messages = [ ChatMessage( - role=Role.USER, text="First", additional_properties={"user_id": "55555555-5555-5555-5555-555555555555"} + role="user", text="First", additional_properties={"user_id": "55555555-5555-5555-5555-555555555555"} ), - ChatMessage(role=Role.ASSISTANT, text="Response"), - ChatMessage(role=Role.USER, text="Second"), + ChatMessage("assistant", ["Response"]), + ChatMessage("user", ["Second"]), ] requests, user_id = await processor._map_messages(messages, Activity.UPLOAD_TEXT) @@ -594,14 +594,14 @@ class TestUserIdResolution: processor = ScopedContentProcessor(mock_client, settings) messages = [ - ChatMessage(role=Role.USER, text="First", author_name="Not a GUID"), + ChatMessage("user", ["First"], author_name="Not a GUID"), ChatMessage( - role=Role.ASSISTANT, + role="assistant", text="Response", additional_properties={"user_id": "66666666-6666-6666-6666-666666666666"}, ), ChatMessage( - role=Role.USER, text="Third", additional_properties={"user_id": "77777777-7777-7777-7777-777777777777"} + role="user", text="Third", additional_properties={"user_id": "77777777-7777-7777-7777-777777777777"} ), ] @@ -654,7 +654,7 @@ class TestScopedContentProcessorCaching: scope_identifier="scope-123", scopes=[] ) - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] await processor.process_messages(messages, Activity.UPLOAD_TEXT, user_id="12345678-1234-1234-1234-123456789012") @@ -676,7 +676,7 @@ class TestScopedContentProcessorCaching: mock_client.get_protection_scopes.side_effect = PurviewPaymentRequiredError("Payment required") - messages = [ChatMessage(role=Role.USER, text="Test")] + messages = [ChatMessage("user", ["Test"])] with pytest.raises(PurviewPaymentRequiredError): await processor.process_messages( diff --git a/python/packages/redis/agent_framework_redis/_chat_message_store.py b/python/packages/redis/agent_framework_redis/_chat_message_store.py index 4b50c63571..a68bc9f1d8 100644 --- a/python/packages/redis/agent_framework_redis/_chat_message_store.py +++ b/python/packages/redis/agent_framework_redis/_chat_message_store.py @@ -225,7 +225,7 @@ class RedisChatMessageStore: Example: .. code-block:: python - messages = [ChatMessage(role="user", text="Hello"), ChatMessage(role="assistant", text="Hi there!")] + messages = [ChatMessage("user", ["Hello"]), ChatMessage("assistant", ["Hi there!"])] await store.add_messages(messages) """ if not messages: diff --git a/python/packages/redis/agent_framework_redis/_provider.py b/python/packages/redis/agent_framework_redis/_provider.py index cd8541086e..ce3090b92a 100644 --- a/python/packages/redis/agent_framework_redis/_provider.py +++ b/python/packages/redis/agent_framework_redis/_provider.py @@ -8,7 +8,7 @@ from operator import and_ from typing import Any, Literal, cast import numpy as np -from agent_framework import ChatMessage, Context, ContextProvider, Role +from agent_framework import ChatMessage, Context, ContextProvider from agent_framework.exceptions import ( AgentException, ServiceInitializationError, @@ -503,13 +503,9 @@ class RedisProvider(ContextProvider): messages: list[dict[str, Any]] = [] for message in messages_list: - if ( - message.role.value in {Role.USER.value, Role.ASSISTANT.value, Role.SYSTEM.value} - and message.text - and message.text.strip() - ): + if message.role in {"user", "assistant", "system"} and message.text and message.text.strip(): shaped: dict[str, Any] = { - "role": message.role.value, + "role": message.role, "content": message.text, "conversation_id": self._conversation_id, "message_id": message.message_id, @@ -545,7 +541,7 @@ class RedisProvider(ContextProvider): ) return Context( - messages=[ChatMessage(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")] + messages=[ChatMessage("user", [f"{self.context_prompt}\n{line_separated_memories}"])] if line_separated_memories else None ) diff --git a/python/packages/redis/tests/test_redis_chat_message_store.py b/python/packages/redis/tests/test_redis_chat_message_store.py index a69aef1b0a..0bbb200dfe 100644 --- a/python/packages/redis/tests/test_redis_chat_message_store.py +++ b/python/packages/redis/tests/test_redis_chat_message_store.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import ChatMessage, Content, Role +from agent_framework import ChatMessage, Content from agent_framework_redis import RedisChatMessageStore @@ -19,9 +19,9 @@ class TestRedisChatMessageStore: def sample_messages(self): """Sample chat messages for testing.""" return [ - ChatMessage(role=Role.USER, text="Hello", message_id="msg1"), - ChatMessage(role=Role.ASSISTANT, text="Hi there!", message_id="msg2"), - ChatMessage(role=Role.USER, text="How are you?", message_id="msg3"), + ChatMessage("user", ["Hello"], message_id="msg1"), + ChatMessage("assistant", ["Hi there!"], message_id="msg2"), + ChatMessage("user", ["How are you?"], message_id="msg3"), ] @pytest.fixture @@ -250,7 +250,7 @@ class TestRedisChatMessageStore: store = RedisChatMessageStore(redis_url="redis://localhost:6379", thread_id="test123", max_messages=3) store._redis_client = mock_redis_client - message = ChatMessage(role=Role.USER, text="Test") + message = ChatMessage("user", ["Test"]) await store.add_messages([message]) # Should trim after adding to keep only last 3 messages @@ -269,8 +269,8 @@ class TestRedisChatMessageStore: """Test listing messages with data in Redis.""" # Create proper serialized messages using the actual serialization method test_messages = [ - ChatMessage(role=Role.USER, text="Hello", message_id="msg1"), - ChatMessage(role=Role.ASSISTANT, text="Hi there!", message_id="msg2"), + ChatMessage("user", ["Hello"], message_id="msg1"), + ChatMessage("assistant", ["Hi there!"], message_id="msg2"), ] serialized_messages = [redis_store._serialize_message(msg) for msg in test_messages] mock_redis_client.lrange.return_value = serialized_messages @@ -278,9 +278,9 @@ class TestRedisChatMessageStore: messages = await redis_store.list_messages() assert len(messages) == 2 - assert messages[0].role == Role.USER + assert messages[0].role == "user" assert messages[0].text == "Hello" - assert messages[1].role == Role.ASSISTANT + assert messages[1].role == "assistant" assert messages[1].text == "Hi there!" async def test_list_messages_with_initial_messages(self, sample_messages): @@ -412,7 +412,7 @@ class TestRedisChatMessageStore: # Message with multiple content types message = ChatMessage( - role=Role.ASSISTANT, + role="assistant", contents=[Content.from_text(text="Hello"), Content.from_text(text="World")], author_name="TestBot", message_id="complex_msg", @@ -422,7 +422,7 @@ class TestRedisChatMessageStore: serialized = store._serialize_message(message) deserialized = store._deserialize_message(serialized) - assert deserialized.role == Role.ASSISTANT + assert deserialized.role == "assistant" assert deserialized.text == "Hello World" assert deserialized.author_name == "TestBot" assert deserialized.message_id == "complex_msg" @@ -444,7 +444,7 @@ class TestRedisChatMessageStore: store = RedisChatMessageStore(redis_url="redis://localhost:6379", thread_id="test123") store._redis_client = mock_client - message = ChatMessage(role=Role.USER, text="Test") + message = ChatMessage("user", ["Test"]) # Should propagate Redis connection errors with pytest.raises(Exception, match="Connection failed"): @@ -485,7 +485,7 @@ class TestRedisChatMessageStore: mock_redis_client.llen.return_value = 2 mock_redis_client.lset = AsyncMock() - new_message = ChatMessage(role=Role.USER, text="Updated message") + new_message = ChatMessage("user", ["Updated message"]) await redis_store.setitem(0, new_message) mock_redis_client.lset.assert_called_once() @@ -497,13 +497,13 @@ class TestRedisChatMessageStore: """Test setitem raises IndexError for invalid index.""" mock_redis_client.llen.return_value = 0 - new_message = ChatMessage(role=Role.USER, text="Test") + new_message = ChatMessage("user", ["Test"]) with pytest.raises(IndexError): await redis_store.setitem(0, new_message) async def test_append(self, redis_store, mock_redis_client): """Test append method delegates to add_messages.""" - message = ChatMessage(role=Role.USER, text="Appended message") + message = ChatMessage("user", ["Appended message"]) await redis_store.append(message) # Should call pipeline operations via add_messages diff --git a/python/packages/redis/tests/test_redis_provider.py b/python/packages/redis/tests/test_redis_provider.py index 723334741b..e5db9d25fd 100644 --- a/python/packages/redis/tests/test_redis_provider.py +++ b/python/packages/redis/tests/test_redis_provider.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import numpy as np import pytest -from agent_framework import ChatMessage, Role +from agent_framework import ChatMessage from agent_framework.exceptions import AgentException, ServiceInitializationError from redisvl.utils.vectorize import CustomTextVectorizer @@ -115,16 +115,16 @@ class TestRedisProviderMessages: @pytest.fixture def sample_messages(self) -> list[ChatMessage]: return [ - ChatMessage(role=Role.USER, text="Hello, how are you?"), - ChatMessage(role=Role.ASSISTANT, text="I'm doing well, thank you!"), - ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant"), + ChatMessage("user", ["Hello, how are you?"]), + ChatMessage("assistant", ["I'm doing well, thank you!"]), + ChatMessage("system", ["You are a helpful assistant"]), ] # Writes require at least one scoping filter to avoid unbounded operations async def test_messages_adding_requires_filters(self, patch_index_from_dict): # noqa: ARG002 provider = RedisProvider() with pytest.raises(ServiceInitializationError): - await provider.invoked("thread123", ChatMessage(role=Role.USER, text="Hello")) + await provider.invoked("thread123", ChatMessage("user", ["Hello"])) # Captures the per-operation thread id when provided async def test_thread_created_sets_per_operation_id(self, patch_index_from_dict): # noqa: ARG002 @@ -157,7 +157,7 @@ class TestRedisProviderModelInvoking: async def test_model_invoking_requires_filters(self, patch_index_from_dict): # noqa: ARG002 provider = RedisProvider() with pytest.raises(ServiceInitializationError): - await provider.invoking(ChatMessage(role=Role.USER, text="Hi")) + await provider.invoking(ChatMessage("user", ["Hi"])) # Ensures text-only search path is used and context is composed from hits async def test_textquery_path_and_context_contents( @@ -168,7 +168,7 @@ class TestRedisProviderModelInvoking: provider = RedisProvider(user_id="u1") # Act - ctx = await provider.invoking([ChatMessage(role=Role.USER, text="q1")]) + ctx = await provider.invoking([ChatMessage("user", ["q1"])]) # Assert: TextQuery used (not HybridQuery), filter_expression included assert patch_queries["TextQuery"].call_count == 1 @@ -190,7 +190,7 @@ class TestRedisProviderModelInvoking: ): # noqa: ARG002 mock_index.query = AsyncMock(return_value=[]) provider = RedisProvider(user_id="u1") - ctx = await provider.invoking([ChatMessage(role=Role.USER, text="any")]) + ctx = await provider.invoking([ChatMessage("user", ["any"])]) assert ctx.messages == [] # Ensures hybrid vector-text search is used when a vectorizer and vector field are configured @@ -198,7 +198,7 @@ class TestRedisProviderModelInvoking: mock_index.query = AsyncMock(return_value=[{"content": "Hit"}]) provider = RedisProvider(user_id="u1", redis_vectorizer=CUSTOM_VECTORIZER, vector_field_name="vec") - ctx = await provider.invoking([ChatMessage(role=Role.USER, text="hello")]) + ctx = await provider.invoking([ChatMessage("user", ["hello"])]) # Assert: HybridQuery used with vector and vector field assert patch_queries["HybridQuery"].call_count == 1 @@ -240,9 +240,9 @@ class TestMessagesAddingBehavior: ) msgs = [ - ChatMessage(role=Role.USER, text="u"), - ChatMessage(role=Role.ASSISTANT, text="a"), - ChatMessage(role=Role.SYSTEM, text="s"), + ChatMessage("user", ["u"]), + ChatMessage("assistant", ["a"]), + ChatMessage("system", ["s"]), ] await provider.invoked(msgs) @@ -265,8 +265,8 @@ class TestMessagesAddingBehavior: ): # noqa: ARG002 provider = RedisProvider(user_id="u1", scope_to_per_operation_thread_id=True) msgs = [ - ChatMessage(role=Role.USER, text=" "), - ChatMessage(role=Role.TOOL, text="tool output"), + ChatMessage("user", [" "]), + ChatMessage("tool", ["tool output"]), ] await provider.invoked(msgs) # No valid messages -> no load @@ -279,8 +279,8 @@ class TestIndexCreationPublicCalls: self, mock_index: AsyncMock, patch_index_from_dict ): # noqa: ARG002 provider = RedisProvider(user_id="u1") - await provider.invoked(ChatMessage(role=Role.USER, text="m1")) - await provider.invoked(ChatMessage(role=Role.USER, text="m2")) + await provider.invoked(ChatMessage("user", ["m1"])) + await provider.invoked(ChatMessage("user", ["m2"])) # create only on first call assert mock_index.create.await_count == 1 @@ -291,7 +291,7 @@ class TestIndexCreationPublicCalls: mock_index.exists = AsyncMock(return_value=False) provider = RedisProvider(user_id="u1") mock_index.query = AsyncMock(return_value=[{"content": "C"}]) - await provider.invoking([ChatMessage(role=Role.USER, text="q")]) + await provider.invoking([ChatMessage("user", ["q"])]) assert mock_index.create.await_count == 1 @@ -321,7 +321,7 @@ class TestVectorPopulation: vector_field_name="vec", ) - await provider.invoked(ChatMessage(role=Role.USER, text="hello")) + await provider.invoked(ChatMessage("user", ["hello"])) assert mock_index.load.await_count == 1 (loaded_args, _kwargs) = mock_index.load.call_args docs = loaded_args[0] 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 39d360b1e1..38df1424db 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 @@ -103,7 +103,6 @@ async def run_agent_framework_with_cycle() -> None: WorkflowContext, WorkflowOutputEvent, executor, - tool, ) from agent_framework.openai import OpenAIChatClient diff --git a/python/samples/autogen-migration/orchestrations/03_swarm.py b/python/samples/autogen-migration/orchestrations/03_swarm.py index 3fa9f7a04d..09d8ac0486 100644 --- a/python/samples/autogen-migration/orchestrations/03_swarm.py +++ b/python/samples/autogen-migration/orchestrations/03_swarm.py @@ -102,7 +102,6 @@ async def run_agent_framework() -> None: RequestInfoEvent, WorkflowRunState, WorkflowStatusEvent, - tool, ) from agent_framework.openai import OpenAIChatClient @@ -142,7 +141,7 @@ async def run_agent_framework() -> None: ) .set_coordinator(triage_agent) .add_handoff(triage_agent, [billing_agent, tech_support]) - .with_termination_condition(lambda conv: sum(1 for msg in conv if msg.role.value == "user") > 3) + .with_termination_condition(lambda conv: sum(1 for msg in conv if msg.role == "user") > 3) .build() ) diff --git a/python/samples/demos/chatkit-integration/app.py b/python/samples/demos/chatkit-integration/app.py index 4e11e4948c..11b3140769 100644 --- a/python/samples/demos/chatkit-integration/app.py +++ b/python/samples/demos/chatkit-integration/app.py @@ -18,8 +18,7 @@ from typing import Annotated, Any import uvicorn # Agent Framework imports -from agent_framework import AgentResponseUpdate, ChatAgent, ChatMessage, FunctionResultContent, Role -from agent_framework import tool +from agent_framework import AgentResponseUpdate, ChatAgent, ChatMessage, FunctionResultContent, tool from agent_framework.azure import AzureOpenAIChatClient # Agent Framework ChatKit integration @@ -131,6 +130,7 @@ async def stream_widget( yield ThreadItemDoneEvent(type="thread.item.done", item=widget_item) + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( @@ -170,6 +170,7 @@ def get_weather( ) return WeatherResponse(text, weather_data) + @tool(approval_mode="never_require") def get_time() -> str: """Get the current UTC time.""" @@ -177,6 +178,7 @@ def get_time() -> str: logger.info("Getting current UTC time") return f"Current UTC time: {current_time.strftime('%Y-%m-%d %H:%M:%S')} UTC" + @tool(approval_mode="never_require") def show_city_selector() -> str: """Show an interactive city selector widget to the user. @@ -279,7 +281,7 @@ class WeatherChatKitServer(ChatKitServer[dict[str, Any]]): title_prompt = [ ChatMessage( - role=Role.USER, + role="user", text=( f"Generate a very short, concise title (max 40 characters) for a conversation " f"that starts with:\n\n{conversation_context}\n\n" @@ -456,7 +458,7 @@ class WeatherChatKitServer(ChatKitServer[dict[str, Any]]): weather_data: WeatherData | None = None # Create an agent message asking about the weather - agent_messages = [ChatMessage(role=Role.USER, text=f"What's the weather in {city_label}?")] + agent_messages = [ChatMessage("user", [f"What's the weather in {city_label}?"])] logger.debug(f"Processing weather query: {agent_messages[0].text}") diff --git a/python/samples/demos/hosted_agents/agent_with_text_search_rag/main.py b/python/samples/demos/hosted_agents/agent_with_text_search_rag/main.py index 2d99eac9f4..0c0660ceb0 100644 --- a/python/samples/demos/hosted_agents/agent_with_text_search_rag/main.py +++ b/python/samples/demos/hosted_agents/agent_with_text_search_rag/main.py @@ -6,7 +6,7 @@ from collections.abc import MutableSequence from dataclasses import dataclass from typing import Any -from agent_framework import ChatMessage, Context, ContextProvider, Role +from agent_framework import ChatMessage, Context, ContextProvider from agent_framework.azure import AzureOpenAIChatClient from azure.ai.agentserver.agentframework import from_agent_framework # pyright: ignore[reportUnknownVariableType] from azure.identity import DefaultAzureCredential @@ -85,7 +85,7 @@ class TextSearchContextProvider(ContextProvider): return Context( messages=[ ChatMessage( - role=Role.USER, text="\n\n".join(json.dumps(result.__dict__, indent=2) for result in results) + role="user", text="\n\n".join(json.dumps(result.__dict__, indent=2) for result in results) ) ] ) diff --git a/python/samples/demos/m365-agent/m365_agent_demo/app.py b/python/samples/demos/m365-agent/m365_agent_demo/app.py index 9e11780614..3aa7382811 100644 --- a/python/samples/demos/m365-agent/m365_agent_demo/app.py +++ b/python/samples/demos/m365-agent/m365_agent_demo/app.py @@ -16,8 +16,7 @@ from dataclasses import dataclass from random import randint from typing import Annotated -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.openai import OpenAIChatClient from aiohttp import web from aiohttp.web_middlewares import middleware @@ -77,6 +76,7 @@ def load_app_config() -> AppConfig: port = 3978 return AppConfig(use_anonymous_mode=use_anonymous_mode, port=port, agents_sdk_config=agents_sdk_config) + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/demos/workflow_evaluation/_tools.py b/python/samples/demos/workflow_evaluation/_tools.py index 12be0f4094..0e5443d5b0 100644 --- a/python/samples/demos/workflow_evaluation/_tools.py +++ b/python/samples/demos/workflow_evaluation/_tools.py @@ -70,7 +70,7 @@ def search_hotels( "availability": "Available" } ] - + return json.dumps({ "location": location, "check_in": check_in, @@ -140,7 +140,7 @@ def get_hotel_details( "nearby_attractions": ["Eiffel Tower (0.2 mi)", "Seine River Cruise Dock (0.3 mi)", "Trocadéro (0.5 mi)"] } } - + details = hotel_details.get(hotel_name, { "name": hotel_name, "description": "Comfortable hotel with modern amenities", @@ -150,7 +150,7 @@ def get_hotel_details( "reviews": {"total": 0, "recent_comments": []}, "nearby_attractions": [] }) - + return json.dumps({ "hotel_name": hotel_name, "details": details @@ -270,7 +270,7 @@ def search_flights( "stops": "Nonstop" } ] - + return json.dumps({ "origin": origin, "destination": destination, @@ -317,7 +317,7 @@ def get_flight_details( }, "amenities": ["WiFi", "In-flight entertainment", "Meals included"] } - + return json.dumps({ "flight_details": mock_details }) @@ -439,7 +439,7 @@ def search_activities( "booking_required": False } ] - + if category: activities = [act for act in all_activities if act["category"] == category] else: @@ -456,7 +456,7 @@ def search_activities( "availability": "Daily at 10:00 AM and 2:00 PM" } ] - + return json.dumps({ "location": location, "date": date, @@ -523,7 +523,7 @@ def get_activity_details( "reviews_count": 2341 } } - + details = activity_details_map.get(activity_name, { "name": activity_name, "description": "An immersive experience that showcases the best of local culture and attractions.", @@ -538,7 +538,7 @@ def get_activity_details( "rating": 4.5, "reviews_count": 100 }) - + return json.dumps({ "activity_details": details }) @@ -558,7 +558,7 @@ def confirm_booking( booking status, customer information, and next steps. """ confirmation_number = f"CONF-{booking_type.upper()}-{booking_id}" - + confirmation_data = { "confirmation_number": confirmation_number, "booking_type": booking_type, @@ -572,7 +572,7 @@ def confirm_booking( "Bring confirmation number and valid ID" ] } - + return json.dumps({ "confirmation": confirmation_data }) @@ -595,7 +595,7 @@ def check_hotel_availability( and last checked timestamp. """ availability_status = "Available" - + availability_data = { "service_type": "hotel", "hotel_name": hotel_name, @@ -607,7 +607,7 @@ def check_hotel_availability( "price_per_night": "$185", "last_checked": datetime.now().isoformat() } - + return json.dumps({ "availability": availability_data }) @@ -629,7 +629,7 @@ def check_flight_availability( and last checked timestamp. """ availability_status = "Available" - + availability_data = { "service_type": "flight", "flight_number": flight_number, @@ -640,7 +640,7 @@ def check_flight_availability( "price_per_passenger": "$520", "last_checked": datetime.now().isoformat() } - + return json.dumps({ "availability": availability_data }) @@ -662,7 +662,7 @@ def check_activity_availability( and last checked timestamp. """ availability_status = "Available" - + availability_data = { "service_type": "activity", "activity_name": activity_name, @@ -673,7 +673,7 @@ def check_activity_availability( "price_per_person": "$45", "last_checked": datetime.now().isoformat() } - + return json.dumps({ "availability": availability_data }) @@ -694,7 +694,7 @@ def process_payment( payment method details, and receipt URL. """ transaction_id = f"TXN-{datetime.now().strftime('%Y%m%d%H%M%S')}" - + payment_result = { "transaction_id": transaction_id, "amount": amount, @@ -706,13 +706,12 @@ def process_payment( "timestamp": datetime.now().isoformat(), "receipt_url": f"https://payments.travelagency.com/receipt/{transaction_id}" } - + return json.dumps({ "payment_result": payment_result }) - # Mock payment validation tool @tool(name="validate_payment_method", description="Validate a payment method before processing.") def validate_payment_method( @@ -725,11 +724,11 @@ def validate_payment_method( validation messages, supported currencies, and processing fee information. """ method_type = payment_method.get("type", "credit_card") - + # Validation logic is_valid = True validation_messages = [] - + if method_type == "credit_card": if not payment_method.get("number"): is_valid = False @@ -740,7 +739,7 @@ def validate_payment_method( if not payment_method.get("cvv"): is_valid = False validation_messages.append("CVV is required") - + validation_result = { "is_valid": is_valid, "payment_method_type": method_type, @@ -748,7 +747,7 @@ def validate_payment_method( "supported_currencies": ["USD", "EUR", "GBP", "JPY"], "processing_fee": "2.5%" } - + return json.dumps({ "validation_result": validation_result }) diff --git a/python/samples/demos/workflow_evaluation/create_workflow.py b/python/samples/demos/workflow_evaluation/create_workflow.py index dc1e920b69..665be0667e 100644 --- a/python/samples/demos/workflow_evaluation/create_workflow.py +++ b/python/samples/demos/workflow_evaluation/create_workflow.py @@ -51,13 +51,11 @@ from agent_framework import ( AgentRunUpdateEvent, ChatMessage, Executor, - Role, WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, executor, handler, - tool, ) from agent_framework.azure import AzureAIClient from azure.ai.projects.aio import AIProjectClient @@ -71,7 +69,7 @@ load_dotenv() @executor(id="start_executor") async def start_executor(input: str, ctx: WorkflowContext[list[ChatMessage]]) -> None: """Initiates the workflow by sending the user query to all specialized agents.""" - await ctx.send_message([ChatMessage(role="user", text=input)]) + await ctx.send_message([ChatMessage("user", [input])]) class ResearchLead(Executor): @@ -107,11 +105,11 @@ class ResearchLead(Executor): # Generate comprehensive travel plan summary messages = [ ChatMessage( - role=Role.SYSTEM, + role="system", text="You are a travel planning coordinator. Summarize findings from multiple specialized travel agents and provide a clear, comprehensive travel plan based on the user's query.", ), ChatMessage( - role=Role.USER, + role="user", text=f"Original query: {user_query}\n\nFindings from specialized travel agents:\n{summary_text}\n\nPlease provide a comprehensive travel plan based on these findings.", ), ] @@ -136,7 +134,7 @@ class ResearchLead(Executor): findings = [] if response.agent_response and response.agent_response.messages: for msg in response.agent_response.messages: - if msg.role == Role.ASSISTANT and msg.text and msg.text.strip(): + if msg.role == "assistant" and msg.text and msg.text.strip(): findings.append(msg.text.strip()) if findings: diff --git a/python/samples/demos/workflow_evaluation/run_evaluation.py b/python/samples/demos/workflow_evaluation/run_evaluation.py index 610f7ade00..defcde114f 100644 --- a/python/samples/demos/workflow_evaluation/run_evaluation.py +++ b/python/samples/demos/workflow_evaluation/run_evaluation.py @@ -16,16 +16,15 @@ import time from azure.ai.projects import AIProjectClient from azure.identity import DefaultAzureCredential -from dotenv import load_dotenv - from create_workflow import create_and_run_workflow +from dotenv import load_dotenv def print_section(title: str): """Print a formatted section header.""" - print(f"\n{'='*80}") + print(f"\n{'=' * 80}") print(f"{title}") - print(f"{'='*80}") + print(f"{'=' * 80}") async def run_workflow(): @@ -37,9 +36,9 @@ async def run_workflow(): print_section("Step 1: Running Workflow") print("Executing multi-agent travel planning workflow...") print("This may take a few minutes...") - + workflow_data = await create_and_run_workflow() - + print("Workflow execution completed") return workflow_data @@ -47,31 +46,31 @@ async def run_workflow(): def display_response_summary(workflow_data: dict): """Display summary of response data.""" print_section("Step 2: Response Data Summary") - + print(f"Query: {workflow_data['query']}") print(f"\nAgents tracked: {len(workflow_data['agents'])}") - - for agent_name, agent_data in workflow_data['agents'].items(): - response_count = agent_data['response_count'] + + for agent_name, agent_data in workflow_data["agents"].items(): + response_count = agent_data["response_count"] print(f" {agent_name}: {response_count} response(s)") def fetch_agent_responses(openai_client, workflow_data: dict, agent_names: list): """Fetch and display final responses from specified agents.""" print_section("Step 3: Fetching Agent Responses") - + for agent_name in agent_names: - if agent_name not in workflow_data['agents']: + if agent_name not in workflow_data["agents"]: continue - - agent_data = workflow_data['agents'][agent_name] - if not agent_data['response_ids']: + + agent_data = workflow_data["agents"][agent_name] + if not agent_data["response_ids"]: continue - - final_response_id = agent_data['response_ids'][-1] + + final_response_id = agent_data["response_ids"][-1] print(f"\n{agent_name}") print(f" Response ID: {final_response_id}") - + try: response = openai_client.responses.retrieve(response_id=final_response_id) content = response.output[-1].content[-1].text @@ -84,9 +83,9 @@ def fetch_agent_responses(openai_client, workflow_data: dict, agent_names: list) def create_evaluation(openai_client, model_deployment: str): """Create evaluation with multiple evaluators.""" print_section("Step 4: Creating Evaluation") - + data_source_config = {"type": "azure_ai_source", "scenario": "responses"} - + testing_criteria = [ { "type": "azure_ai_evaluator", @@ -113,33 +112,33 @@ def create_evaluation(openai_client, model_deployment: str): "initialization_parameters": {"deployment_name": model_deployment} }, ] - + eval_object = openai_client.evals.create( name="Travel Workflow Multi-Evaluator Assessment", data_source_config=data_source_config, testing_criteria=testing_criteria, ) - + evaluator_names = [criterion["name"] for criterion in testing_criteria] print(f"Evaluation created: {eval_object.id}") print(f"Evaluators ({len(evaluator_names)}): {', '.join(evaluator_names)}") - + return eval_object def run_evaluation(openai_client, eval_object, workflow_data: dict, agent_names: list): """Run evaluation on selected agent responses.""" print_section("Step 5: Running Evaluation") - + selected_response_ids = [] for agent_name in agent_names: - if agent_name in workflow_data['agents']: - agent_data = workflow_data['agents'][agent_name] - if agent_data['response_ids']: - selected_response_ids.append(agent_data['response_ids'][-1]) - + if agent_name in workflow_data["agents"]: + agent_data = workflow_data["agents"][agent_name] + if agent_data["response_ids"]: + selected_response_ids.append(agent_data["response_ids"][-1]) + print(f"Selected {len(selected_response_ids)} responses for evaluation") - + data_source = { "type": "azure_ai_responses", "item_generation_params": { @@ -151,24 +150,24 @@ def run_evaluation(openai_client, eval_object, workflow_data: dict, agent_names: }, }, } - + eval_run = openai_client.evals.runs.create( eval_id=eval_object.id, name="Multi-Agent Response Evaluation", data_source=data_source ) - + print(f"Evaluation run created: {eval_run.id}") - + return eval_run def monitor_evaluation(openai_client, eval_object, eval_run): """Monitor evaluation progress and display results.""" print_section("Step 6: Monitoring Evaluation") - + print("Waiting for evaluation to complete...") - + while eval_run.status not in ["completed", "failed"]: eval_run = openai_client.evals.runs.retrieve( run_id=eval_run.id, @@ -176,7 +175,7 @@ def monitor_evaluation(openai_client, eval_object, eval_run): ) print(f"Status: {eval_run.status}") time.sleep(5) - + if eval_run.status == "completed": print("\nEvaluation completed successfully") print(f"Result counts: {eval_run.result_counts}") @@ -188,31 +187,31 @@ def monitor_evaluation(openai_client, eval_object, eval_run): async def main(): """Main execution flow.""" load_dotenv() - + print("Travel Planning Workflow Evaluation") - + workflow_data = await run_workflow() - + display_response_summary(workflow_data) - + project_client = AIProjectClient( endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=DefaultAzureCredential(), api_version="2025-11-15-preview" ) openai_client = project_client.get_openai_client() - + agents_to_evaluate = ["hotel-search-agent", "flight-search-agent", "activity-search-agent"] - + fetch_agent_responses(openai_client, workflow_data, agents_to_evaluate) - + model_deployment = os.environ.get("AZURE_AI_MODEL_DEPLOYMENT_NAME", "gpt-4o-mini") eval_object = create_evaluation(openai_client, model_deployment) - + eval_run = run_evaluation(openai_client, eval_object, workflow_data, agents_to_evaluate) - + monitor_evaluation(openai_client, eval_object, eval_run) - + print_section("Complete") diff --git a/python/samples/getting_started/agents/anthropic/anthropic_basic.py b/python/samples/getting_started/agents/anthropic/anthropic_basic.py index 41fbb3b7e6..18a49d5e88 100644 --- a/python/samples/getting_started/agents/anthropic/anthropic_basic.py +++ b/python/samples/getting_started/agents/anthropic/anthropic_basic.py @@ -4,8 +4,8 @@ import asyncio from random import randint from typing import Annotated -from agent_framework.anthropic import AnthropicClient from agent_framework import tool +from agent_framework.anthropic import AnthropicClient """ Anthropic Chat Agent Example @@ -13,6 +13,7 @@ Anthropic Chat Agent Example This sample demonstrates using Anthropic with an agent and a single custom tool. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py b/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py index f6bf9802e0..77465c3c52 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py @@ -4,10 +4,10 @@ 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 pydantic import Field -from agent_framework import tool """ Azure AI Agent Basic Example @@ -16,6 +16,7 @@ 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py index 266cfbdfdd..b05ec92f80 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py @@ -5,12 +5,12 @@ 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 AgentReference, PromptAgentDefinition from azure.identity.aio import AzureCliCredential from pydantic import Field -from agent_framework import tool """ Azure AI Project Agent Provider Methods Example @@ -26,6 +26,7 @@ with different configurations, which is efficient for multi-agent scenarios. Each method returns a ChatAgent that can be used for conversations. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py b/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py index 7106bb1f31..b9472c9f1a 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py @@ -4,10 +4,10 @@ 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 pydantic import Field -from agent_framework import tool """ Azure AI Agent Latest Version Example @@ -17,6 +17,7 @@ instead of creating a new agent version on each instantiation. The first call cr 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py index 9e61d2486c..3e2b520ede 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py @@ -5,7 +5,6 @@ import asyncio from agent_framework import ( AgentResponseUpdate, HostedCodeInterpreterTool, - tool, ) from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py index 8438abcf67..0410c00bd7 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py @@ -4,11 +4,11 @@ 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 pydantic import Field -from agent_framework import tool """ Azure AI Agent Existing Conversation Example @@ -16,6 +16,7 @@ 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py index ba131817d1..382205b7cc 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py @@ -5,10 +5,10 @@ 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 pydantic import Field -from agent_framework import tool """ Azure AI Agent with Explicit Settings Example @@ -17,6 +17,7 @@ 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py index 76394a8aac..9262240088 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py @@ -25,10 +25,10 @@ async def handle_approvals_without_thread(query: str, agent: "AgentProtocol") -> 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(ChatMessage(role="assistant", contents=[user_input_needed])) + new_inputs.append(ChatMessage("assistant", [user_input_needed])) user_approval = input("Approve function call? (y/n): ") new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) + ChatMessage("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) result = await agent.run(new_inputs, store=False) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_response_format.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_response_format.py index a0af51da6a..39ea0b722c 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_response_format.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_response_format.py @@ -41,12 +41,13 @@ async def main() -> None: print(f"User: {query}") result = await agent.run(query) - if release_brief := result.try_parse_value(ReleaseBrief): + 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}") - else: + except Exception: print(f"Failed to parse response: {result.text}") diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_basic.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_basic.py index 787b1f317b..e06232cf56 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_basic.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_basic.py @@ -4,10 +4,10 @@ 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 pydantic import Field -from agent_framework import tool """ Azure AI Agent Basic Example @@ -16,6 +16,7 @@ This sample demonstrates basic usage of AzureAIAgentsProvider to create agents w 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_provider_methods.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_provider_methods.py index adb6386797..5dd06f16f0 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_provider_methods.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_provider_methods.py @@ -5,11 +5,11 @@ 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 pydantic import Field -from agent_framework import tool """ Azure AI Agent Provider Methods Example @@ -20,6 +20,7 @@ This sample demonstrates the methods available on the AzureAIAgentsProvider clas - 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py index 44554af05a..665c707adc 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py @@ -7,7 +7,6 @@ from agent_framework import ( AgentResponseUpdate, HostedCodeInterpreterTool, HostedFileContent, - tool, ) from agent_framework.azure import AzureAIAgentsProvider from azure.ai.agents.aio import AgentsClient diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py index 4852ba15b7..0f17d35183 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py @@ -5,11 +5,11 @@ 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 pydantic import Field -from agent_framework import tool """ Azure AI Agent with Existing Thread Example @@ -18,6 +18,7 @@ This sample demonstrates working with pre-existing conversation threads by providing thread IDs for thread reuse patterns. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py index 85b4d55b95..05c8c60a36 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py @@ -5,10 +5,10 @@ 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 pydantic import Field -from agent_framework import tool """ Azure AI Agent with Explicit Settings Example @@ -17,6 +17,7 @@ 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_function_tools.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_function_tools.py index 7da870c42a..97cd59ca19 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_function_tools.py @@ -5,10 +5,10 @@ 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 pydantic import Field -from agent_framework import tool """ Azure AI Agent with Function Tools Example @@ -17,6 +17,7 @@ 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( @@ -26,6 +27,7 @@ def get_weather( 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.""" diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py index 4539696bc6..7185203c24 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py @@ -34,9 +34,9 @@ To set up Bing Grounding: 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def get_time() -> str: """Get the current UTC time.""" current_time = datetime.now(timezone.utc) diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_response_format.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_response_format.py index 1a55724c60..a607304724 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_response_format.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_response_format.py @@ -56,13 +56,14 @@ async def main() -> None: result1 = await agent.run(query1) - if weather := result1.try_parse_value(WeatherInfo): + 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}") - else: + except Exception: print(f"Failed to parse response: {result1.text}") # Request 2: Override response_format at runtime with CityInfo @@ -72,12 +73,13 @@ async def main() -> None: result2 = await agent.run(query2, options={"response_format": CityInfo}) - if city := result2.try_parse_value(CityInfo): + try: + city = result2.value print("Agent:") print(f" City: {city.city_name}") print(f" Population: {city.population}") print(f" Country: {city.country}") - else: + except Exception: print(f"Failed to parse response: {result2.text}") diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_thread.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_thread.py index 04128c80a1..a48851d67c 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_thread.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_thread.py @@ -4,8 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread -from agent_framework import tool +from agent_framework import AgentThread, tool from agent_framework.azure import AzureAIAgentsProvider from azure.identity.aio import AzureCliCredential from pydantic import Field @@ -17,6 +16,7 @@ This sample demonstrates thread management with Azure AI Agents, comparing automatic thread creation with explicit thread management for persistent context. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_openai/azure_assistants_basic.py b/python/samples/getting_started/agents/azure_openai/azure_assistants_basic.py index 7613eb62dc..243ba55bf3 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_assistants_basic.py +++ b/python/samples/getting_started/agents/azure_openai/azure_assistants_basic.py @@ -4,10 +4,10 @@ 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 pydantic import Field -from agent_framework import tool """ Azure OpenAI Assistants Basic Example @@ -16,6 +16,7 @@ This sample demonstrates basic usage of AzureOpenAIAssistantsClient with automat 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_existing_assistant.py b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_existing_assistant.py index 70cd79b41a..7e373d4fad 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_existing_assistant.py +++ b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_existing_assistant.py @@ -5,8 +5,7 @@ import os from random import randint from typing import Annotated -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.azure import AzureOpenAIAssistantsClient from azure.identity import AzureCliCredential, get_bearer_token_provider from openai import AsyncAzureOpenAI @@ -19,6 +18,7 @@ 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_explicit_settings.py b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_explicit_settings.py index 581c447240..65b0214ab8 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_explicit_settings.py @@ -5,10 +5,10 @@ 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 pydantic import Field -from agent_framework import tool """ Azure OpenAI Assistants with Explicit Settings Example @@ -17,6 +17,7 @@ This sample demonstrates creating Azure OpenAI Assistants with explicit configur settings rather than relying on environment variable defaults. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_function_tools.py b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_function_tools.py index 6256681fce..8333e7fdc8 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_function_tools.py @@ -5,8 +5,7 @@ from datetime import datetime, timezone from random import randint from typing import Annotated -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.azure import AzureOpenAIAssistantsClient from azure.identity import AzureCliCredential from pydantic import Field @@ -18,6 +17,7 @@ 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( @@ -27,6 +27,7 @@ def get_weather( 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.""" diff --git a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_thread.py b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_thread.py index a791604744..793f8260c3 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_thread.py +++ b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_thread.py @@ -4,8 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread, ChatAgent -from agent_framework import tool +from agent_framework import AgentThread, ChatAgent, tool from agent_framework.azure import AzureOpenAIAssistantsClient from azure.identity import AzureCliCredential from pydantic import Field @@ -17,6 +16,7 @@ This sample demonstrates thread management with Azure OpenAI Assistants, compari automatic thread creation with explicit thread management for persistent context. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_openai/azure_chat_client_basic.py b/python/samples/getting_started/agents/azure_openai/azure_chat_client_basic.py index 25b0cc5bd3..feb2ab5f89 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_chat_client_basic.py +++ b/python/samples/getting_started/agents/azure_openai/azure_chat_client_basic.py @@ -4,10 +4,10 @@ import asyncio from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential from pydantic import Field -from agent_framework import tool """ Azure OpenAI Chat Client Basic Example @@ -16,6 +16,7 @@ This sample demonstrates basic usage of AzureOpenAIChatClient for direct chat-ba interactions, showing both streaming and non-streaming responses. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py index db97390aa8..5f7bc794e5 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py @@ -5,10 +5,10 @@ import os from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential from pydantic import Field -from agent_framework import tool """ Azure OpenAI Chat Client with Explicit Settings Example @@ -17,6 +17,7 @@ This sample demonstrates creating Azure OpenAI Chat Client with explicit configu settings rather than relying on environment variable defaults. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_function_tools.py b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_function_tools.py index 33b8ffe577..777bcc51b1 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_function_tools.py @@ -5,8 +5,7 @@ from datetime import datetime, timezone from random import randint from typing import Annotated -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential from pydantic import Field @@ -18,6 +17,7 @@ This sample demonstrates function tool integration with Azure OpenAI Chat Client 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( @@ -27,6 +27,7 @@ def get_weather( 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.""" diff --git a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_thread.py b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_thread.py index 16fee4226e..08ada3ba97 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_thread.py +++ b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_thread.py @@ -4,8 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread, ChatAgent, ChatMessageStore -from agent_framework import tool +from agent_framework import AgentThread, ChatAgent, ChatMessageStore, tool from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential from pydantic import Field @@ -17,6 +16,7 @@ This sample demonstrates thread management with Azure OpenAI Chat Client, compar automatic thread creation with explicit thread management for persistent context. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_basic.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_basic.py index 921ee76634..af79b0465c 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_basic.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_basic.py @@ -4,10 +4,10 @@ import asyncio from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential from pydantic import Field -from agent_framework import tool """ Azure OpenAI Responses Client Basic Example @@ -16,6 +16,7 @@ This sample demonstrates basic usage of AzureOpenAIResponsesClient for structure response generation, showing both streaming and non-streaming responses. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py index 5a38798ef0..c21462b11f 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py @@ -5,10 +5,10 @@ import os from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential from pydantic import Field -from agent_framework import tool """ Azure OpenAI Responses Client with Explicit Settings Example @@ -17,6 +17,7 @@ This sample demonstrates creating Azure OpenAI Responses Client with explicit co settings rather than relying on environment variable defaults. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_function_tools.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_function_tools.py index 1799f88560..a5d6d85aa6 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_function_tools.py @@ -5,8 +5,7 @@ from datetime import datetime, timezone from random import randint from typing import Annotated -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential from pydantic import Field @@ -18,6 +17,7 @@ This sample demonstrates function tool integration with Azure OpenAI Responses C 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( @@ -27,6 +27,7 @@ def get_weather( 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.""" diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py index 0833065742..7d346c8fc8 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py @@ -30,10 +30,10 @@ async def handle_approvals_without_thread(query: str, agent: "AgentProtocol"): 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(ChatMessage(role="assistant", contents=[user_input_needed])) + new_inputs.append(ChatMessage("assistant", [user_input_needed])) user_approval = input("Approve function call? (y/n): ") new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) + ChatMessage("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) result = await agent.run(new_inputs) @@ -71,7 +71,7 @@ async def handle_approvals_with_thread_streaming(query: str, agent: "AgentProtoc new_input_added = True while new_input_added: new_input_added = False - new_input.append(ChatMessage(role="user", text=query)) + new_input.append(ChatMessage("user", [query])) async for update in agent.run_stream(new_input, thread=thread, store=True): if update.user_input_requests: for user_input_needed in update.user_input_requests: diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_thread.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_thread.py index 817ac69ef2..01ade8da6f 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_thread.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_thread.py @@ -4,8 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread, ChatAgent -from agent_framework import tool +from agent_framework import AgentThread, ChatAgent, tool from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential from pydantic import Field @@ -17,6 +16,7 @@ This sample demonstrates thread management with Azure OpenAI Responses Client, c automatic thread creation with explicit thread management for persistent context. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/custom/custom_agent.py b/python/samples/getting_started/agents/custom/custom_agent.py index 4bdc0d79e3..cc3c376964 100644 --- a/python/samples/getting_started/agents/custom/custom_agent.py +++ b/python/samples/getting_started/agents/custom/custom_agent.py @@ -11,8 +11,6 @@ from agent_framework import ( BaseAgent, ChatMessage, Content, - Role, - tool, ) """ @@ -77,8 +75,8 @@ class EchoAgent(BaseAgent): if not normalized_messages: response_message = ChatMessage( - role=Role.ASSISTANT, - contents=[Content.from_text(text="Hello! I'm a custom echo agent. Send me a message and I'll echo it back.")], + "assistant", + [Content.from_text(text="Hello! I'm a custom echo agent. Send me a message and I'll echo it back.")], ) else: # For simplicity, echo the last user message @@ -88,7 +86,7 @@ class EchoAgent(BaseAgent): else: echo_text = f"{self.echo_prefix}[Non-text message received]" - response_message = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=echo_text)]) + response_message = ChatMessage("assistant", [Content.from_text(text=echo_text)]) # Notify the thread of new messages if provided if thread is not None: @@ -134,7 +132,7 @@ class EchoAgent(BaseAgent): yield AgentResponseUpdate( contents=[Content.from_text(text=chunk_text)], - role=Role.ASSISTANT, + role="assistant", ) # Small delay to simulate streaming @@ -142,7 +140,7 @@ class EchoAgent(BaseAgent): # Notify the thread of the complete response if provided if thread is not None: - complete_response = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)]) + complete_response = ChatMessage("assistant", [Content.from_text(text=response_text)]) await self._notify_thread_of_new_messages(thread, normalized_messages, complete_response) diff --git a/python/samples/getting_started/agents/custom/custom_chat_client.py b/python/samples/getting_started/agents/custom/custom_chat_client.py index c7df328b00..a6c38fcbca 100644 --- a/python/samples/getting_started/agents/custom/custom_chat_client.py +++ b/python/samples/getting_started/agents/custom/custom_chat_client.py @@ -12,10 +12,8 @@ from agent_framework import ( ChatResponse, ChatResponseUpdate, Content, - Role, use_chat_middleware, use_function_invocation, - tool, ) from agent_framework._clients import TOptions_co @@ -68,7 +66,7 @@ class EchoingChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]): # Echo the last user message last_user_message = None for message in reversed(messages): - if message.role == Role.USER: + if message.role == "user": last_user_message = message break @@ -77,7 +75,7 @@ class EchoingChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]): else: response_text = f"{self.prefix} [No text message found]" - response_message = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)]) + response_message = ChatMessage("assistant", [Content.from_text(text=response_text)]) return ChatResponse( messages=[response_message], @@ -104,7 +102,7 @@ class EchoingChatClient(BaseChatClient[TOptions_co], Generic[TOptions_co]): for char in response_text: yield ChatResponseUpdate( contents=[Content.from_text(text=char)], - role=Role.ASSISTANT, + role="assistant", response_id=f"echo-stream-resp-{random.randint(1000, 9999)}", model_id="echo-model-v1", ) diff --git a/python/samples/getting_started/agents/ollama/ollama_agent_basic.py b/python/samples/getting_started/agents/ollama/ollama_agent_basic.py index afe6700083..80b17e3b39 100644 --- a/python/samples/getting_started/agents/ollama/ollama_agent_basic.py +++ b/python/samples/getting_started/agents/ollama/ollama_agent_basic.py @@ -3,8 +3,8 @@ import asyncio from datetime import datetime -from agent_framework.ollama import OllamaChatClient from agent_framework import tool +from agent_framework.ollama import OllamaChatClient """ Ollama Agent Basic Example @@ -18,6 +18,7 @@ https://ollama.com/ """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_time(location: str) -> str: diff --git a/python/samples/getting_started/agents/ollama/ollama_chat_client.py b/python/samples/getting_started/agents/ollama/ollama_chat_client.py index d22fd737f7..67c71ff249 100644 --- a/python/samples/getting_started/agents/ollama/ollama_chat_client.py +++ b/python/samples/getting_started/agents/ollama/ollama_chat_client.py @@ -3,8 +3,8 @@ import asyncio from datetime import datetime -from agent_framework.ollama import OllamaChatClient from agent_framework import tool +from agent_framework.ollama import OllamaChatClient """ Ollama Chat Client Example @@ -18,6 +18,7 @@ https://ollama.com/ """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_time(): diff --git a/python/samples/getting_started/agents/ollama/ollama_chat_multimodal.py b/python/samples/getting_started/agents/ollama/ollama_chat_multimodal.py index c78053a91a..3deb6f6e92 100644 --- a/python/samples/getting_started/agents/ollama/ollama_chat_multimodal.py +++ b/python/samples/getting_started/agents/ollama/ollama_chat_multimodal.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatMessage, Content, Role +from agent_framework import ChatMessage, Content from agent_framework.ollama import OllamaChatClient """ @@ -33,7 +33,7 @@ async def test_image() -> None: image_uri = create_sample_image() message = ChatMessage( - role=Role.USER, + role="user", contents=[ Content.from_text(text="What's in this image?"), Content.from_uri(uri=image_uri, media_type="image/png"), diff --git a/python/samples/getting_started/agents/ollama/ollama_with_openai_chat_client.py b/python/samples/getting_started/agents/ollama/ollama_with_openai_chat_client.py index 47f58cd6e7..b555b7789f 100644 --- a/python/samples/getting_started/agents/ollama/ollama_with_openai_chat_client.py +++ b/python/samples/getting_started/agents/ollama/ollama_with_openai_chat_client.py @@ -5,8 +5,8 @@ import os from random import randint from typing import Annotated -from agent_framework.openai import OpenAIChatClient from agent_framework import tool +from agent_framework.openai import OpenAIChatClient """ Ollama with OpenAI Chat Client Example @@ -20,6 +20,7 @@ Environment Variables: - OLLAMA_MODEL: The model name to use (e.g., "mistral", "llama3.2", "phi3") """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_assistants_basic.py b/python/samples/getting_started/agents/openai/openai_assistants_basic.py index bf52405218..eb267b4a88 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_basic.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_basic.py @@ -5,10 +5,10 @@ import os from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.openai import OpenAIAssistantProvider from openai import AsyncOpenAI from pydantic import Field -from agent_framework import tool """ OpenAI Assistants Basic Example @@ -17,6 +17,7 @@ This sample demonstrates basic usage of OpenAIAssistantProvider 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_assistants_provider_methods.py b/python/samples/getting_started/agents/openai/openai_assistants_provider_methods.py index 55e1110075..1c3ed11642 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_provider_methods.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_provider_methods.py @@ -5,10 +5,10 @@ import os from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.openai import OpenAIAssistantProvider from openai import AsyncOpenAI from pydantic import Field -from agent_framework import tool """ OpenAI Assistant Provider Methods Example @@ -19,6 +19,7 @@ This sample demonstrates the methods available on the OpenAIAssistantProvider cl - as_agent(): Wrap an SDK Assistant object without making HTTP calls """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_assistants_with_existing_assistant.py b/python/samples/getting_started/agents/openai/openai_assistants_with_existing_assistant.py index 827d8c412c..b004253796 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_with_existing_assistant.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_with_existing_assistant.py @@ -5,10 +5,10 @@ import os from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.openai import OpenAIAssistantProvider from openai import AsyncOpenAI from pydantic import Field -from agent_framework import tool """ OpenAI Assistants with Existing Assistant Example @@ -17,6 +17,7 @@ This sample demonstrates working with pre-existing OpenAI Assistants using the provider's get_agent() and as_agent() methods. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_assistants_with_explicit_settings.py b/python/samples/getting_started/agents/openai/openai_assistants_with_explicit_settings.py index 53afefa5e9..70622f714b 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_with_explicit_settings.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_with_explicit_settings.py @@ -5,10 +5,10 @@ import os from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.openai import OpenAIAssistantProvider from openai import AsyncOpenAI from pydantic import Field -from agent_framework import tool """ OpenAI Assistants with Explicit Settings Example @@ -17,6 +17,7 @@ This sample demonstrates creating 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_assistants_with_function_tools.py b/python/samples/getting_started/agents/openai/openai_assistants_with_function_tools.py index bf75affc55..fe4b3d3b4e 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_with_function_tools.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_with_function_tools.py @@ -6,10 +6,10 @@ from datetime import datetime, timezone from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.openai import OpenAIAssistantProvider from openai import AsyncOpenAI from pydantic import Field -from agent_framework import tool """ OpenAI Assistants with Function Tools Example @@ -18,6 +18,7 @@ This sample demonstrates function tool integration with 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( @@ -27,6 +28,7 @@ def get_weather( 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.""" diff --git a/python/samples/getting_started/agents/openai/openai_assistants_with_response_format.py b/python/samples/getting_started/agents/openai/openai_assistants_with_response_format.py index e48338b558..0719ecc7de 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_with_response_format.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_with_response_format.py @@ -59,13 +59,14 @@ async def main() -> None: result1 = await agent.run(query1) - if weather := result1.try_parse_value(WeatherInfo): + 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}") - else: + except Exception: print(f"Failed to parse response: {result1.text}") # Request 2: Override response_format at runtime with CityInfo @@ -75,12 +76,13 @@ async def main() -> None: result2 = await agent.run(query2, options={"response_format": CityInfo}) - if city := result2.try_parse_value(CityInfo): + try: + city = result2.value print("Agent:") print(f" City: {city.city_name}") print(f" Population: {city.population}") print(f" Country: {city.country}") - else: + except Exception: print(f"Failed to parse response: {result2.text}") finally: await client.beta.assistants.delete(agent.id) diff --git a/python/samples/getting_started/agents/openai/openai_assistants_with_thread.py b/python/samples/getting_started/agents/openai/openai_assistants_with_thread.py index d3b167ebdd..02b8086199 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_with_thread.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_with_thread.py @@ -5,8 +5,7 @@ import os from random import randint from typing import Annotated -from agent_framework import AgentThread -from agent_framework import tool +from agent_framework import AgentThread, tool from agent_framework.openai import OpenAIAssistantProvider from openai import AsyncOpenAI from pydantic import Field @@ -18,6 +17,7 @@ This sample demonstrates thread management with OpenAI Assistants, showing persistent conversation threads and context preservation across interactions. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_basic.py b/python/samples/getting_started/agents/openai/openai_chat_client_basic.py index 6c1a94760d..49cfb29447 100644 --- a/python/samples/getting_started/agents/openai/openai_chat_client_basic.py +++ b/python/samples/getting_started/agents/openai/openai_chat_client_basic.py @@ -4,8 +4,8 @@ import asyncio from random import randint from typing import Annotated -from agent_framework.openai import OpenAIChatClient from agent_framework import tool +from agent_framework.openai import OpenAIChatClient """ OpenAI Chat Client Basic Example @@ -14,6 +14,7 @@ This sample demonstrates basic usage of OpenAIChatClient for direct chat-based interactions, showing both streaming and non-streaming responses. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_with_explicit_settings.py b/python/samples/getting_started/agents/openai/openai_chat_client_with_explicit_settings.py index 1302841ecf..0bac0b863c 100644 --- a/python/samples/getting_started/agents/openai/openai_chat_client_with_explicit_settings.py +++ b/python/samples/getting_started/agents/openai/openai_chat_client_with_explicit_settings.py @@ -5,9 +5,9 @@ import os from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.openai import OpenAIChatClient from pydantic import Field -from agent_framework import tool """ OpenAI Chat Client with Explicit Settings Example @@ -16,6 +16,7 @@ This sample demonstrates creating OpenAI Chat Client 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_with_function_tools.py b/python/samples/getting_started/agents/openai/openai_chat_client_with_function_tools.py index 3fa7fd9e8a..057989d228 100644 --- a/python/samples/getting_started/agents/openai/openai_chat_client_with_function_tools.py +++ b/python/samples/getting_started/agents/openai/openai_chat_client_with_function_tools.py @@ -5,8 +5,7 @@ from datetime import datetime, timezone from random import randint from typing import Annotated -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.openai import OpenAIChatClient from pydantic import Field @@ -17,6 +16,7 @@ This sample demonstrates function tool integration with OpenAI Chat Client, 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( @@ -26,6 +26,7 @@ def get_weather( 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.""" diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_with_thread.py b/python/samples/getting_started/agents/openai/openai_chat_client_with_thread.py index 0c6595ca16..f7a824c370 100644 --- a/python/samples/getting_started/agents/openai/openai_chat_client_with_thread.py +++ b/python/samples/getting_started/agents/openai/openai_chat_client_with_thread.py @@ -4,8 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread, ChatAgent, ChatMessageStore -from agent_framework import tool +from agent_framework import AgentThread, ChatAgent, ChatMessageStore, tool from agent_framework.openai import OpenAIChatClient from pydantic import Field @@ -16,6 +15,7 @@ This sample demonstrates thread management with OpenAI Chat Client, showing conversation threads and message history preservation across interactions. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_basic.py b/python/samples/getting_started/agents/openai/openai_responses_client_basic.py index c09a4c816a..4e7fcbf07d 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_basic.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_basic.py @@ -4,8 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.openai import OpenAIResponsesClient from pydantic import Field @@ -16,6 +15,7 @@ This sample demonstrates basic usage of OpenAIResponsesClient for structured response generation, showing both streaming and non-streaming responses. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py index 6988219dcc..5a73752bd9 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py @@ -8,7 +8,6 @@ from agent_framework import ( CodeInterpreterToolResultContent, Content, HostedCodeInterpreterTool, - tool, ) from agent_framework.openai import OpenAIResponsesClient diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_explicit_settings.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_explicit_settings.py index fa5583f296..826fd880bf 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_explicit_settings.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_explicit_settings.py @@ -5,9 +5,9 @@ import os from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.openai import OpenAIResponsesClient from pydantic import Field -from agent_framework import tool """ OpenAI Responses Client with Explicit Settings Example @@ -16,6 +16,7 @@ This sample demonstrates creating OpenAI Responses Client with explicit configur settings rather than relying on environment variable defaults. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_function_tools.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_function_tools.py index d18a522406..032a8b20d8 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_function_tools.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_function_tools.py @@ -5,8 +5,7 @@ from datetime import datetime, timezone from random import randint from typing import Annotated -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.openai import OpenAIResponsesClient from pydantic import Field @@ -17,6 +16,7 @@ This sample demonstrates function tool integration with OpenAI Responses Client, 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/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( @@ -26,6 +26,7 @@ def get_weather( 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.""" diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py index e5fa62f040..264971d8e7 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py @@ -29,10 +29,10 @@ async def handle_approvals_without_thread(query: str, agent: "AgentProtocol"): 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(ChatMessage(role="assistant", contents=[user_input_needed])) + new_inputs.append(ChatMessage("assistant", [user_input_needed])) user_approval = input("Approve function call? (y/n): ") new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) + ChatMessage("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) result = await agent.run(new_inputs) @@ -70,7 +70,7 @@ async def handle_approvals_with_thread_streaming(query: str, agent: "AgentProtoc new_input_added = True while new_input_added: new_input_added = False - new_input.append(ChatMessage(role="user", text=query)) + new_input.append(ChatMessage("user", [query])) async for update in agent.run_stream(new_input, thread=thread, store=True): if update.user_input_requests: for user_input_needed in update.user_input_requests: diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_thread.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_thread.py index 6a7fc71efc..e17c2d2748 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_thread.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_thread.py @@ -4,8 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread, ChatAgent -from agent_framework import tool +from agent_framework import AgentThread, ChatAgent, tool from agent_framework.openai import OpenAIResponsesClient from pydantic import Field @@ -16,6 +15,7 @@ This sample demonstrates thread management with OpenAI Responses Client, showing persistent conversation context and simplified response handling. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/azure_functions/02_multi_agent/function_app.py b/python/samples/getting_started/azure_functions/02_multi_agent/function_app.py index f7181cb4b1..cb735baecd 100644 --- a/python/samples/getting_started/azure_functions/02_multi_agent/function_app.py +++ b/python/samples/getting_started/azure_functions/02_multi_agent/function_app.py @@ -11,12 +11,13 @@ Prerequisites: set `AZURE_OPENAI_ENDPOINT` and `AZURE_OPENAI_CHAT_DEPLOYMENT_NAM import logging from typing import Any +from agent_framework import tool from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient from azure.identity import AzureCliCredential -from agent_framework import tool logger = logging.getLogger(__name__) + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather(location: str) -> dict[str, Any]: @@ -32,6 +33,7 @@ def get_weather(location: str) -> dict[str, Any]: logger.info(f"✓ [TOOL RESULT] {result}") return result + @tool(approval_mode="never_require") def calculate_tip(bill_amount: float, tip_percentage: float = 15.0) -> dict[str, Any]: """Calculate tip amount and total bill.""" diff --git a/python/samples/getting_started/azure_functions/03_reliable_streaming/redis_stream_response_handler.py b/python/samples/getting_started/azure_functions/03_reliable_streaming/redis_stream_response_handler.py index e6d60735bf..ff9f544062 100644 --- a/python/samples/getting_started/azure_functions/03_reliable_streaming/redis_stream_response_handler.py +++ b/python/samples/getting_started/azure_functions/03_reliable_streaming/redis_stream_response_handler.py @@ -8,9 +8,9 @@ as a message broker. It enables clients to disconnect and reconnect without losi import asyncio import time +from collections.abc import AsyncIterator from dataclasses import dataclass from datetime import timedelta -from collections.abc import AsyncIterator import redis.asyncio as aioredis diff --git a/python/samples/getting_started/azure_functions/03_reliable_streaming/tools.py b/python/samples/getting_started/azure_functions/03_reliable_streaming/tools.py index 6a71fdfa03..29be74a846 100644 --- a/python/samples/getting_started/azure_functions/03_reliable_streaming/tools.py +++ b/python/samples/getting_started/azure_functions/03_reliable_streaming/tools.py @@ -153,13 +153,12 @@ def _get_weather_recommendation(condition: str) -> str: if "rain" in condition_lower or "drizzle" in condition_lower: return "Bring an umbrella and waterproof jacket. Consider indoor activities for backup." - elif "fog" in condition_lower: + if "fog" in condition_lower: return "Morning visibility may be limited. Plan outdoor sightseeing for afternoon." - elif "cold" in condition_lower: + if "cold" in condition_lower: return "Layer up with warm clothing. Hot drinks and cozy cafés recommended." - elif "hot" in condition_lower or "warm" in condition_lower: + if "hot" in condition_lower or "warm" in condition_lower: return "Stay hydrated and use sunscreen. Plan strenuous activities for cooler morning hours." - elif "thunder" in condition_lower or "storm" in condition_lower: + if "thunder" in condition_lower or "storm" in condition_lower: return "Keep an eye on weather updates. Have indoor alternatives ready." - else: - return "Pleasant conditions expected. Great day for outdoor exploration!" + return "Pleasant conditions expected. Great day for outdoor exploration!" diff --git a/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py b/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py index ea373e588a..1165f0cc8e 100644 --- a/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py +++ b/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py @@ -102,9 +102,10 @@ def spam_detection_orchestration(context: DurableOrchestrationContext) -> Genera options={"response_format": SpamDetectionResult}, ) - spam_result = spam_result_raw.try_parse_value(SpamDetectionResult) - if spam_result is None: - raise ValueError("Failed to parse spam detection result") + try: + spam_result = spam_result_raw.value + except Exception as ex: + raise ValueError("Failed to parse spam detection result") from ex if spam_result.is_spam: result = yield context.call_activity("handle_spam_email", spam_result.reason) # type: ignore[misc] @@ -125,9 +126,10 @@ def spam_detection_orchestration(context: DurableOrchestrationContext) -> Genera options={"response_format": EmailResponse}, ) - email_result = email_result_raw.try_parse_value(EmailResponse) - if email_result is None: - raise ValueError("Failed to parse email response") + try: + email_result = email_result_raw.value + except Exception as ex: + raise ValueError("Failed to parse email response") from ex result = yield context.call_activity("send_email", email_result.response) # type: ignore[misc] return result diff --git a/python/samples/getting_started/azure_functions/07_single_agent_orchestration_hitl/function_app.py b/python/samples/getting_started/azure_functions/07_single_agent_orchestration_hitl/function_app.py index 1b55620233..6ed85081bc 100644 --- a/python/samples/getting_started/azure_functions/07_single_agent_orchestration_hitl/function_app.py +++ b/python/samples/getting_started/azure_functions/07_single_agent_orchestration_hitl/function_app.py @@ -137,11 +137,11 @@ def content_generation_hitl_orchestration(context: DurableOrchestrationContext) context.set_custom_status( "Content rejected by human reviewer. Incorporating feedback and regenerating..." ) - + # Check if we've exhausted attempts if attempt >= payload.max_review_attempts: break - + rewrite_prompt = ( "The content was rejected by a human reviewer. Please rewrite the article incorporating their feedback.\n\n" f"Human Feedback: {approval_payload.feedback or 'No feedback provided.'}" @@ -152,9 +152,10 @@ def content_generation_hitl_orchestration(context: DurableOrchestrationContext) options={"response_format": GeneratedContent}, ) - content = rewritten_raw.try_parse_value(GeneratedContent) - if content is None: - raise ValueError("Agent returned no content after rewrite.") + try: + content = rewritten_raw.value + except Exception as ex: + raise ValueError("Agent returned no content after rewrite.") from ex else: context.set_custom_status( f"Human approval timed out after {payload.approval_timeout_hours} hour(s). Treating as rejection." @@ -162,7 +163,7 @@ def content_generation_hitl_orchestration(context: DurableOrchestrationContext) raise TimeoutError( f"Human approval timed out after {payload.approval_timeout_hours} hour(s)." ) - + # If we exit the loop without returning, max attempts were exhausted context.set_custom_status("Max review attempts exhausted.") raise RuntimeError( diff --git a/python/samples/getting_started/chat_client/azure_ai_chat_client.py b/python/samples/getting_started/chat_client/azure_ai_chat_client.py index ab502b8f35..97aa015f13 100644 --- a/python/samples/getting_started/chat_client/azure_ai_chat_client.py +++ b/python/samples/getting_started/chat_client/azure_ai_chat_client.py @@ -4,10 +4,10 @@ import asyncio from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.azure import AzureAIAgentClient from azure.identity.aio import AzureCliCredential from pydantic import Field -from agent_framework import tool """ Azure AI Chat Client Direct Usage Example @@ -16,6 +16,7 @@ Demonstrates direct AzureAIChatClient usage for chat interactions with Azure AI Shows function calling capabilities with custom business logic. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/chat_client/azure_assistants_client.py b/python/samples/getting_started/chat_client/azure_assistants_client.py index 1a40696bd5..99f4de5b9c 100644 --- a/python/samples/getting_started/chat_client/azure_assistants_client.py +++ b/python/samples/getting_started/chat_client/azure_assistants_client.py @@ -4,10 +4,10 @@ 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 pydantic import Field -from agent_framework import tool """ Azure Assistants Client Direct Usage Example @@ -16,6 +16,7 @@ Demonstrates direct AzureAssistantsClient usage for chat interactions with Azure Shows function calling capabilities and automatic assistant creation. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/chat_client/azure_chat_client.py b/python/samples/getting_started/chat_client/azure_chat_client.py index 211fc6d869..77b3358a39 100644 --- a/python/samples/getting_started/chat_client/azure_chat_client.py +++ b/python/samples/getting_started/chat_client/azure_chat_client.py @@ -4,10 +4,10 @@ import asyncio from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential from pydantic import Field -from agent_framework import tool """ Azure Chat Client Direct Usage Example @@ -16,6 +16,7 @@ Demonstrates direct AzureChatClient usage for chat interactions with Azure OpenA Shows function calling capabilities with custom business logic. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/chat_client/azure_responses_client.py b/python/samples/getting_started/chat_client/azure_responses_client.py index 050225e559..17a1ab335a 100644 --- a/python/samples/getting_started/chat_client/azure_responses_client.py +++ b/python/samples/getting_started/chat_client/azure_responses_client.py @@ -4,8 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import ChatResponse -from agent_framework import tool +from agent_framework import ChatResponse, tool from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential from pydantic import BaseModel, Field @@ -17,6 +16,7 @@ Demonstrates direct AzureResponsesClient usage for structured response generatio Shows function calling capabilities with custom business logic. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( @@ -42,19 +42,21 @@ async def main() -> None: stream = True print(f"User: {message}") if stream: - response = await ChatResponse.from_chat_response_generator( + response = await ChatResponse.from_update_generator( client.get_streaming_response(message, tools=get_weather, options={"response_format": OutputStruct}), output_format_type=OutputStruct, ) - if result := response.try_parse_value(OutputStruct): + try: + result = response.value print(f"Assistant: {result}") - else: + except Exception: print(f"Assistant: {response.text}") else: response = await client.get_response(message, tools=get_weather, options={"response_format": OutputStruct}) - if result := response.try_parse_value(OutputStruct): + try: + result = response.value print(f"Assistant: {result}") - else: + except Exception: print(f"Assistant: {response.text}") diff --git a/python/samples/getting_started/chat_client/openai_assistants_client.py b/python/samples/getting_started/chat_client/openai_assistants_client.py index b4dc03ea71..88aec44ed2 100644 --- a/python/samples/getting_started/chat_client/openai_assistants_client.py +++ b/python/samples/getting_started/chat_client/openai_assistants_client.py @@ -4,9 +4,9 @@ import asyncio from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.openai import OpenAIAssistantsClient from pydantic import Field -from agent_framework import tool """ OpenAI Assistants Client Direct Usage Example @@ -16,6 +16,7 @@ Shows function calling capabilities and automatic assistant creation. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/chat_client/openai_chat_client.py b/python/samples/getting_started/chat_client/openai_chat_client.py index f45f17d71f..da50ae59bf 100644 --- a/python/samples/getting_started/chat_client/openai_chat_client.py +++ b/python/samples/getting_started/chat_client/openai_chat_client.py @@ -4,9 +4,9 @@ import asyncio from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.openai import OpenAIChatClient from pydantic import Field -from agent_framework import tool """ OpenAI Chat Client Direct Usage Example @@ -16,6 +16,7 @@ Shows function calling capabilities with custom business logic. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/chat_client/openai_responses_client.py b/python/samples/getting_started/chat_client/openai_responses_client.py index 2c5f3953e9..c9d476faa3 100644 --- a/python/samples/getting_started/chat_client/openai_responses_client.py +++ b/python/samples/getting_started/chat_client/openai_responses_client.py @@ -4,9 +4,9 @@ import asyncio from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.openai import OpenAIResponsesClient from pydantic import Field -from agent_framework import tool """ OpenAI Responses Client Direct Usage Example @@ -16,6 +16,7 @@ Shows function calling capabilities with custom business logic. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( diff --git a/python/samples/getting_started/context_providers/mem0/mem0_basic.py b/python/samples/getting_started/context_providers/mem0/mem0_basic.py index a163b000e8..b82dab0ae1 100644 --- a/python/samples/getting_started/context_providers/mem0/mem0_basic.py +++ b/python/samples/getting_started/context_providers/mem0/mem0_basic.py @@ -3,10 +3,11 @@ import asyncio import uuid +from agent_framework import tool from agent_framework.azure import AzureAIAgentClient from agent_framework.mem0 import Mem0Provider from azure.identity.aio import AzureCliCredential -from agent_framework import tool + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") diff --git a/python/samples/getting_started/context_providers/mem0/mem0_oss.py b/python/samples/getting_started/context_providers/mem0/mem0_oss.py index 1f5591c004..84156434b0 100644 --- a/python/samples/getting_started/context_providers/mem0/mem0_oss.py +++ b/python/samples/getting_started/context_providers/mem0/mem0_oss.py @@ -3,11 +3,12 @@ import asyncio import uuid +from agent_framework import tool from agent_framework.azure import AzureAIAgentClient from agent_framework.mem0 import Mem0Provider from azure.identity.aio import AzureCliCredential from mem0 import AsyncMemory -from agent_framework import tool + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") diff --git a/python/samples/getting_started/context_providers/mem0/mem0_threads.py b/python/samples/getting_started/context_providers/mem0/mem0_threads.py index ea0375495a..15a57ad796 100644 --- a/python/samples/getting_started/context_providers/mem0/mem0_threads.py +++ b/python/samples/getting_started/context_providers/mem0/mem0_threads.py @@ -3,10 +3,11 @@ import asyncio import uuid +from agent_framework import tool from agent_framework.azure import AzureAIAgentClient from agent_framework.mem0 import Mem0Provider from azure.identity.aio import AzureCliCredential -from agent_framework import tool + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") diff --git a/python/samples/getting_started/context_providers/redis/redis_basics.py b/python/samples/getting_started/context_providers/redis/redis_basics.py index afbe835cc0..9f5a654ea1 100644 --- a/python/samples/getting_started/context_providers/redis/redis_basics.py +++ b/python/samples/getting_started/context_providers/redis/redis_basics.py @@ -30,13 +30,13 @@ Run: import asyncio import os -from agent_framework import ChatMessage, Role -from agent_framework import tool +from agent_framework import ChatMessage, tool from agent_framework.openai import OpenAIChatClient from agent_framework_redis._provider import RedisProvider from redisvl.extensions.cache.embeddings import EmbeddingsCache from redisvl.utils.vectorize import OpenAITextVectorizer + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def search_flights(origin_airport_code: str, destination_airport_code: str, detailed: bool = False) -> str: @@ -128,9 +128,9 @@ async def main() -> None: # Build sample chat messages to persist to Redis messages = [ - ChatMessage(role=Role.USER, text="runA CONVO: User Message"), - ChatMessage(role=Role.ASSISTANT, text="runA CONVO: Assistant Message"), - ChatMessage(role=Role.SYSTEM, text="runA CONVO: System Message"), + ChatMessage("user", ["runA CONVO: User Message"]), + ChatMessage("assistant", ["runA CONVO: Assistant Message"]), + ChatMessage("system", ["runA CONVO: System Message"]), ] # Declare/start a conversation/thread and write messages under 'runA'. @@ -142,7 +142,7 @@ async def main() -> None: # Retrieve relevant memories for a hypothetical model call. The provider uses # the current request messages as the retrieval query and returns context to # be injected into the model's instructions. - ctx = await provider.invoking([ChatMessage(role=Role.SYSTEM, text="B: Assistant Message")]) + ctx = await provider.invoking([ChatMessage("system", ["B: Assistant Message"])]) # Inspect retrieved memories that would be injected into instructions # (Debug-only output so you can verify retrieval works as expected.) diff --git a/python/samples/getting_started/context_providers/simple_context_provider.py b/python/samples/getting_started/context_providers/simple_context_provider.py index e85de6aced..e32266cb14 100644 --- a/python/samples/getting_started/context_providers/simple_context_provider.py +++ b/python/samples/getting_started/context_providers/simple_context_provider.py @@ -39,7 +39,7 @@ class UserInfoMemory(ContextProvider): ) -> None: """Extract user information from messages after each agent call.""" # Check if we need to extract user info from user messages - user_messages = [msg for msg in request_messages if hasattr(msg, "role") and msg.role.value == "user"] # type: ignore + user_messages = [msg for msg in request_messages if hasattr(msg, "role") and msg.role == "user"] # type: ignore if (self.user_info.name is None or self.user_info.age is None) and user_messages: try: @@ -52,11 +52,14 @@ class UserInfoMemory(ContextProvider): ) # Update user info with extracted data - if extracted := result.try_parse_value(UserInfo): + try: + extracted = result.value if self.user_info.name is None and extracted.name: self.user_info.name = extracted.name if self.user_info.age is None and extracted.age: self.user_info.age = extracted.age + except Exception: + pass # Failed to extract, continue without updating except Exception: pass # Failed to extract, continue without updating diff --git a/python/samples/getting_started/declarative/azure_openai_responses_agent.py b/python/samples/getting_started/declarative/azure_openai_responses_agent.py index 1dbcc6adea..edcf0f0805 100644 --- a/python/samples/getting_started/declarative/azure_openai_responses_agent.py +++ b/python/samples/getting_started/declarative/azure_openai_responses_agent.py @@ -20,10 +20,11 @@ async def main(): agent = AgentFactory(client_kwargs={"credential": AzureCliCredential()}).create_agent_from_yaml(yaml_str) # use the agent response = await agent.run("Why is the sky blue, answer in Dutch?") - # Use try_parse_value() for safe parsing - returns None if no response_format or parsing fails - if parsed := response.try_parse_value(): + # Use response.value with try/except for safe parsing + try: + parsed = response.value print("Agent response:", parsed.model_dump_json(indent=2)) - else: + except Exception: print("Agent response:", response.text) diff --git a/python/samples/getting_started/declarative/openai_responses_agent.py b/python/samples/getting_started/declarative/openai_responses_agent.py index ed2cc89d08..2931168587 100644 --- a/python/samples/getting_started/declarative/openai_responses_agent.py +++ b/python/samples/getting_started/declarative/openai_responses_agent.py @@ -19,10 +19,11 @@ async def main(): agent = AgentFactory().create_agent_from_yaml(yaml_str) # use the agent response = await agent.run("Why is the sky blue, answer in Dutch?") - # Use try_parse_value() for safe parsing - returns None if no response_format or parsing fails - if parsed := response.try_parse_value(): + # Use response.value with try/except for safe parsing + try: + parsed = response.value print("Agent response:", parsed) - else: + except Exception: print("Agent response:", response.text) diff --git a/python/samples/getting_started/devui/fanout_workflow/workflow.py b/python/samples/getting_started/devui/fanout_workflow/workflow.py index bb84c28db7..fa9d4edd92 100644 --- a/python/samples/getting_started/devui/fanout_workflow/workflow.py +++ b/python/samples/getting_started/devui/fanout_workflow/workflow.py @@ -25,7 +25,6 @@ from agent_framework import ( WorkflowBuilder, WorkflowContext, handler, - tool, ) from pydantic import BaseModel, Field from typing_extensions import Never diff --git a/python/samples/getting_started/devui/foundry_agent/agent.py b/python/samples/getting_started/devui/foundry_agent/agent.py index 9091bb2d7e..f2ce12058d 100644 --- a/python/samples/getting_started/devui/foundry_agent/agent.py +++ b/python/samples/getting_started/devui/foundry_agent/agent.py @@ -8,12 +8,12 @@ Make sure to run 'az login' before starting devui. import os from typing import Annotated -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.azure import AzureAIAgentClient from azure.identity.aio import AzureCliCredential from pydantic import Field + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( @@ -24,6 +24,7 @@ def get_weather( temperature = 22 return f"The weather in {location} is {conditions[0]} with a high of {temperature}°C." + @tool(approval_mode="never_require") def get_forecast( location: Annotated[str, Field(description="The location to get the forecast for.")], diff --git a/python/samples/getting_started/devui/in_memory_mode.py b/python/samples/getting_started/devui/in_memory_mode.py index 597f9babf3..e8441e9eb5 100644 --- a/python/samples/getting_started/devui/in_memory_mode.py +++ b/python/samples/getting_started/devui/in_memory_mode.py @@ -10,8 +10,7 @@ import logging import os from typing import Annotated -from agent_framework import ChatAgent, Executor, WorkflowBuilder, WorkflowContext, handler -from agent_framework import tool +from agent_framework import ChatAgent, Executor, WorkflowBuilder, WorkflowContext, handler, tool from agent_framework.azure import AzureOpenAIChatClient from agent_framework.devui import serve from typing_extensions import Never @@ -29,6 +28,7 @@ def get_weather( temperature = 53 return f"The weather in {location} is {conditions[0]} with a high of {temperature}°C." + @tool(approval_mode="never_require") def get_time( timezone: Annotated[str, "The timezone to get time for."] = "UTC", diff --git a/python/samples/getting_started/devui/spam_workflow/workflow.py b/python/samples/getting_started/devui/spam_workflow/workflow.py index 54cf6265ca..73be349cc6 100644 --- a/python/samples/getting_started/devui/spam_workflow/workflow.py +++ b/python/samples/getting_started/devui/spam_workflow/workflow.py @@ -27,7 +27,6 @@ from agent_framework import ( WorkflowContext, handler, response_handler, - tool, ) from pydantic import BaseModel, Field from typing_extensions import Never diff --git a/python/samples/getting_started/devui/weather_agent_azure/agent.py b/python/samples/getting_started/devui/weather_agent_azure/agent.py index cc2992d0c5..71525c24a1 100644 --- a/python/samples/getting_started/devui/weather_agent_azure/agent.py +++ b/python/samples/getting_started/devui/weather_agent_azure/agent.py @@ -14,10 +14,9 @@ from agent_framework import ( ChatResponseUpdate, Content, FunctionInvocationContext, - Role, - tool, chat_middleware, function_middleware, + tool, ) from agent_framework.azure import AzureOpenAIChatClient from agent_framework_devui import register_cleanup @@ -43,7 +42,7 @@ async def security_filter_middleware( # Check only the last message (most recent user input) last_message = context.messages[-1] if context.messages else None - if last_message and last_message.role == Role.USER and last_message.text: + if last_message and last_message.role == "user" and last_message.text: message_lower = last_message.text.lower() for term in blocked_terms: if term in message_lower: @@ -58,7 +57,7 @@ async def security_filter_middleware( async def blocked_stream() -> AsyncIterable[ChatResponseUpdate]: yield ChatResponseUpdate( contents=[Content.from_text(text=error_message)], - role=Role.ASSISTANT, + role="assistant", ) context.result = blocked_stream() @@ -67,7 +66,7 @@ async def security_filter_middleware( context.result = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", text=error_message, ) ] diff --git a/python/samples/getting_started/durabletask/01_single_agent/client.py b/python/samples/getting_started/durabletask/01_single_agent/client.py index 7c8b27d80c..71d897e1f2 100644 --- a/python/samples/getting_started/durabletask/01_single_agent/client.py +++ b/python/samples/getting_started/durabletask/01_single_agent/client.py @@ -40,12 +40,12 @@ def get_client( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + dts_client = DurableTaskSchedulerClient( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -53,7 +53,7 @@ def get_client( token_credential=credential, log_handler=log_handler ) - + return DurableAIAgentClient(dts_client) @@ -66,12 +66,12 @@ def run_client(agent_client: DurableAIAgentClient) -> None: # Get a reference to the Joker agent logger.debug("Getting reference to Joker agent...") joker = agent_client.get_agent("Joker") - + # Create a new thread for the conversation thread = joker.get_new_thread() logger.debug(f"Thread ID: {thread.session_id}") logger.info("Start chatting with the Joker agent! (Type 'exit' to quit)") - + # Interactive conversation loop while True: # Get user input @@ -80,33 +80,33 @@ def run_client(agent_client: DurableAIAgentClient) -> None: except (EOFError, KeyboardInterrupt): logger.info("\nExiting...") break - + # Check for exit command if user_message.lower() == "exit": logger.info("Goodbye!") break - + # Skip empty messages if not user_message: continue - + # Send message to agent and get response try: response = joker.run(user_message, thread=thread) logger.info(f"Joker: {response.text} \n") except Exception as e: logger.error(f"Error getting response: {e}") - + logger.info("Conversation completed.") async def main() -> None: """Main entry point for the client application.""" logger.debug("Starting Durable Task Agent Client...") - + # Create client using helper function agent_client = get_client() - + try: run_client(agent_client) except Exception as e: diff --git a/python/samples/getting_started/durabletask/01_single_agent/sample.py b/python/samples/getting_started/durabletask/01_single_agent/sample.py index b8c39974c0..323549d8bf 100644 --- a/python/samples/getting_started/durabletask/01_single_agent/sample.py +++ b/python/samples/getting_started/durabletask/01_single_agent/sample.py @@ -15,40 +15,40 @@ To run this sample: import logging -from dotenv import load_dotenv - # Import helper functions from worker and client modules from client import get_client, run_client +from dotenv import load_dotenv from worker import get_worker, setup_worker # Configure logging (must be after imports to override their basicConfig) logging.basicConfig(level=logging.INFO, force=True) logger = logging.getLogger(__name__) + 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/getting_started/durabletask/01_single_agent/worker.py b/python/samples/getting_started/durabletask/01_single_agent/worker.py index 4b837a8a8e..03fc5a667f 100644 --- a/python/samples/getting_started/durabletask/01_single_agent/worker.py +++ b/python/samples/getting_started/durabletask/01_single_agent/worker.py @@ -52,12 +52,12 @@ def get_worker( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerWorker( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -78,42 +78,42 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker: """ # Wrap it with the agent worker agent_worker = DurableAIAgentWorker(worker) - + # Create and register the Joker agent logger.debug("Creating and registering Joker agent...") joker_agent = create_joker_agent() agent_worker.add_agent(joker_agent) - + logger.debug(f"✓ Registered agent: {joker_agent.name}") logger.debug(f" Entity name: dafx-{joker_agent.name}") - + return agent_worker async def main(): """Main entry point for the worker process.""" logger.debug("Starting Durable Task Agent Worker...") - + # Create a worker using the helper function worker = get_worker() - + # Setup worker with agents setup_worker(worker) - + logger.info("Worker is ready and listening for requests...") logger.info("Press Ctrl+C to stop.") logger.info("") - + try: # Start the worker (this blocks until stopped) worker.start() - + # Keep the worker running while True: await asyncio.sleep(1) except KeyboardInterrupt: logger.debug("Worker shutdown initiated") - + logger.debug("Worker stopped") diff --git a/python/samples/getting_started/durabletask/02_multi_agent/client.py b/python/samples/getting_started/durabletask/02_multi_agent/client.py index d7cecabd99..b3d8434062 100644 --- a/python/samples/getting_started/durabletask/02_multi_agent/client.py +++ b/python/samples/getting_started/durabletask/02_multi_agent/client.py @@ -41,12 +41,12 @@ def get_client( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + dts_client = DurableTaskSchedulerClient( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -54,7 +54,7 @@ def get_client( token_credential=credential, log_handler=log_handler ) - + return DurableAIAgentClient(dts_client) @@ -65,45 +65,45 @@ def run_client(agent_client: DurableAIAgentClient) -> None: agent_client: The DurableAIAgentClient instance """ logger.debug("Testing WeatherAgent") - + # Get reference to WeatherAgent weather_agent = agent_client.get_agent("WeatherAgent") weather_thread = weather_agent.get_new_thread() - + logger.debug(f"Created weather conversation thread: {weather_thread.session_id}") - + # Test WeatherAgent weather_message = "What is the weather in Seattle?" logger.info(f"User: {weather_message}") - + weather_response = weather_agent.run(weather_message, thread=weather_thread) logger.info(f"WeatherAgent: {weather_response.text} \n") - + logger.debug("Testing MathAgent") - + # Get reference to MathAgent math_agent = agent_client.get_agent("MathAgent") math_thread = math_agent.get_new_thread() - + logger.debug(f"Created math conversation thread: {math_thread.session_id}") - + # Test MathAgent math_message = "Calculate a 20% tip on a $50 bill" logger.info(f"User: {math_message}") - + math_response = math_agent.run(math_message, thread=math_thread) logger.info(f"MathAgent: {math_response.text} \n") - + logger.debug("Both agents completed successfully!") async def main() -> None: """Main entry point for the client application.""" logger.debug("Starting Durable Task Multi-Agent Client...") - + # Create client using helper function agent_client = get_client() - + try: run_client(agent_client) except Exception as e: diff --git a/python/samples/getting_started/durabletask/02_multi_agent/sample.py b/python/samples/getting_started/durabletask/02_multi_agent/sample.py index 9945601c20..17475ca06a 100644 --- a/python/samples/getting_started/durabletask/02_multi_agent/sample.py +++ b/python/samples/getting_started/durabletask/02_multi_agent/sample.py @@ -15,10 +15,9 @@ To run this sample: import logging -from dotenv import load_dotenv - # Import helper functions from worker and client modules from client import get_client, run_client +from dotenv import load_dotenv from worker import get_worker, setup_worker # Configure logging @@ -29,26 +28,26 @@ logger = logging.getLogger(__name__) 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/getting_started/durabletask/02_multi_agent/worker.py b/python/samples/getting_started/durabletask/02_multi_agent/worker.py index b0e51541b9..968d8fc997 100644 --- a/python/samples/getting_started/durabletask/02_multi_agent/worker.py +++ b/python/samples/getting_started/durabletask/02_multi_agent/worker.py @@ -101,12 +101,12 @@ def get_worker( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerWorker( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -127,43 +127,43 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker: """ # Wrap it with the agent worker agent_worker = DurableAIAgentWorker(worker) - + # Create and register both agents logger.debug("Creating and registering agents...") weather_agent = create_weather_agent() math_agent = create_math_agent() - + agent_worker.add_agent(weather_agent) agent_worker.add_agent(math_agent) - + logger.debug(f"✓ Registered agents: {weather_agent.name}, {math_agent.name}") - + return agent_worker async def main(): """Main entry point for the worker process.""" logger.debug("Starting Durable Task Multi-Agent Worker...") - + # Create a worker using the helper function worker = get_worker() - + # Setup worker with agents setup_worker(worker) - + logger.info("Worker is ready and listening for requests...") logger.info("Press Ctrl+C to stop. \n") - + try: # Start the worker (this blocks until stopped) worker.start() - + # Keep the worker running while True: await asyncio.sleep(1) except KeyboardInterrupt: logger.debug("Worker shutdown initiated") - + logger.info("Worker stopped") diff --git a/python/samples/getting_started/durabletask/03_single_agent_streaming/client.py b/python/samples/getting_started/durabletask/03_single_agent_streaming/client.py index 92c941d766..c017829dfb 100644 --- a/python/samples/getting_started/durabletask/03_single_agent_streaming/client.py +++ b/python/samples/getting_started/durabletask/03_single_agent_streaming/client.py @@ -23,7 +23,6 @@ import redis.asyncio as aioredis from agent_framework.azure import DurableAIAgentClient from azure.identity import DefaultAzureCredential from durabletask.azuremanaged.client import DurableTaskSchedulerClient - from redis_stream_response_handler import RedisStreamResponseHandler # Configure logging @@ -71,12 +70,12 @@ def get_client( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + dts_client = DurableTaskSchedulerClient( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -84,7 +83,7 @@ def get_client( token_credential=credential, log_handler=log_handler ) - + return DurableAIAgentClient(dts_client) @@ -100,33 +99,33 @@ async def stream_from_redis(thread_id: str, cursor: str | None = None) -> None: logger.debug(f"To manually check Redis, run: redis-cli XLEN {stream_key}") if cursor: logger.info(f"Resuming from cursor: {cursor}") - + async with await get_stream_handler() as stream_handler: - logger.info(f"Stream handler created, starting to read...") + logger.info("Stream handler created, starting to read...") try: chunk_count = 0 async for chunk in stream_handler.read_stream(thread_id, cursor): chunk_count += 1 logger.debug(f"Received chunk #{chunk_count}: error={chunk.error}, is_done={chunk.is_done}, text_len={len(chunk.text) if chunk.text else 0}") - + if chunk.error: logger.error(f"Stream error: {chunk.error}") break - + if chunk.is_done: print("\n✓ Response complete!", flush=True) logger.info(f"Stream completed after {chunk_count} chunks") break - + if chunk.text: # Print directly to console with flush for immediate display - print(chunk.text, end='', flush=True) - + print(chunk.text, end="", flush=True) + if chunk_count == 0: logger.warning("No chunks received from Redis stream!") logger.warning(f"Check Redis manually: redis-cli XLEN {stream_key}") logger.warning(f"View stream contents: redis-cli XREAD STREAMS {stream_key} 0") - + except Exception as ex: logger.error(f"Error reading from Redis: {ex}", exc_info=True) @@ -140,47 +139,47 @@ def run_client(agent_client: DurableAIAgentClient) -> None: # Get a reference to the TravelPlanner agent logger.debug("Getting reference to TravelPlanner agent...") travel_planner = agent_client.get_agent("TravelPlanner") - + # Create a new thread for the conversation thread = travel_planner.get_new_thread() if not thread.session_id: logger.error("Failed to create a new thread with session ID!") return - + key = thread.session_id.key logger.info(f"Thread ID: {key}") - + # Get user input print("\nEnter your travel planning request:") user_message = input("> ").strip() - + if not user_message: logger.warning("No input provided. Using default message.") user_message = "Plan a 3-day trip to Tokyo with emphasis on culture and food" - + logger.info(f"\nYou: {user_message}\n") logger.info("TravelPlanner (streaming from Redis):") logger.info("-" * 80) - + # Start the agent run with wait_for_response=False for non-blocking execution # This signals the agent to start processing without waiting for completion # The agent will execute in the background and write chunks to Redis travel_planner.run(user_message, thread=thread, options={"wait_for_response": False}) - + # Stream the response from Redis # This demonstrates that the client can stream from Redis while # the agent is still processing (or after it completes) asyncio.run(stream_from_redis(str(key))) - + logger.info("\nDemo completed!") if __name__ == "__main__": from dotenv import load_dotenv load_dotenv() - + # Create the client client = get_client() - + # Run the demo run_client(client) diff --git a/python/samples/getting_started/durabletask/03_single_agent_streaming/redis_stream_response_handler.py b/python/samples/getting_started/durabletask/03_single_agent_streaming/redis_stream_response_handler.py index 981393cf00..4a3298df50 100644 --- a/python/samples/getting_started/durabletask/03_single_agent_streaming/redis_stream_response_handler.py +++ b/python/samples/getting_started/durabletask/03_single_agent_streaming/redis_stream_response_handler.py @@ -8,9 +8,9 @@ as a message broker. It enables clients to disconnect and reconnect without losi import asyncio import time +from collections.abc import AsyncIterator from dataclasses import dataclass from datetime import timedelta -from collections.abc import AsyncIterator import redis.asyncio as aioredis diff --git a/python/samples/getting_started/durabletask/03_single_agent_streaming/sample.py b/python/samples/getting_started/durabletask/03_single_agent_streaming/sample.py index 14de97caf8..e6d77c6785 100644 --- a/python/samples/getting_started/durabletask/03_single_agent_streaming/sample.py +++ b/python/samples/getting_started/durabletask/03_single_agent_streaming/sample.py @@ -20,40 +20,40 @@ To run this sample: import logging -from dotenv import load_dotenv - # Import helper functions from worker and client modules from client import get_client, run_client +from dotenv import load_dotenv from worker import get_worker, setup_worker # Configure logging (must be after imports to override their basicConfig) logging.basicConfig(level=logging.INFO, force=True) logger = logging.getLogger(__name__) + 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/getting_started/durabletask/03_single_agent_streaming/tools.py b/python/samples/getting_started/durabletask/03_single_agent_streaming/tools.py index 6a71fdfa03..29be74a846 100644 --- a/python/samples/getting_started/durabletask/03_single_agent_streaming/tools.py +++ b/python/samples/getting_started/durabletask/03_single_agent_streaming/tools.py @@ -153,13 +153,12 @@ def _get_weather_recommendation(condition: str) -> str: if "rain" in condition_lower or "drizzle" in condition_lower: return "Bring an umbrella and waterproof jacket. Consider indoor activities for backup." - elif "fog" in condition_lower: + if "fog" in condition_lower: return "Morning visibility may be limited. Plan outdoor sightseeing for afternoon." - elif "cold" in condition_lower: + if "cold" in condition_lower: return "Layer up with warm clothing. Hot drinks and cozy cafés recommended." - elif "hot" in condition_lower or "warm" in condition_lower: + if "hot" in condition_lower or "warm" in condition_lower: return "Stay hydrated and use sunscreen. Plan strenuous activities for cooler morning hours." - elif "thunder" in condition_lower or "storm" in condition_lower: + if "thunder" in condition_lower or "storm" in condition_lower: return "Keep an eye on weather updates. Have indoor alternatives ready." - else: - return "Pleasant conditions expected. Great day for outdoor exploration!" + return "Pleasant conditions expected. Great day for outdoor exploration!" diff --git a/python/samples/getting_started/durabletask/03_single_agent_streaming/worker.py b/python/samples/getting_started/durabletask/03_single_agent_streaming/worker.py index 1ca37ff607..318b222e54 100644 --- a/python/samples/getting_started/durabletask/03_single_agent_streaming/worker.py +++ b/python/samples/getting_started/durabletask/03_single_agent_streaming/worker.py @@ -27,7 +27,6 @@ from agent_framework.azure import ( ) from azure.identity import AzureCliCredential, DefaultAzureCredential from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker - from redis_stream_response_handler import RedisStreamResponseHandler from tools import get_local_events, get_weather_forecast @@ -186,12 +185,12 @@ def get_worker( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerWorker( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -212,34 +211,34 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker: """ # Create the Redis streaming callback redis_callback = RedisStreamCallback() - + # Wrap it with the agent worker agent_worker = DurableAIAgentWorker(worker, callback=redis_callback) - + # Create and register the TravelPlanner agent logger.debug("Creating and registering TravelPlanner agent...") travel_agent = create_travel_agent() agent_worker.add_agent(travel_agent) - + logger.debug(f"✓ Registered agent: {travel_agent.name}") - + return agent_worker async def main(): """Main entry point for the worker process.""" logger.debug("Starting Durable Task Agent Worker with Redis Streaming...") - + # Create a worker using the helper function worker = get_worker() - + # Setup worker with agent and callback setup_worker(worker) - + # Start the worker logger.debug("Worker started and listening for requests...") worker.start() - + try: # Keep the worker running while True: diff --git a/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/client.py b/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/client.py index 23ac266b36..d9eb12c369 100644 --- a/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/client.py +++ b/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/client.py @@ -41,12 +41,12 @@ def get_client( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerClient( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -63,32 +63,32 @@ def run_client(client: DurableTaskSchedulerClient) -> None: client: The DurableTaskSchedulerClient instance """ logger.debug("Starting single agent chaining orchestration...") - + # Start the orchestration instance_id = client.schedule_new_orchestration( # type: ignore orchestrator="single_agent_chaining_orchestration", input="", ) - + logger.info(f"Orchestration started with instance ID: {instance_id}") logger.debug("Waiting for orchestration to complete...") - + # Retrieve the final state metadata = client.wait_for_orchestration_completion( instance_id=instance_id, timeout=300 ) - + if metadata and metadata.runtime_status.name == "COMPLETED": result = metadata.serialized_output - + logger.debug("Orchestration completed successfully!") - + # Parse and display the result if result: final_text = json.loads(result) logger.info("Final refined sentence: %s \n", final_text) - + elif metadata: logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}") if metadata.serialized_output: @@ -100,10 +100,10 @@ def run_client(client: DurableTaskSchedulerClient) -> None: async def main() -> None: """Main entry point for the client application.""" logger.debug("Starting Durable Task Single Agent Chaining Orchestration Client...") - + # Create client using helper function client = get_client() - + try: run_client(client) except Exception as e: diff --git a/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/sample.py b/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/sample.py index 208c223f5e..d09421c6b4 100644 --- a/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/sample.py +++ b/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/sample.py @@ -21,10 +21,9 @@ To run this sample: import logging -from dotenv import load_dotenv - # Import helper functions from worker and client modules from client import get_client, run_client +from dotenv import load_dotenv from worker import get_worker, setup_worker # Configure logging @@ -35,22 +34,22 @@ logger = logging.getLogger(__name__) 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) @@ -60,7 +59,7 @@ 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/getting_started/durabletask/04_single_agent_orchestration_chaining/worker.py b/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/worker.py index 321a5f1149..18c2fed8e3 100644 --- a/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/worker.py +++ b/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/worker.py @@ -11,15 +11,15 @@ Prerequisites: """ import asyncio -from collections.abc import Generator import logging import os +from collections.abc import Generator from agent_framework import AgentResponse, ChatAgent from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker from azure.identity import AzureCliCredential, DefaultAzureCredential -from durabletask.task import OrchestrationContext, Task from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker +from durabletask.task import OrchestrationContext, Task # Configure logging logging.basicConfig(level=logging.INFO) @@ -42,7 +42,7 @@ def create_writer_agent() -> "ChatAgent": "You refine short pieces of text. When given an initial sentence you enhance it;\n" "when given an improved sentence you polish it further." ) - + return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name=WRITER_AGENT_NAME, instructions=instructions, @@ -78,18 +78,18 @@ def single_agent_chaining_orchestration( str: The final refined text from the second agent run """ logger.debug("[Orchestration] Starting single agent chaining...") - + # Wrap the orchestration context to access agents agent_context = DurableAIAgentOrchestrationContext(context) - + # Get the writer agent using the agent context writer = agent_context.get_agent(WRITER_AGENT_NAME) - + # Create a new thread for the conversation - this will be shared across both runs writer_thread = writer.get_new_thread() - + logger.debug(f"[Orchestration] Created thread: {writer_thread.session_id}") - + prompt = "Write a concise inspirational sentence about learning." # First run: Generate an initial inspirational sentence logger.info("[Orchestration] First agent run: Generating initial sentence about: %s", prompt) @@ -98,21 +98,21 @@ def single_agent_chaining_orchestration( thread=writer_thread, ) logger.info(f"[Orchestration] Initial response: {initial_response.text}") - + # Second run: Refine the initial response on the same thread improved_prompt = ( f"Improve this further while keeping it under 25 words: " f"{initial_response.text}" ) - + logger.info("[Orchestration] Second agent run: Refining the sentence: %s", improved_prompt) refined_response = yield writer.run( messages=improved_prompt, thread=writer_thread, ) - + logger.info(f"[Orchestration] Refined response: {refined_response.text}") - + logger.debug("[Orchestration] Chaining complete") return refined_response.text @@ -134,12 +134,12 @@ def get_worker( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerWorker( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -160,45 +160,45 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker: """ # Wrap it with the agent worker agent_worker = DurableAIAgentWorker(worker) - + # Create and register the Writer agent logger.debug("Creating and registering Writer agent...") writer_agent = create_writer_agent() agent_worker.add_agent(writer_agent) - + logger.debug(f"✓ Registered agent: {writer_agent.name}") - + # Register the orchestration function logger.debug("Registering orchestration function...") worker.add_orchestrator(single_agent_chaining_orchestration) # type: ignore logger.debug(f"✓ Registered orchestration: {single_agent_chaining_orchestration.__name__}") - + return agent_worker async def main(): """Main entry point for the worker process.""" logger.debug("Starting Durable Task Single Agent Chaining Worker with Orchestration...") - + # Create a worker using the helper function worker = get_worker() - + # Setup worker with agents and orchestrations setup_worker(worker) - + logger.debug("Worker is ready and listening for requests...") logger.debug("Press Ctrl+C to stop.") - + try: # Start the worker (this blocks until stopped) worker.start() - + # Keep the worker running while True: await asyncio.sleep(1) except KeyboardInterrupt: logger.debug("Worker shutdown initiated") - + logger.debug("Worker stopped") diff --git a/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/client.py b/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/client.py index f3e92c9fb9..f3d2ee8819 100644 --- a/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/client.py +++ b/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/client.py @@ -41,12 +41,12 @@ def get_client( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerClient( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -68,25 +68,25 @@ def run_client(client: DurableTaskSchedulerClient, prompt: str = "What is temper orchestrator="multi_agent_concurrent_orchestration", input=prompt, ) - + logger.info(f"Orchestration started with instance ID: {instance_id}") logger.debug("Waiting for orchestration to complete...") - + # Retrieve the final state metadata = client.wait_for_orchestration_completion( instance_id=instance_id, ) - + if metadata and metadata.runtime_status.name == "COMPLETED": result = metadata.serialized_output - + logger.debug("Orchestration completed successfully!") - + # Parse and display the result if result: result_json = json.loads(result) if isinstance(result, str) else result logger.info("Orchestration Results:\n%s", json.dumps(result_json, indent=2)) - + elif metadata: logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}") if metadata.serialized_output: @@ -98,10 +98,10 @@ def run_client(client: DurableTaskSchedulerClient, prompt: str = "What is temper async def main() -> None: """Main entry point for the client application.""" logger.debug("Starting Durable Task Multi-Agent Orchestration Client...") - + # Create client using helper function client = get_client() - + try: run_client(client) except Exception as e: diff --git a/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/sample.py b/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/sample.py index ca80aa043e..02ee48c52f 100644 --- a/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/sample.py +++ b/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/sample.py @@ -18,10 +18,9 @@ To run this sample: import logging -from dotenv import load_dotenv - # Import helper functions from worker and client modules from client import get_client, run_client +from dotenv import load_dotenv from worker import get_worker, setup_worker # Configure logging @@ -32,20 +31,20 @@ logger = logging.getLogger(__name__) 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...") @@ -55,7 +54,7 @@ def main(): 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/getting_started/durabletask/05_multi_agent_orchestration_concurrency/worker.py b/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/worker.py index 41b4bd8dda..bae292af0a 100644 --- a/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/worker.py +++ b/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/worker.py @@ -11,16 +11,16 @@ Prerequisites: """ import asyncio -from collections.abc import Generator import logging import os +from collections.abc import Generator from typing import Any from agent_framework import AgentResponse, ChatAgent from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker from azure.identity import AzureCliCredential, DefaultAzureCredential -from durabletask.task import OrchestrationContext, when_all, Task from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker +from durabletask.task import OrchestrationContext, Task, when_all # Configure logging logging.basicConfig(level=logging.INFO) @@ -68,44 +68,44 @@ def multi_agent_concurrent_orchestration(context: OrchestrationContext, prompt: Returns: dict: Dictionary with 'physicist' and 'chemist' response texts """ - + logger.info(f"[Orchestration] Starting concurrent execution for prompt: {prompt}") - + # Wrap the orchestration context to access agents agent_context = DurableAIAgentOrchestrationContext(context) - + # Get agents using the agent context (returns DurableAIAgent proxies) physicist = agent_context.get_agent(PHYSICIST_AGENT_NAME) chemist = agent_context.get_agent(CHEMIST_AGENT_NAME) - + # Create separate threads for each agent physicist_thread = physicist.get_new_thread() chemist_thread = chemist.get_new_thread() - + logger.debug(f"[Orchestration] Created threads - Physicist: {physicist_thread.session_id}, Chemist: {chemist_thread.session_id}") - + # Create tasks from agent.run() calls - these return DurableAgentTask instances physicist_task = physicist.run(messages=str(prompt), thread=physicist_thread) chemist_task = chemist.run(messages=str(prompt), thread=chemist_thread) - + logger.debug("[Orchestration] Created agent tasks, executing concurrently...") - + # Execute both tasks concurrently using when_all # The DurableAgentTask instances wrap the underlying entity calls task_results = yield when_all([physicist_task, chemist_task]) - + logger.debug("[Orchestration] Both agents completed") - + # Extract results from the tasks - DurableAgentTask yields AgentResponse physicist_result: AgentResponse = task_results[0] chemist_result: AgentResponse = task_results[1] - + result = { "physicist": physicist_result.text, "chemist": chemist_result.text, } - - logger.debug(f"[Orchestration] Aggregated results ready") + + logger.debug("[Orchestration] Aggregated results ready") return result @@ -126,12 +126,12 @@ def get_worker( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerWorker( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -152,48 +152,48 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker: """ # Wrap it with the agent worker agent_worker = DurableAIAgentWorker(worker) - + # Create and register both agents logger.debug("Creating and registering agents...") physicist_agent = create_physicist_agent() chemist_agent = create_chemist_agent() - + agent_worker.add_agent(physicist_agent) agent_worker.add_agent(chemist_agent) - + logger.debug(f"✓ Registered agents: {physicist_agent.name}, {chemist_agent.name}") - + # Register the orchestration function logger.debug("Registering orchestration function...") worker.add_orchestrator(multi_agent_concurrent_orchestration) # type: ignore logger.debug(f"✓ Registered orchestration: {multi_agent_concurrent_orchestration.__name__}") - + return agent_worker async def main(): """Main entry point for the worker process.""" logger.debug("Starting Durable Task Multi-Agent Worker with Orchestration...") - + # Create a worker using the helper function worker = get_worker() - + # Setup worker with agents and orchestrations setup_worker(worker) - + logger.debug("Worker is ready and listening for requests...") logger.debug("Press Ctrl+C to stop.") - + try: # Start the worker (this blocks until stopped) worker.start() - + # Keep the worker running while True: await asyncio.sleep(1) except KeyboardInterrupt: logger.debug("Worker shutdown initiated") - + logger.debug("Worker stopped") diff --git a/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/client.py b/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/client.py index 58d4ecc1e8..a0f7f6072c 100644 --- a/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/client.py +++ b/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/client.py @@ -39,12 +39,12 @@ def get_client( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerClient( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -70,36 +70,36 @@ def run_client( "email_id": email_id, "email_content": email_content, } - + logger.debug("Starting spam detection orchestration...") - + # Start the orchestration with the email payload instance_id = client.schedule_new_orchestration( # type: ignore orchestrator="spam_detection_orchestration", input=payload, ) - + logger.debug(f"Orchestration started with instance ID: {instance_id}") logger.debug("Waiting for orchestration to complete...") - + # Retrieve the final state metadata = client.wait_for_orchestration_completion( instance_id=instance_id, timeout=300 ) - + if metadata and metadata.runtime_status.name == "COMPLETED": result = metadata.serialized_output - + logger.debug("Orchestration completed successfully!") - + # Parse and display the result if result: # Remove quotes if present if result.startswith('"') and result.endswith('"'): result = result[1:-1] logger.info(f"Result: {result}") - + elif metadata: logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}") if metadata.serialized_output: @@ -111,29 +111,29 @@ def run_client( async def main() -> None: """Main entry point for the client application.""" logger.debug("Starting Durable Task Spam Detection Orchestration Client...") - + # Create client using helper function client = get_client() - + try: # Test with a legitimate email logger.info("TEST 1: Legitimate Email") - + run_client( client, email_id="email-001", email_content="Hello! I wanted to reach out about our upcoming project meeting scheduled for next week." ) - + # Test with a spam email logger.info("TEST 2: Spam Email") - + run_client( client, email_id="email-002", email_content="URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!" ) - + except Exception as e: logger.exception(f"Error during orchestration: {e}") finally: diff --git a/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/sample.py b/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/sample.py index d8e9d0a4b3..479158dea7 100644 --- a/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/sample.py +++ b/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/sample.py @@ -18,10 +18,9 @@ To run this sample: import logging -from dotenv import load_dotenv - # Import helper functions from worker and client modules from client import get_client, run_client +from dotenv import load_dotenv from worker import get_worker, setup_worker logging.basicConfig( @@ -34,43 +33,43 @@ logger = logging.getLogger() def main(): """Main entry point - runs both worker and client in single process.""" logger.debug("Starting Durable Task Spam Detection 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, orchestrations, 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) logger.debug("CLIENT: Starting orchestration tests...") - + try: # Test 1: Legitimate email # logger.info("TEST 1: Legitimate Email") - + run_client( client, email_id="email-001", email_content="Hello! I wanted to reach out about our upcoming project meeting scheduled for next week." ) - + # Test 2: Spam email logger.info("TEST 2: Spam Email") - + run_client( client, email_id="email-002", email_content="URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!" ) - + except Exception as e: logger.exception(f"Error during sample execution: {e}") - + logger.debug("Sample completed. Worker shutting down...") diff --git a/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/worker.py b/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/worker.py index 78ac71ce8a..5bea536867 100644 --- a/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/worker.py +++ b/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/worker.py @@ -11,16 +11,16 @@ Prerequisites: """ import asyncio -from collections.abc import Generator import logging import os +from collections.abc import Generator from typing import Any, cast from agent_framework import AgentResponse, ChatAgent from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker from azure.identity import AzureCliCredential, DefaultAzureCredential -from durabletask.task import ActivityContext, OrchestrationContext, Task from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker +from durabletask.task import ActivityContext, OrchestrationContext, Task from pydantic import BaseModel, ValidationError # Configure logging @@ -118,24 +118,24 @@ def spam_detection_orchestration(context: OrchestrationContext, payload_raw: Any str: Result message from activity functions """ logger.debug("[Orchestration] Starting spam detection orchestration") - + # Validate input if not isinstance(payload_raw, dict): raise ValueError("Email data is required") - + try: payload = EmailPayload.model_validate(payload_raw) except ValidationError as exc: raise ValueError(f"Invalid email payload: {exc}") from exc - + logger.debug(f"[Orchestration] Processing email ID: {payload.email_id}") - + # Wrap the orchestration context to access agents agent_context = DurableAIAgentOrchestrationContext(context) - + # Get spam detection agent spam_agent = agent_context.get_agent(SPAM_AGENT_NAME) - + # Run spam detection spam_prompt = ( "Analyze this email for spam content and return a JSON response with 'is_spam' (boolean) " @@ -143,52 +143,52 @@ def spam_detection_orchestration(context: OrchestrationContext, payload_raw: Any f"Email ID: {payload.email_id}\n" f"Content: {payload.email_content}" ) - + logger.info("[Orchestration] Running spam detection agent: %s", spam_prompt) spam_result_task = spam_agent.run( messages=spam_prompt, options={"response_format": SpamDetectionResult}, ) - + spam_result_raw: AgentResponse = yield spam_result_task spam_result = cast(SpamDetectionResult, spam_result_raw.value) - + logger.info("[Orchestration] Spam detection result: is_spam=%s", spam_result.is_spam) - + # Branch based on spam detection result if spam_result.is_spam: logger.debug("[Orchestration] Email is spam, handling...") result_task: Task[str] = context.call_activity("handle_spam_email", input=spam_result.reason) result: str = yield result_task return result - + # Email is legitimate - draft a response logger.debug("[Orchestration] Email is legitimate, drafting response...") - + email_agent = agent_context.get_agent(EMAIL_AGENT_NAME) - + email_prompt = ( "Draft a professional response to this email. Return a JSON response with a 'response' field " "containing the reply:\n\n" f"Email ID: {payload.email_id}\n" f"Content: {payload.email_content}" ) - + logger.info("[Orchestration] Running email assistant agent: %s", email_prompt) email_result_task = email_agent.run( messages=email_prompt, options={"response_format": EmailResponse}, ) - + email_result_raw: AgentResponse = yield email_result_task email_result = cast(EmailResponse, email_result_raw.value) - + logger.debug("[Orchestration] Email response drafted, sending...") result_task: Task[str] = context.call_activity("send_email", input=email_result.response) result: str = yield result_task logger.info("Sent Email: %s", result) - + return result @@ -209,12 +209,12 @@ def get_worker( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerWorker( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -235,55 +235,55 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker: """ # Wrap it with the agent worker agent_worker = DurableAIAgentWorker(worker) - + # Create and register both agents logger.debug("Creating and registering agents...") spam_agent = create_spam_agent() email_agent = create_email_agent() - + agent_worker.add_agent(spam_agent) agent_worker.add_agent(email_agent) - + logger.debug(f"✓ Registered agents: {spam_agent.name}, {email_agent.name}") - + # Register activity functions logger.debug("Registering activity functions...") worker.add_activity(handle_spam_email) # type: ignore[arg-type] worker.add_activity(send_email) # type: ignore[arg-type] - logger.debug(f"✓ Registered activity: handle_spam_email") - logger.debug(f"✓ Registered activity: send_email") - + logger.debug("✓ Registered activity: handle_spam_email") + logger.debug("✓ Registered activity: send_email") + # Register the orchestration function logger.debug("Registering orchestration function...") worker.add_orchestrator(spam_detection_orchestration) # type: ignore[arg-type] logger.debug(f"✓ Registered orchestration: {spam_detection_orchestration.__name__}") - + return agent_worker async def main(): """Main entry point for the worker process.""" logger.debug("Starting Durable Task Spam Detection Worker with Orchestration...") - + # Create a worker using the helper function worker = get_worker() - + # Setup worker with agents, orchestrations, and activities setup_worker(worker) - + logger.debug("Worker is ready and listening for requests...") logger.debug("Press Ctrl+C to stop.") - + try: # Start the worker (this blocks until stopped) worker.start() - + # Keep the worker running while True: await asyncio.sleep(1) except KeyboardInterrupt: logger.debug("Worker shutdown initiated") - + logger.debug("Worker stopped") diff --git a/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/client.py b/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/client.py index 446ab1b347..8b7a24853d 100644 --- a/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/client.py +++ b/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/client.py @@ -45,12 +45,12 @@ def get_client( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerClient( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -70,16 +70,16 @@ def _log_completion_result( """ if metadata and metadata.runtime_status.name == "COMPLETED": result = metadata.serialized_output - - logger.debug(f"Orchestration completed successfully!") - + + logger.debug("Orchestration completed successfully!") + if result: try: result_dict = json.loads(result) logger.info("Final Result: %s", json.dumps(result_dict, indent=2)) except json.JSONDecodeError: logger.debug(f"Result: {result}") - + elif metadata: logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}") if metadata.serialized_output: @@ -105,7 +105,7 @@ def _wait_and_log_completion( instance_id=instance_id, timeout=timeout ) - + _log_completion_result(metadata) @@ -127,18 +127,18 @@ def send_approval( "approved": approved, "feedback": feedback } - + logger.debug(f"Sending {'APPROVAL' if approved else 'REJECTION'} to instance {instance_id}") if feedback: logger.debug(f"Feedback: {feedback}") - + # Raise the external event client.raise_orchestration_event( instance_id=instance_id, event_name=HUMAN_APPROVAL_EVENT, data=approval_data ) - + logger.debug("Event sent successfully") @@ -160,14 +160,14 @@ def wait_for_notification( True if notification detected, False if timeout """ logger.debug("Waiting for orchestration to reach notification point...") - + start_time = time.time() while time.time() - start_time < timeout_seconds: try: metadata = client.get_orchestration_state( instance_id=instance_id, ) - + if metadata: # Check if we're waiting for approval by examining custom status if metadata.serialized_custom_status: @@ -183,19 +183,19 @@ def wait_for_notification( if metadata.serialized_custom_status.lower().startswith("requesting human feedback"): logger.debug("Orchestration is requesting human feedback") return True - + # Check for terminal states if metadata.runtime_status.name == "COMPLETED": logger.debug("Orchestration already completed") return False - elif metadata.runtime_status.name == "FAILED": + if metadata.runtime_status.name == "FAILED": logger.error("Orchestration failed") return False except Exception as e: logger.debug(f"Status check: {e}") - + time.sleep(1) - + logger.warning("Timeout waiting for notification") return False @@ -208,94 +208,93 @@ def run_interactive_client(client: DurableTaskSchedulerClient) -> None: """ # Get user inputs logger.debug("Content Generation - Human-in-the-Loop") - + topic = input("Enter the topic for content generation: ").strip() if not topic: topic = "The benefits of cloud computing" logger.info(f"Using default topic: {topic}") - + max_attempts_str = input("Enter max review attempts (default: 3): ").strip() max_review_attempts = int(max_attempts_str) if max_attempts_str else 3 - + timeout_hours_str = input("Enter approval timeout in hours (default: 5): ").strip() timeout_hours = float(timeout_hours_str) if timeout_hours_str else 5.0 approval_timeout_seconds = int(timeout_hours * 3600) - + payload = { "topic": topic, "max_review_attempts": max_review_attempts, "approval_timeout_seconds": approval_timeout_seconds } - + logger.debug(f"Configuration: Topic={topic}, Max attempts={max_review_attempts}, Timeout={timeout_hours}h") - + # Start the orchestration logger.debug("Starting content generation orchestration...") instance_id = client.schedule_new_orchestration( # type: ignore orchestrator="content_generation_hitl_orchestration", input=payload, ) - + logger.info(f"Orchestration started with instance ID: {instance_id}") - + # Review loop attempt = 1 while attempt <= max_review_attempts: logger.info(f"Review Attempt {attempt}/{max_review_attempts}") - + # Wait for orchestration to reach notification point logger.debug("Waiting for content generation...") if not wait_for_notification(client, instance_id, timeout_seconds=120): logger.error("Failed to receive notification. Orchestration may have completed or failed.") break - + logger.info("Content is ready for review! Please review the content in the worker logs.") - + # Get user decision while True: decision = input("Do you approve this content? (yes/no): ").strip().lower() - if decision in ['yes', 'y', 'no', 'n']: + if decision in ["yes", "y", "no", "n"]: break logger.info("Please enter 'yes' or 'no'") - - approved = decision in ['yes', 'y'] - + + approved = decision in ["yes", "y"] + if approved: logger.debug("Sending approval...") send_approval(client, instance_id, approved=True) logger.info("Approval sent. Waiting for orchestration to complete...") _wait_and_log_completion(client, instance_id, timeout=60) break - else: - feedback = input("Enter feedback for improvement: ").strip() - if not feedback: - feedback = "Please revise the content." - - logger.debug("Sending rejection with feedback...") - send_approval(client, instance_id, approved=False, feedback=feedback) - logger.info("Rejection sent. Content will be regenerated...") - - attempt += 1 - - if attempt > max_review_attempts: - logger.info(f"Maximum review attempts ({max_review_attempts}) reached.") - _wait_and_log_completion(client, instance_id, timeout=30) - break - - # Small pause before next iteration - time.sleep(2) + feedback = input("Enter feedback for improvement: ").strip() + if not feedback: + feedback = "Please revise the content." + + logger.debug("Sending rejection with feedback...") + send_approval(client, instance_id, approved=False, feedback=feedback) + logger.info("Rejection sent. Content will be regenerated...") + + attempt += 1 + + if attempt > max_review_attempts: + logger.info(f"Maximum review attempts ({max_review_attempts}) reached.") + _wait_and_log_completion(client, instance_id, timeout=30) + break + + # Small pause before next iteration + time.sleep(2) async def main() -> None: """Main entry point for the client application.""" logger.debug("Starting Durable Task HITL Content Generation Client") - + # Create client using helper function client = get_client() - + try: run_interactive_client(client) - + except KeyboardInterrupt: logger.info("Interrupted by user") except Exception as e: diff --git a/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/sample.py b/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/sample.py index 5468a70dd3..7843621db0 100644 --- a/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/sample.py +++ b/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/sample.py @@ -18,10 +18,9 @@ To run this sample: import logging -from dotenv import load_dotenv - # Import helper functions from worker and client modules from client import get_client, run_interactive_client +from dotenv import load_dotenv from worker import get_worker, setup_worker logging.basicConfig( @@ -34,28 +33,28 @@ logger = logging.getLogger() 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/getting_started/durabletask/07_single_agent_orchestration_hitl/worker.py b/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/worker.py index db32aecf14..77aef7fa22 100644 --- a/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/worker.py +++ b/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/worker.py @@ -11,17 +11,17 @@ Prerequisites: """ import asyncio -from collections.abc import Generator -from datetime import timedelta import logging import os +from collections.abc import Generator +from datetime import timedelta from typing import Any, cast from agent_framework import AgentResponse, ChatAgent from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker from azure.identity import AzureCliCredential, DefaultAzureCredential -from durabletask.task import ActivityContext, OrchestrationContext, Task, when_any # type: ignore from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker +from durabletask.task import ActivityContext, OrchestrationContext, Task, when_any # type: ignore from pydantic import BaseModel, ValidationError # Configure logging @@ -64,7 +64,7 @@ def create_writer_agent() -> "ChatAgent": "Return your response as JSON with 'title' and 'content' fields." "Limit response to 300 words or less." ) - + return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name=WRITER_AGENT_NAME, instructions=instructions, @@ -85,6 +85,7 @@ def notify_user_for_approval(context: ActivityContext, content: dict[str, str]) logger.info("Use the client to send approval or rejection.") return "Notification sent to user for approval." + def publish_content(context: ActivityContext, content: dict[str, str]) -> str: """Activity function to publish approved content. @@ -100,7 +101,7 @@ def publish_content(context: ActivityContext, content: dict[str, str]) -> str: def content_generation_hitl_orchestration( - context: OrchestrationContext, + context: OrchestrationContext, payload_raw: Any ) -> Generator[Task[Any], Any, dict[str, str]]: """Human-in-the-loop orchestration for content generation with approval workflow. @@ -128,44 +129,44 @@ def content_generation_hitl_orchestration( RuntimeError: If max review attempts exhausted """ logger.debug("[Orchestration] Starting HITL content generation orchestration") - + # Validate input if not isinstance(payload_raw, dict): raise ValueError("Content generation input is required") - + try: payload = ContentGenerationInput.model_validate(payload_raw) except ValidationError as exc: raise ValueError(f"Invalid content generation input: {exc}") from exc - + logger.debug(f"[Orchestration] Topic: {payload.topic}") logger.debug(f"[Orchestration] Max attempts: {payload.max_review_attempts}") logger.debug(f"[Orchestration] Approval timeout: {payload.approval_timeout_seconds}s") - + # Wrap the orchestration context to access agents agent_context = DurableAIAgentOrchestrationContext(context) - + # Get the writer agent writer = agent_context.get_agent(WRITER_AGENT_NAME) writer_thread = writer.get_new_thread() logger.info(f"ThreadID: {writer_thread.session_id}") - + # Generate initial content logger.info("[Orchestration] Generating initial content...") - + initial_response: AgentResponse = yield writer.run( messages=f"Write a short article about '{payload.topic}'.", thread=writer_thread, options={"response_format": GeneratedContent}, ) content = cast(GeneratedContent, initial_response.value) - + if not isinstance(content, GeneratedContent): raise ValueError("Agent returned no content after extraction.") - + logger.debug(f"[Orchestration] Initial content generated: {content.title}") - + # Review loop attempt = 0 while attempt < payload.max_review_attempts: @@ -173,34 +174,34 @@ def content_generation_hitl_orchestration( logger.debug(f"[Orchestration] Review iteration #{attempt}/{payload.max_review_attempts}") context.set_custom_status(f"Requesting human feedback (Attempt {attempt}, timeout {payload.approval_timeout_seconds}s)") - + # Notify user for approval yield context.call_activity( - "notify_user_for_approval", + "notify_user_for_approval", input=content.model_dump() ) logger.debug("[Orchestration] Waiting for human approval or timeout...") - + # Wait for approval event or timeout approval_task: Task[Any] = context.wait_for_external_event(HUMAN_APPROVAL_EVENT) # type: ignore timeout_task: Task[Any] = context.create_timer( # type: ignore context.current_utc_datetime + timedelta(seconds=payload.approval_timeout_seconds) ) - + # Race between approval and timeout winner_task = yield when_any([approval_task, timeout_task]) # type: ignore - + if winner_task == approval_task: # Approval received before timeout logger.debug("[Orchestration] Received human approval event") context.set_custom_status("Content reviewed by human reviewer.") - + # Parse approval - approval_data: Any = approval_task.get_result() # type: ignore + approval_data: Any = approval_task.get_result() # type: ignore logger.debug(f"[Orchestration] Approval data: {approval_data}") - + # Handle different formats of approval_data if isinstance(approval_data, dict): approval = HumanApproval.model_validate(approval_data) @@ -215,7 +216,7 @@ def content_generation_hitl_orchestration( approval = HumanApproval(approved=False, feedback=approval_data) else: approval = HumanApproval(approved=False, feedback=str(approval_data)) # type: ignore - + if approval.approved: # Content approved - publish and return logger.debug("[Orchestration] Content approved! Publishing...") @@ -225,52 +226,52 @@ def content_generation_hitl_orchestration( input=content.model_dump() ) yield publish_task - + logger.debug("[Orchestration] Content published successfully") return {"content": content.content, "title": content.title} - + # Content rejected - incorporate feedback and regenerate logger.debug(f"[Orchestration] Content rejected. Feedback: {approval.feedback}") - + # Check if we've exhausted attempts if attempt >= payload.max_review_attempts: context.set_custom_status("Max review attempts exhausted.") # Max attempts exhausted logger.error(f"[Orchestration] Max attempts ({payload.max_review_attempts}) exhausted") break - - context.set_custom_status(f"Content rejected by human reviewer. Regenerating...") - + + context.set_custom_status("Content rejected by human reviewer. Regenerating...") + rewrite_prompt = ( "The content was rejected by a human reviewer. Please rewrite the article incorporating their feedback.\n\n" f"Human Feedback: {approval.feedback or 'No specific feedback provided.'}" ) - + logger.debug("[Orchestration] Regenerating content with feedback...") logger.warning(f"Regenerating with ThreadID: {writer_thread.session_id}") - + rewrite_response: AgentResponse = yield writer.run( messages=rewrite_prompt, thread=writer_thread, options={"response_format": GeneratedContent}, ) rewritten_content = cast(GeneratedContent, rewrite_response.value) - + if not isinstance(rewritten_content, GeneratedContent): raise ValueError("Agent returned no content after rewrite.") - + content = rewritten_content logger.debug(f"[Orchestration] Content regenerated: {content.title}") - + else: # Timeout occurred logger.error(f"[Orchestration] Approval timeout after {payload.approval_timeout_seconds}s") - + raise TimeoutError( f"Human approval timed out after {payload.approval_timeout_seconds} second(s)." ) - + # If we exit the loop without returning, max attempts were exhausted context.set_custom_status("Max review attempts exhausted.") raise RuntimeError( @@ -295,12 +296,12 @@ def get_worker( """ taskhub_name = taskhub or os.getenv("TASKHUB", "default") endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080") - + logger.debug(f"Using taskhub: {taskhub_name}") logger.debug(f"Using endpoint: {endpoint_url}") - + credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential() - + return DurableTaskSchedulerWorker( host_address=endpoint_url, secure_channel=endpoint_url != "http://localhost:8080", @@ -321,52 +322,52 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker: """ # Wrap it with the agent worker agent_worker = DurableAIAgentWorker(worker) - + # Create and register the writer agent logger.debug("Creating and registering Writer agent...") writer_agent = create_writer_agent() agent_worker.add_agent(writer_agent) - + logger.debug(f"✓ Registered agent: {writer_agent.name}") - + # Register activity functions logger.debug("Registering activity functions...") worker.add_activity(notify_user_for_approval) # type: ignore worker.add_activity(publish_content) # type: ignore - logger.debug(f"✓ Registered activity: notify_user_for_approval") - logger.debug(f"✓ Registered activity: publish_content") - + logger.debug("✓ Registered activity: notify_user_for_approval") + logger.debug("✓ Registered activity: publish_content") + # Register the orchestration function logger.debug("Registering orchestration function...") - worker.add_orchestrator(content_generation_hitl_orchestration) # type: ignore + worker.add_orchestrator(content_generation_hitl_orchestration) # type: ignore logger.debug(f"✓ Registered orchestration: {content_generation_hitl_orchestration.__name__}") - + return agent_worker async def main(): """Main entry point for the worker process.""" logger.debug("Starting Durable Task HITL Content Generation Worker...") - + # Create a worker using the helper function worker = get_worker() - + # Setup worker with agents, orchestrations, and activities setup_worker(worker) - + logger.debug("Worker is ready and listening for requests...") logger.debug("Press Ctrl+C to stop.") - + try: # Start the worker (this blocks until stopped) worker.start() - + # Keep the worker running while True: await asyncio.sleep(1) except KeyboardInterrupt: logger.debug("Worker shutdown initiated") - + logger.debug("Worker stopped") diff --git a/python/samples/getting_started/evaluation/self_reflection/self_reflection.py b/python/samples/getting_started/evaluation/self_reflection/self_reflection.py index 01d4823305..bc079fbfcb 100644 --- a/python/samples/getting_started/evaluation/self_reflection/self_reflection.py +++ b/python/samples/getting_started/evaluation/self_reflection/self_reflection.py @@ -1,12 +1,17 @@ # Copyright (c) Microsoft. All rights reserved. # type: ignore +import argparse import asyncio import os import time -import argparse -import pandas as pd -import openai from typing import Any + +import openai +import pandas as pd +from agent_framework import ChatAgent, ChatMessage +from agent_framework.azure import AzureOpenAIChatClient +from azure.ai.projects import AIProjectClient +from azure.identity import AzureCliCredential from dotenv import load_dotenv from openai.types.eval_create_params import DataSourceConfigCustom from openai.types.evals.create_eval_jsonl_run_data_source_param import ( @@ -15,11 +20,6 @@ from openai.types.evals.create_eval_jsonl_run_data_source_param import ( SourceFileContentContent, ) -from agent_framework import ChatAgent, ChatMessage -from agent_framework.azure import AzureOpenAIChatClient -from azure.ai.projects import AIProjectClient -from azure.identity import AzureCliCredential - """ Self-Reflection LLM Runner @@ -122,7 +122,7 @@ def run_eval( if run.status == "failed": print(f"Eval run failed. Run ID: {run.id}, Status: {run.status}, Error: {getattr(run, 'error', 'Unknown error')}") continue - elif run.status == "completed": + if run.status == "completed": output_items = list(client.evals.runs.output_items.list(run_id=run.id, eval_id=eval_object.id)) return output_items time.sleep(5) @@ -162,7 +162,7 @@ async def execute_query_with_self_reflection( - total_groundedness_eval_time: Time spent on evaluations (seconds) - total_end_to_end_time: Total execution time (seconds) """ - messages = [ChatMessage(role="user", text=full_user_query)] + messages = [ChatMessage("user", [full_user_query])] best_score = 0 max_score = 5 @@ -174,8 +174,8 @@ async def execute_query_with_self_reflection( iteration_scores = [] # Store all iteration scores in structured format for i in range(max_self_reflections): - print(f" Self-reflection iteration {i+1}/{max_self_reflections}...") - + print(f" Self-reflection iteration {i + 1}/{max_self_reflections}...") + raw_response = await agent.run(messages=messages) agent_response = raw_response.text @@ -189,7 +189,7 @@ async def execute_query_with_self_reflection( context=context, ) if eval_run_output_items is None: - print(f" ⚠️ Groundedness evaluation failed (timeout or error) for iteration {i+1}.") + print(f" ⚠️ Groundedness evaluation failed (timeout or error) for iteration {i + 1}.") continue score = eval_run_output_items[0].results[0].score end_time_eval = time.time() @@ -209,21 +209,21 @@ async def execute_query_with_self_reflection( best_response = agent_response best_iteration = i + 1 if score == max_score: - print(f" ✓ Perfect groundedness score achieved!") + print(" ✓ Perfect groundedness score achieved!") break else: print(f" → No improvement (score: {score}/{max_score}). Trying again...") - + # Add to conversation history - messages.append(ChatMessage(role="assistant", text=agent_response)) + messages.append(ChatMessage("assistant", [agent_response])) # Request improvement reflection_prompt = ( f"The groundedness score of your response is {score}/{max_score}. " f"Reflect on your answer and improve it to get the maximum score of {max_score} " ) - messages.append(ChatMessage(role="user", text=reflection_prompt)) - + messages.append(ChatMessage("user", [reflection_prompt])) + end_time = time.time() latency = end_time - start_time @@ -290,46 +290,46 @@ async def run_self_reflection_batch( print(f"Processing first {len(df)} prompts (limited by -n {limit})") # Validate required columns - required_columns = ['system_instruction', 'user_request', 'context_document', - 'full_prompt', 'domain', 'type', 'high_level_type'] + required_columns = ["system_instruction", "user_request", "context_document", + "full_prompt", "domain", "type", "high_level_type"] missing_columns = [col for col in required_columns if col not in df.columns] if missing_columns: raise ValueError(f"Input file missing required columns: {missing_columns}") - + # Configure clients - print(f"Configuring Azure OpenAI client...") + print("Configuring Azure OpenAI client...") client = create_openai_client() # Create Eval eval_object = create_eval(client=client, judge_model=judge_model) - + # Process each prompt print(f"Max self-reflections: {max_self_reflections}\n") - + results = [] for counter, (idx, row) in enumerate(df.iterrows(), start=1): print(f"[{counter}/{len(df)}] Processing prompt {row.get('original_index', idx)}...") - + try: result = await execute_query_with_self_reflection( client=client, agent=agent, eval_object=eval_object, - full_user_query=row['full_prompt'], - context=row['context_document'], + full_user_query=row["full_prompt"], + context=row["context_document"], max_self_reflections=max_self_reflections, ) # Prepare result data result_data = { - "original_index": row.get('original_index', idx), - "domain": row['domain'], - "question_type": row['type'], - "high_level_type": row['high_level_type'], - "full_prompt": row['full_prompt'], - "system_prompt": row['system_instruction'], - "user_request": row['user_request'], - "context_document": row['context_document'], + "original_index": row.get("original_index", idx), + "domain": row["domain"], + "question_type": row["type"], + "high_level_type": row["high_level_type"], + "full_prompt": row["full_prompt"], + "system_prompt": row["system_instruction"], + "user_request": row["user_request"], + "context_document": row["context_document"], "agent_response_model": agent_model, "agent_response": result, "error": None, @@ -346,14 +346,14 @@ async def run_self_reflection_batch( # Save error information error_data = { - "original_index": row.get('original_index', idx), - "domain": row['domain'], - "question_type": row['type'], - "high_level_type": row['high_level_type'], - "full_prompt": row['full_prompt'], - "system_prompt": row['system_instruction'], - "user_request": row['user_request'], - "context_document": row['context_document'], + "original_index": row.get("original_index", idx), + "domain": row["domain"], + "question_type": row["type"], + "high_level_type": row["high_level_type"], + "full_prompt": row["full_prompt"], + "system_prompt": row["system_instruction"], + "user_request": row["user_request"], + "context_document": row["context_document"], "agent_response_model": agent_model, "agent_response": None, "error": str(e), @@ -361,36 +361,36 @@ async def run_self_reflection_batch( } results.append(error_data) continue - + # Create DataFrame and save results_df = pd.DataFrame(results) print(f"\nSaving results to: {output_file}") - results_df.to_json(output_file, orient='records', lines=True) + results_df.to_json(output_file, orient="records", lines=True) # Generate detailed summary - successful_runs = results_df[results_df['error'].isna()] - failed_runs = results_df[results_df['error'].notna()] + successful_runs = results_df[results_df["error"].isna()] + failed_runs = results_df[results_df["error"].notna()] - print("\n" + "="*60) + print("\n" + "=" * 60) print("SUMMARY") - print("="*60) + print("=" * 60) print(f"Total prompts processed: {len(results_df)}") print(f" ✓ Successful: {len(successful_runs)}") print(f" ✗ Failed: {len(failed_runs)}") if len(successful_runs) > 0: # Extract scores and iteration data from nested agent_response dict - best_scores = [r['best_response_score'] for r in successful_runs['agent_response'] if r is not None] - iterations = [r['best_iteration'] for r in successful_runs['agent_response'] if r is not None] - iteration_scores_list = [r['iteration_scores'] for r in successful_runs['agent_response'] if r is not None and 'iteration_scores' in r] + best_scores = [r["best_response_score"] for r in successful_runs["agent_response"] if r is not None] + iterations = [r["best_iteration"] for r in successful_runs["agent_response"] if r is not None] + iteration_scores_list = [r["iteration_scores"] for r in successful_runs["agent_response"] if r is not None and "iteration_scores" in r] if best_scores: avg_score = sum(best_scores) / len(best_scores) perfect_scores = sum(1 for s in best_scores if s == 5) - print(f"\nGroundedness Scores:") + print("\nGroundedness Scores:") print(f" Average best score: {avg_score:.2f}/5") - print(f" Perfect scores (5/5): {perfect_scores}/{len(best_scores)} ({100*perfect_scores/len(best_scores):.1f}%)") + print(f" Perfect scores (5/5): {perfect_scores}/{len(best_scores)} ({100 * perfect_scores / len(best_scores):.1f}%)") # Calculate improvement metrics if iteration_scores_list: @@ -404,33 +404,33 @@ async def run_self_reflection_batch( avg_last_score = sum(last_scores) / len(last_scores) avg_improvement = sum(improvements) / len(improvements) - print(f"\nImprovement Analysis:") + print("\nImprovement Analysis:") print(f" Average first score: {avg_first_score:.2f}/5") print(f" Average final score: {avg_last_score:.2f}/5") print(f" Average improvement: +{avg_improvement:.2f}") - print(f" Responses that improved: {improved_count}/{len(improvements)} ({100*improved_count/len(improvements):.1f}%)") + print(f" Responses that improved: {improved_count}/{len(improvements)} ({100 * improved_count / len(improvements):.1f}%)") # Show iteration statistics if iterations: avg_iteration = sum(iterations) / len(iterations) first_try = sum(1 for it in iterations if it == 1) - print(f"\nIteration Statistics:") + print("\nIteration Statistics:") print(f" Average best iteration: {avg_iteration:.2f}") - print(f" Best on first try: {first_try}/{len(iterations)} ({100*first_try/len(iterations):.1f}%)") + print(f" Best on first try: {first_try}/{len(iterations)} ({100 * first_try / len(iterations):.1f}%)") - print("="*60) + print("=" * 60) async def main(): """CLI entry point.""" parser = argparse.ArgumentParser(description="Run self-reflection loop on LLM prompts with groundedness evaluation") - parser.add_argument('--input', '-i', default="resources/suboptimal_groundedness_prompts.jsonl", help='Input JSONL file with prompts') - parser.add_argument('--output', '-o', default="resources/results.jsonl", help='Output JSONL file for results') - parser.add_argument('--agent-model', '-m', default=DEFAULT_AGENT_MODEL, help=f'Agent model deployment name (default: {DEFAULT_AGENT_MODEL})') - parser.add_argument('--judge-model', '-e', default=DEFAULT_JUDGE_MODEL, help=f'Judge model deployment name (default: {DEFAULT_JUDGE_MODEL})') - parser.add_argument('--max-reflections', type=int, default=3, help='Maximum number of self-reflection iterations (default: 3)') - parser.add_argument('--env-file', help='Path to .env file with Azure OpenAI credentials') - parser.add_argument('--limit', '-n', type=int, default=None, help='Process only the first N prompts from the input file') + parser.add_argument("--input", "-i", default="resources/suboptimal_groundedness_prompts.jsonl", help="Input JSONL file with prompts") + parser.add_argument("--output", "-o", default="resources/results.jsonl", help="Output JSONL file for results") + parser.add_argument("--agent-model", "-m", default=DEFAULT_AGENT_MODEL, help=f"Agent model deployment name (default: {DEFAULT_AGENT_MODEL})") + parser.add_argument("--judge-model", "-e", default=DEFAULT_JUDGE_MODEL, help=f"Judge model deployment name (default: {DEFAULT_JUDGE_MODEL})") + parser.add_argument("--max-reflections", type=int, default=3, help="Maximum number of self-reflection iterations (default: 3)") + parser.add_argument("--env-file", help="Path to .env file with Azure OpenAI credentials") + parser.add_argument("--limit", "-n", type=int, default=None, help="Process only the first N prompts from the input file") args = parser.parse_args() diff --git a/python/samples/getting_started/mcp/agent_as_mcp_server.py b/python/samples/getting_started/mcp/agent_as_mcp_server.py index bd095207a7..7d09663625 100644 --- a/python/samples/getting_started/mcp/agent_as_mcp_server.py +++ b/python/samples/getting_started/mcp/agent_as_mcp_server.py @@ -3,8 +3,8 @@ from typing import Annotated, Any import anyio -from agent_framework.openai import OpenAIResponsesClient from agent_framework import tool +from agent_framework.openai import OpenAIResponsesClient """ This sample demonstrates how to expose an Agent as an MCP server. @@ -31,6 +31,7 @@ with the following configuration: ``` """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_specials() -> Annotated[str, "Returns the specials from the menu."]: @@ -40,6 +41,7 @@ def get_specials() -> Annotated[str, "Returns the specials from the menu."]: Special Drink: Chai Tea """ + @tool(approval_mode="never_require") def get_item_price( menu_item: Annotated[str, "The name of the menu item."], diff --git a/python/samples/getting_started/middleware/agent_and_run_level_middleware.py b/python/samples/getting_started/middleware/agent_and_run_level_middleware.py index 3732a8fbc2..ff4735c01c 100644 --- a/python/samples/getting_started/middleware/agent_and_run_level_middleware.py +++ b/python/samples/getting_started/middleware/agent_and_run_level_middleware.py @@ -34,9 +34,9 @@ The example shows: Execution order: Agent middleware (outermost) -> Run middleware (innermost) -> Agent execution """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], ) -> str: diff --git a/python/samples/getting_started/middleware/chat_middleware.py b/python/samples/getting_started/middleware/chat_middleware.py index 8c26957e96..548b1186fa 100644 --- a/python/samples/getting_started/middleware/chat_middleware.py +++ b/python/samples/getting_started/middleware/chat_middleware.py @@ -10,7 +10,6 @@ from agent_framework import ( ChatMessage, ChatMiddleware, ChatResponse, - Role, chat_middleware, tool, ) @@ -36,9 +35,9 @@ The example covers: - Middleware registration at run level (applies to specific run only) """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], ) -> str: @@ -64,7 +63,7 @@ class InputObserverMiddleware(ChatMiddleware): for i, message in enumerate(context.messages): content = message.text if message.text else str(message.contents) - print(f" Message {i + 1} ({message.role.value}): {content}") + print(f" Message {i + 1} ({message.role}): {content}") print(f"[InputObserverMiddleware] Total messages: {len(context.messages)}") @@ -73,7 +72,7 @@ class InputObserverMiddleware(ChatMiddleware): modified_count = 0 for message in context.messages: - if message.role == Role.USER and message.text: + if message.role == "user" and message.text: original_text = message.text updated_text = original_text @@ -81,7 +80,7 @@ class InputObserverMiddleware(ChatMiddleware): updated_text = self.replacement print(f"[InputObserverMiddleware] Updated: '{original_text}' -> '{updated_text}'") - modified_message = ChatMessage(role=message.role, text=updated_text) + modified_message = ChatMessage(message.role, [updated_text]) modified_messages.append(modified_message) modified_count += 1 else: @@ -119,7 +118,7 @@ async def security_and_override_middleware( context.result = ChatResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", text="I cannot process requests containing sensitive information. " "Please rephrase your question without including passwords, secrets, or other " "sensitive data.", diff --git a/python/samples/getting_started/middleware/class_based_middleware.py b/python/samples/getting_started/middleware/class_based_middleware.py index 59af506e74..63ccfc998b 100644 --- a/python/samples/getting_started/middleware/class_based_middleware.py +++ b/python/samples/getting_started/middleware/class_based_middleware.py @@ -13,7 +13,6 @@ from agent_framework import ( ChatMessage, FunctionInvocationContext, FunctionMiddleware, - Role, tool, ) from agent_framework.azure import AzureAIAgentClient @@ -34,9 +33,9 @@ This approach is useful when you need stateful middleware or complex logic that from object-oriented design patterns. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], ) -> str: @@ -63,7 +62,7 @@ class SecurityAgentMiddleware(AgentMiddleware): # Override the result with warning message context.result = AgentResponse( messages=[ - ChatMessage(role=Role.ASSISTANT, text="Detected sensitive information, the request is blocked.") + ChatMessage("assistant", ["Detected sensitive information, the request is blocked."]) ] ) # Simply don't call next() to prevent execution diff --git a/python/samples/getting_started/middleware/decorator_middleware.py b/python/samples/getting_started/middleware/decorator_middleware.py index 99683fad42..0ac600fd19 100644 --- a/python/samples/getting_started/middleware/decorator_middleware.py +++ b/python/samples/getting_started/middleware/decorator_middleware.py @@ -41,9 +41,9 @@ Key benefits of decorator approach: - Prevents type mismatches """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def get_current_time() -> str: """Get the current time.""" return f"Current time is {datetime.datetime.now().strftime('%H:%M:%S')}" diff --git a/python/samples/getting_started/middleware/exception_handling_with_middleware.py b/python/samples/getting_started/middleware/exception_handling_with_middleware.py index 4bd102c4ff..5efe9fe662 100644 --- a/python/samples/getting_started/middleware/exception_handling_with_middleware.py +++ b/python/samples/getting_started/middleware/exception_handling_with_middleware.py @@ -4,8 +4,7 @@ import asyncio from collections.abc import Awaitable, Callable from typing import Annotated -from agent_framework import FunctionInvocationContext -from agent_framework import tool +from agent_framework import FunctionInvocationContext, tool from agent_framework.azure import AzureAIAgentClient from azure.identity.aio import AzureCliCredential from pydantic import Field @@ -24,6 +23,7 @@ The middleware catches TimeoutError from an unstable data service and replaces i a helpful message for the user, preventing raw exceptions from reaching the end user. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def unstable_data_service( diff --git a/python/samples/getting_started/middleware/function_based_middleware.py b/python/samples/getting_started/middleware/function_based_middleware.py index 83cc9eead6..d58ac46c87 100644 --- a/python/samples/getting_started/middleware/function_based_middleware.py +++ b/python/samples/getting_started/middleware/function_based_middleware.py @@ -30,9 +30,9 @@ lightweight approach compared to class-based middleware. Both agent and function can be implemented as async functions that accept context and next parameters. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], ) -> str: diff --git a/python/samples/getting_started/middleware/middleware_termination.py b/python/samples/getting_started/middleware/middleware_termination.py index 8b8c771ff3..cbd82897b4 100644 --- a/python/samples/getting_started/middleware/middleware_termination.py +++ b/python/samples/getting_started/middleware/middleware_termination.py @@ -10,7 +10,6 @@ from agent_framework import ( AgentResponse, AgentRunContext, ChatMessage, - Role, tool, ) from agent_framework.azure import AzureAIAgentClient @@ -29,9 +28,9 @@ The example includes: This is useful for implementing security checks, rate limiting, or early exit conditions. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], ) -> str: @@ -63,7 +62,7 @@ class PreTerminationMiddleware(AgentMiddleware): context.result = AgentResponse( messages=[ ChatMessage( - role=Role.ASSISTANT, + role="assistant", text=( f"Sorry, I cannot process requests containing '{blocked_word}'. " "Please rephrase your question." diff --git a/python/samples/getting_started/middleware/override_result_with_middleware.py b/python/samples/getting_started/middleware/override_result_with_middleware.py index a22b15a67b..fe55f993ed 100644 --- a/python/samples/getting_started/middleware/override_result_with_middleware.py +++ b/python/samples/getting_started/middleware/override_result_with_middleware.py @@ -11,7 +11,6 @@ from agent_framework import ( AgentRunContext, ChatMessage, Content, - Role, tool, ) from agent_framework.azure import AzureAIAgentClient @@ -35,9 +34,9 @@ then replaces its result with a custom "perfect weather" message. For streaming it creates a custom async generator that yields the override message in chunks. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], ) -> str: @@ -75,7 +74,7 @@ async def weather_override_middleware( else: # For non-streaming: just replace with the string message custom_message = "".join(chunks) - context.result = AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=custom_message)]) + context.result = AgentResponse(messages=[ChatMessage("assistant", [custom_message])]) async def main() -> None: diff --git a/python/samples/getting_started/middleware/runtime_context_delegation.py b/python/samples/getting_started/middleware/runtime_context_delegation.py index 300d6cdb22..44ee2a7893 100644 --- a/python/samples/getting_started/middleware/runtime_context_delegation.py +++ b/python/samples/getting_started/middleware/runtime_context_delegation.py @@ -4,7 +4,7 @@ import asyncio from collections.abc import Awaitable, Callable from typing import Annotated -from agent_framework import FunctionInvocationContext, tool, function_middleware +from agent_framework import FunctionInvocationContext, function_middleware, tool from agent_framework.openai import OpenAIChatClient from pydantic import Field diff --git a/python/samples/getting_started/middleware/shared_state_middleware.py b/python/samples/getting_started/middleware/shared_state_middleware.py index 9b568a2ff6..f2a5232262 100644 --- a/python/samples/getting_started/middleware/shared_state_middleware.py +++ b/python/samples/getting_started/middleware/shared_state_middleware.py @@ -26,9 +26,9 @@ The example includes: This approach shows how middleware can work together by sharing state within the same class instance. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], ) -> str: @@ -36,8 +36,8 @@ def get_weather( 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") +@tool(approval_mode="never_require") def get_time( timezone: Annotated[str, Field(description="The timezone to get the time for.")] = "UTC", ) -> str: diff --git a/python/samples/getting_started/middleware/thread_behavior_middleware.py b/python/samples/getting_started/middleware/thread_behavior_middleware.py index d7723812c9..5cca8cb635 100644 --- a/python/samples/getting_started/middleware/thread_behavior_middleware.py +++ b/python/samples/getting_started/middleware/thread_behavior_middleware.py @@ -31,9 +31,9 @@ Key behaviors demonstrated: 4. After next(): thread contains full conversation history (all previous + current messages) """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], ) -> str: diff --git a/python/samples/getting_started/minimal_sample.py b/python/samples/getting_started/minimal_sample.py index ec28486282..a3315b4962 100644 --- a/python/samples/getting_started/minimal_sample.py +++ b/python/samples/getting_started/minimal_sample.py @@ -4,8 +4,9 @@ import asyncio from random import randint from typing import Annotated -from agent_framework.openai import OpenAIChatClient from agent_framework import tool +from agent_framework.openai import OpenAIChatClient + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") diff --git a/python/samples/getting_started/multimodal_input/azure_chat_multimodal.py b/python/samples/getting_started/multimodal_input/azure_chat_multimodal.py index d5c5e58476..826afcd28d 100644 --- a/python/samples/getting_started/multimodal_input/azure_chat_multimodal.py +++ b/python/samples/getting_started/multimodal_input/azure_chat_multimodal.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatMessage, Content, Role +from agent_framework import ChatMessage, Content from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -25,7 +25,7 @@ async def test_image() -> None: image_uri = create_sample_image() message = ChatMessage( - role=Role.USER, + role="user", contents=[ Content.from_text(text="What's in this image?"), Content.from_uri(uri=image_uri, media_type="image/png"), diff --git a/python/samples/getting_started/multimodal_input/azure_responses_multimodal.py b/python/samples/getting_started/multimodal_input/azure_responses_multimodal.py index 350de89aa4..af9bdb0f0a 100644 --- a/python/samples/getting_started/multimodal_input/azure_responses_multimodal.py +++ b/python/samples/getting_started/multimodal_input/azure_responses_multimodal.py @@ -3,7 +3,7 @@ import asyncio from pathlib import Path -from agent_framework import ChatMessage, Content, Role +from agent_framework import ChatMessage, Content from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential @@ -34,7 +34,7 @@ async def test_image() -> None: image_uri = create_sample_image() message = ChatMessage( - role=Role.USER, + role="user", contents=[ Content.from_text(text="What's in this image?"), Content.from_uri(uri=image_uri, media_type="image/png"), @@ -51,7 +51,7 @@ async def test_pdf() -> None: pdf_bytes = load_sample_pdf() message = ChatMessage( - role=Role.USER, + role="user", contents=[ Content.from_text(text="What information can you extract from this document?"), Content.from_data( diff --git a/python/samples/getting_started/multimodal_input/openai_chat_multimodal.py b/python/samples/getting_started/multimodal_input/openai_chat_multimodal.py index e0743340dd..669b963609 100644 --- a/python/samples/getting_started/multimodal_input/openai_chat_multimodal.py +++ b/python/samples/getting_started/multimodal_input/openai_chat_multimodal.py @@ -5,7 +5,7 @@ import base64 import struct from pathlib import Path -from agent_framework import ChatMessage, Content, Role +from agent_framework import ChatMessage, Content from agent_framework.openai import OpenAIChatClient ASSETS_DIR = Path(__file__).resolve().parent.parent / "sample_assets" @@ -46,7 +46,7 @@ async def test_image() -> None: image_uri = create_sample_image() message = ChatMessage( - role=Role.USER, + role="user", contents=[ Content.from_text(text="What's in this image?"), Content.from_uri(uri=image_uri, media_type="image/png"), @@ -63,7 +63,7 @@ async def test_audio() -> None: audio_uri = create_sample_audio() message = ChatMessage( - role=Role.USER, + role="user", contents=[ Content.from_text(text="What do you hear in this audio?"), Content.from_uri(uri=audio_uri, media_type="audio/wav"), @@ -80,7 +80,7 @@ async def test_pdf() -> None: pdf_bytes = load_sample_pdf() message = ChatMessage( - role=Role.USER, + role="user", contents=[ Content.from_text(text="What information can you extract from this document?"), Content.from_data( diff --git a/python/samples/getting_started/observability/advanced_manual_setup_console_output.py b/python/samples/getting_started/observability/advanced_manual_setup_console_output.py index 411d0ed2a6..1ac8fae8da 100644 --- a/python/samples/getting_started/observability/advanced_manual_setup_console_output.py +++ b/python/samples/getting_started/observability/advanced_manual_setup_console_output.py @@ -5,6 +5,7 @@ import logging from random import randint from typing import Annotated +from agent_framework import tool from agent_framework.observability import enable_instrumentation from agent_framework.openai import OpenAIChatClient from opentelemetry._logs import set_logger_provider @@ -19,7 +20,6 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExport from opentelemetry.semconv._incubating.attributes.service_attributes import SERVICE_NAME from opentelemetry.trace import set_tracer_provider from pydantic import Field -from agent_framework import tool """ This sample shows how to manually configure to send traces, logs, and metrics to the console, @@ -65,6 +65,7 @@ def setup_metrics(): # Sets the global default meter provider set_meter_provider(meter_provider) + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") async def get_weather( diff --git a/python/samples/getting_started/observability/advanced_zero_code.py b/python/samples/getting_started/observability/advanced_zero_code.py index d6dcef3b76..5f60af0327 100644 --- a/python/samples/getting_started/observability/advanced_zero_code.py +++ b/python/samples/getting_started/observability/advanced_zero_code.py @@ -4,12 +4,12 @@ import asyncio from random import randint from typing import TYPE_CHECKING, Annotated +from agent_framework import tool from agent_framework.observability import get_tracer from agent_framework.openai import OpenAIResponsesClient from opentelemetry.trace import SpanKind from opentelemetry.trace.span import format_trace_id from pydantic import Field -from agent_framework import tool if TYPE_CHECKING: from agent_framework import ChatClientProtocol @@ -39,6 +39,7 @@ You can also set the environment variables instead of passing them as CLI argume """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") async def get_weather( diff --git a/python/samples/getting_started/observability/agent_observability.py b/python/samples/getting_started/observability/agent_observability.py index bdfa3fdcd3..1c5828d56e 100644 --- a/python/samples/getting_started/observability/agent_observability.py +++ b/python/samples/getting_started/observability/agent_observability.py @@ -4,8 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.observability import configure_otel_providers, get_tracer from agent_framework.openai import OpenAIChatClient from opentelemetry.trace import SpanKind @@ -17,6 +16,7 @@ This sample shows how you can observe an agent in Agent Framework by using the same observability setup function. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") async def get_weather( diff --git a/python/samples/getting_started/observability/agent_with_foundry_tracing.py b/python/samples/getting_started/observability/agent_with_foundry_tracing.py index 30921b26ba..72fd74facf 100644 --- a/python/samples/getting_started/observability/agent_with_foundry_tracing.py +++ b/python/samples/getting_started/observability/agent_with_foundry_tracing.py @@ -7,8 +7,7 @@ from random import randint from typing import Annotated import dotenv -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, 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 @@ -32,6 +31,7 @@ dotenv.load_dotenv() logger = logging.getLogger(__name__) + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") async def get_weather( diff --git a/python/samples/getting_started/observability/azure_ai_agent_observability.py b/python/samples/getting_started/observability/azure_ai_agent_observability.py index c9827cb382..56aa228386 100644 --- a/python/samples/getting_started/observability/azure_ai_agent_observability.py +++ b/python/samples/getting_started/observability/azure_ai_agent_observability.py @@ -6,8 +6,7 @@ from random import randint from typing import Annotated import dotenv -from agent_framework import ChatAgent -from agent_framework import tool +from agent_framework import ChatAgent, tool from agent_framework.azure import AzureAIClient from agent_framework.observability import get_tracer from azure.ai.projects.aio import AIProjectClient @@ -29,6 +28,7 @@ for this sample to work. # For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable dotenv.load_dotenv() + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") async def get_weather( diff --git a/python/samples/getting_started/observability/configure_otel_providers_with_parameters.py b/python/samples/getting_started/observability/configure_otel_providers_with_parameters.py index a69dfe76ec..0929114a60 100644 --- a/python/samples/getting_started/observability/configure_otel_providers_with_parameters.py +++ b/python/samples/getting_started/observability/configure_otel_providers_with_parameters.py @@ -6,7 +6,7 @@ from contextlib import suppress from random import randint from typing import TYPE_CHECKING, Annotated, Literal -from agent_framework import tool, setup_logging +from agent_framework import setup_logging, tool from agent_framework.observability import configure_otel_providers, get_tracer from agent_framework.openai import OpenAIResponsesClient from opentelemetry import trace diff --git a/python/samples/getting_started/observability/workflow_observability.py b/python/samples/getting_started/observability/workflow_observability.py index 57e636fd68..7cd5174025 100644 --- a/python/samples/getting_started/observability/workflow_observability.py +++ b/python/samples/getting_started/observability/workflow_observability.py @@ -8,7 +8,6 @@ from agent_framework import ( WorkflowContext, WorkflowOutputEvent, handler, - tool, ) from agent_framework.observability import configure_otel_providers, get_tracer from opentelemetry.trace import SpanKind diff --git a/python/samples/getting_started/purview_agent/sample_purview_agent.py b/python/samples/getting_started/purview_agent/sample_purview_agent.py index 223eed55e3..cb79042979 100644 --- a/python/samples/getting_started/purview_agent/sample_purview_agent.py +++ b/python/samples/getting_started/purview_agent/sample_purview_agent.py @@ -25,7 +25,7 @@ import asyncio import os from typing import Any -from agent_framework import AgentResponse, ChatAgent, ChatMessage, Role +from agent_framework import AgentResponse, ChatAgent, ChatMessage from agent_framework.azure import AzureOpenAIChatClient from agent_framework.microsoft import ( PurviewChatPolicyMiddleware, @@ -159,13 +159,13 @@ async def run_with_agent_middleware() -> None: print("-- Agent Middleware Path --") first: AgentResponse = await agent.run( - ChatMessage(role=Role.USER, text="Tell me a joke about a pirate.", additional_properties={"user_id": user_id}) + ChatMessage("user", ["Tell me a joke about a pirate."], additional_properties={"user_id": user_id}) ) print("First response (agent middleware):\n", first) second: AgentResponse = await agent.run( ChatMessage( - role=Role.USER, text="That was funny. Tell me another one.", additional_properties={"user_id": user_id} + role="user", text="That was funny. Tell me another one.", additional_properties={"user_id": user_id} ) ) print("Second response (agent middleware):\n", second) @@ -203,7 +203,7 @@ async def run_with_chat_middleware() -> None: print("-- Chat Middleware Path --") first: AgentResponse = await agent.run( ChatMessage( - role=Role.USER, + role="user", text="Give me a short clean joke.", additional_properties={"user_id": user_id}, ) @@ -212,7 +212,7 @@ async def run_with_chat_middleware() -> None: second: AgentResponse = await agent.run( ChatMessage( - role=Role.USER, + role="user", text="One more please.", additional_properties={"user_id": user_id}, ) @@ -253,13 +253,13 @@ async def run_with_custom_cache_provider() -> None: first: AgentResponse = await agent.run( ChatMessage( - role=Role.USER, text="Tell me a joke about a programmer.", additional_properties={"user_id": user_id} + role="user", text="Tell me a joke about a programmer.", additional_properties={"user_id": user_id} ) ) print("First response (custom provider):\n", first) second: AgentResponse = await agent.run( - ChatMessage(role=Role.USER, text="That's hilarious! One more?", additional_properties={"user_id": user_id}) + ChatMessage("user", ["That's hilarious! One more?"], additional_properties={"user_id": user_id}) ) print("Second response (custom provider):\n", second) @@ -294,12 +294,12 @@ async def run_with_custom_cache_provider() -> None: print("Using default InMemoryCacheProvider with settings-based configuration") first: AgentResponse = await agent.run( - ChatMessage(role=Role.USER, text="Tell me a joke about AI.", additional_properties={"user_id": user_id}) + ChatMessage("user", ["Tell me a joke about AI."], additional_properties={"user_id": user_id}) ) print("First response (default cache):\n", first) second: AgentResponse = await agent.run( - ChatMessage(role=Role.USER, text="Nice! Another AI joke please.", additional_properties={"user_id": user_id}) + ChatMessage("user", ["Nice! Another AI joke please."], additional_properties={"user_id": user_id}) ) print("Second response (default cache):\n", second) diff --git a/python/samples/getting_started/tools/function_invocation_configuration.py b/python/samples/getting_started/tools/function_invocation_configuration.py index c53eab01a3..a73c683cf9 100644 --- a/python/samples/getting_started/tools/function_invocation_configuration.py +++ b/python/samples/getting_started/tools/function_invocation_configuration.py @@ -3,8 +3,8 @@ import asyncio from typing import Annotated -from agent_framework.openai import OpenAIResponsesClient from agent_framework import tool +from agent_framework.openai import OpenAIResponsesClient """ This sample demonstrates how to configure function invocation settings @@ -13,6 +13,7 @@ for an client and use a simple tool as a tool in an agent. This behavior is the same for all chat client types. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def add( diff --git a/python/samples/getting_started/tools/function_tool_recover_from_failures.py b/python/samples/getting_started/tools/function_tool_recover_from_failures.py index 1349421b24..1637e6ab38 100644 --- a/python/samples/getting_started/tools/function_tool_recover_from_failures.py +++ b/python/samples/getting_started/tools/function_tool_recover_from_failures.py @@ -3,8 +3,7 @@ import asyncio from typing import Annotated -from agent_framework import FunctionCallContent, FunctionResultContent -from agent_framework import tool +from agent_framework import FunctionCallContent, FunctionResultContent, tool from agent_framework.openai import OpenAIResponsesClient """ @@ -14,6 +13,7 @@ Shows how a tool that throws an exception creates gracefull recovery and can kee The LLM decides whether to retry the call or to respond with something else, based on the exception. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def greet(name: Annotated[str, "Name to greet"]) -> str: diff --git a/python/samples/getting_started/tools/function_tool_with_approval.py b/python/samples/getting_started/tools/function_tool_with_approval.py index 813bbb61ea..188697a8ce 100644 --- a/python/samples/getting_started/tools/function_tool_with_approval.py +++ b/python/samples/getting_started/tools/function_tool_with_approval.py @@ -59,14 +59,14 @@ async def handle_approvals(query: str, agent: "AgentProtocol") -> AgentResponse: ) # Add the assistant message with the approval request - new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed])) + new_inputs.append(ChatMessage("assistant", [user_input_needed])) # Get user approval user_approval = await asyncio.to_thread(input, "\nApprove function call? (y/n): ") # Add the user's approval response new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) + ChatMessage("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) # Run again with all the context @@ -109,14 +109,14 @@ async def handle_approvals_streaming(query: str, agent: "AgentProtocol") -> None ) # Add the assistant message with the approval request - new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed])) + new_inputs.append(ChatMessage("assistant", [user_input_needed])) # Get user approval user_approval = await asyncio.to_thread(input, "\nApprove function call? (y/n): ") # Add the user's approval response new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) + ChatMessage("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) # Update input with all the context for next iteration diff --git a/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py b/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py index 9b9c06837a..de1da05991 100644 --- a/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py +++ b/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py @@ -55,7 +55,7 @@ async def approval_example() -> None: # Step 2: Send approval response approval_response = request.to_function_approval_response(approved=approved) - result = await agent.run(ChatMessage(role="user", contents=[approval_response]), thread=thread) + result = await agent.run(ChatMessage("user", [approval_response]), thread=thread) print(f"Agent: {result}\n") @@ -88,7 +88,7 @@ async def rejection_example() -> None: # Send rejection response rejection_response = request.to_function_approval_response(approved=False) - result = await agent.run(ChatMessage(role="user", contents=[rejection_response]), thread=thread) + result = await agent.run(ChatMessage("user", [rejection_response]), thread=thread) print(f"Agent: {result}\n") diff --git a/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py b/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py index f5094e9040..7c9f7a4cbb 100644 --- a/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py +++ b/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py @@ -8,7 +8,6 @@ from agent_framework import ( WorkflowContext, executor, handler, - tool, ) from typing_extensions import Never diff --git a/python/samples/getting_started/workflows/_start-here/step3_streaming.py b/python/samples/getting_started/workflows/_start-here/step3_streaming.py index ffd3e9323d..f44ececc63 100644 --- a/python/samples/getting_started/workflows/_start-here/step3_streaming.py +++ b/python/samples/getting_started/workflows/_start-here/step3_streaming.py @@ -13,7 +13,6 @@ from agent_framework import ( WorkflowRunState, WorkflowStatusEvent, handler, - tool, ) from agent_framework._workflows._events import WorkflowOutputEvent from agent_framework.azure import AzureOpenAIChatClient @@ -123,7 +122,7 @@ async def main(): # Run the workflow with the user's initial message and stream events as they occur. # This surfaces executor events, workflow outputs, run-state changes, and errors. async for event in workflow.run_stream( - ChatMessage(role="user", text="Create a slogan for a new electric SUV that is affordable and fun to drive.") + ChatMessage("user", ["Create a slogan for a new electric SUV that is affordable and fun to drive."]) ): if isinstance(event, WorkflowStatusEvent): prefix = f"State ({event.origin.value}): " diff --git a/python/samples/getting_started/workflows/_start-here/step4_using_factories.py b/python/samples/getting_started/workflows/_start-here/step4_using_factories.py index f9d4f2b971..a7b9918991 100644 --- a/python/samples/getting_started/workflows/_start-here/step4_using_factories.py +++ b/python/samples/getting_started/workflows/_start-here/step4_using_factories.py @@ -11,7 +11,6 @@ from agent_framework import ( WorkflowOutputEvent, executor, handler, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential diff --git a/python/samples/getting_started/workflows/agents/azure_chat_agents_function_bridge.py b/python/samples/getting_started/workflows/agents/azure_chat_agents_function_bridge.py index 11bac9f2c9..64fb3f3e9a 100644 --- a/python/samples/getting_started/workflows/agents/azure_chat_agents_function_bridge.py +++ b/python/samples/getting_started/workflows/agents/azure_chat_agents_function_bridge.py @@ -9,12 +9,10 @@ from agent_framework import ( AgentResponse, AgentRunUpdateEvent, ChatMessage, - Role, WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, executor, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -72,7 +70,7 @@ async def enrich_with_references( ) -> None: """Inject a follow-up user instruction that adds an external note for the next agent.""" conversation = list(draft.full_conversation or draft.agent_response.messages) - original_prompt = next((message.text for message in conversation if message.role == Role.USER), "") + original_prompt = next((message.text for message in conversation if message.role == "user"), "") external_note = _lookup_external_note(original_prompt) or ( "No additional references were found. Please refine the previous assistant response for clarity." ) @@ -82,7 +80,7 @@ async def enrich_with_references( f"{external_note}\n\n" "Please update the prior assistant answer so it weaves this note into the guidance." ) - conversation.append(ChatMessage(role=Role.USER, text=follow_up)) + conversation.append(ChatMessage("user", [follow_up])) await ctx.send_message(AgentExecutorRequest(messages=conversation)) diff --git a/python/samples/getting_started/workflows/agents/azure_chat_agents_tool_calls_with_feedback.py b/python/samples/getting_started/workflows/agents/azure_chat_agents_tool_calls_with_feedback.py index 1b97677374..73e08bd0c0 100644 --- a/python/samples/getting_started/workflows/agents/azure_chat_agents_tool_calls_with_feedback.py +++ b/python/samples/getting_started/workflows/agents/azure_chat_agents_tool_calls_with_feedback.py @@ -16,7 +16,6 @@ from agent_framework import ( FunctionCallContent, FunctionResultContent, RequestInfoEvent, - Role, WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, @@ -50,9 +49,9 @@ Prerequisites: - Authentication via azure-identity. Run `az login` before executing. """ + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") - def fetch_product_brief( product_name: Annotated[str, Field(description="Product name to look up.")], ) -> str: @@ -68,8 +67,8 @@ def fetch_product_brief( } return briefs.get(product_name.lower(), f"No stored brief for '{product_name}'.") -@tool(approval_mode="never_require") +@tool(approval_mode="never_require") def get_brand_voice_profile( voice_name: Annotated[str, Field(description="Brand or campaign voice to emulate.")], ) -> str: @@ -149,7 +148,7 @@ class Coordinator(Executor): await ctx.send_message( AgentExecutorRequest( messages=original_request.conversation - + [ChatMessage(Role.USER, text="The draft is approved as-is.")], + + [ChatMessage("user", text="The draft is approved as-is.")], should_respond=True, ), target_id=self.final_editor_id, @@ -164,7 +163,7 @@ class Coordinator(Executor): "Rewrite the draft from the previous assistant message into a polished final version. " "Keep the response under 120 words and reflect any requested tone adjustments." ) - conversation.append(ChatMessage(Role.USER, text=instruction)) + conversation.append(ChatMessage("user", text=instruction)) await ctx.send_message( AgentExecutorRequest(messages=conversation, should_respond=True), target_id=self.writer_id ) diff --git a/python/samples/getting_started/workflows/agents/custom_agent_executors.py b/python/samples/getting_started/workflows/agents/custom_agent_executors.py index 3f95aab0e4..c9fe07b0a2 100644 --- a/python/samples/getting_started/workflows/agents/custom_agent_executors.py +++ b/python/samples/getting_started/workflows/agents/custom_agent_executors.py @@ -9,7 +9,6 @@ from agent_framework import ( WorkflowBuilder, WorkflowContext, handler, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -121,7 +120,7 @@ async def main(): # Run the workflow with the user's initial message. # For foundational clarity, use run (non streaming) and print the workflow output. events = await workflow.run( - ChatMessage(role="user", text="Create a slogan for a new electric SUV that is affordable and fun to drive.") + ChatMessage("user", ["Create a slogan for a new electric SUV that is affordable and fun to drive."]) ) # The terminal node yields output; print its contents. outputs = events.get_outputs() diff --git a/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py b/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py index 3b820fe969..46c015fa42 100644 --- a/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py +++ b/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py @@ -11,7 +11,6 @@ from agent_framework import ( FunctionResultContent, HandoffAgentUserRequest, HandoffBuilder, - Role, WorkflowAgent, tool, ) @@ -118,7 +117,7 @@ def handle_response_and_requests(response: AgentResponse) -> dict[str, HandoffAg pending_requests: dict[str, HandoffAgentUserRequest] = {} for message in response.messages: if message.text: - print(f"- {message.author_name or message.role.value}: {message.text}") + print(f"- {message.author_name or message.role}: {message.text}") for content in message.contents: if isinstance(content, FunctionCallContent): if isinstance(content.arguments, dict): @@ -200,7 +199,7 @@ async def main() -> None: for request in pending_requests.values(): for message in request.agent_response.messages: if message.text: - print(f"- {message.author_name or message.role.value}: {message.text}") + print(f"- {message.author_name or message.role}: {message.text}") if not scripted_responses: # No more scripted responses; terminate the workflow @@ -217,7 +216,7 @@ async def main() -> None: function_results = [ FunctionResultContent(call_id=req_id, result=response) for req_id, response in responses.items() ] - response = await agent.run(ChatMessage(role=Role.TOOL, contents=function_results)) + response = await agent.run(ChatMessage("tool", function_results)) pending_requests = handle_response_and_requests(response) diff --git a/python/samples/getting_started/workflows/agents/magentic_workflow_as_agent.py b/python/samples/getting_started/workflows/agents/magentic_workflow_as_agent.py index adfeffbc9e..3badeae78a 100644 --- a/python/samples/getting_started/workflows/agents/magentic_workflow_as_agent.py +++ b/python/samples/getting_started/workflows/agents/magentic_workflow_as_agent.py @@ -6,7 +6,6 @@ from agent_framework import ( ChatAgent, HostedCodeInterpreterTool, MagenticBuilder, - tool, ) from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient diff --git a/python/samples/getting_started/workflows/agents/mixed_agents_and_executors.py b/python/samples/getting_started/workflows/agents/mixed_agents_and_executors.py index ab36cf3962..3ec8d0f530 100644 --- a/python/samples/getting_started/workflows/agents/mixed_agents_and_executors.py +++ b/python/samples/getting_started/workflows/agents/mixed_agents_and_executors.py @@ -11,7 +11,6 @@ from agent_framework import ( WorkflowBuilder, WorkflowContext, handler, - tool, ) from agent_framework.azure import AzureAIAgentClient from azure.identity.aio import AzureCliCredential diff --git a/python/samples/getting_started/workflows/agents/sequential_workflow_as_agent.py b/python/samples/getting_started/workflows/agents/sequential_workflow_as_agent.py index bb2ade5e01..3a0264844b 100644 --- a/python/samples/getting_started/workflows/agents/sequential_workflow_as_agent.py +++ b/python/samples/getting_started/workflows/agents/sequential_workflow_as_agent.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import Role, SequentialBuilder +from agent_framework import SequentialBuilder from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -52,7 +52,7 @@ async def main() -> None: for i, msg in enumerate(agent_response.messages, start=1): role_value = getattr(msg.role, "value", msg.role) normalized_role = str(role_value).lower() if role_value is not None else "assistant" - name = msg.author_name or ("assistant" if normalized_role == Role.ASSISTANT.value else "user") + name = msg.author_name or ("assistant" if normalized_role == "assistant".value else "user") print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}") """ diff --git a/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop.py b/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop.py index 118800765d..a0d9769695 100644 --- a/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop.py +++ b/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop.py @@ -20,13 +20,11 @@ from agent_framework import ( # noqa: E402 Executor, FunctionCallContent, FunctionResultContent, - Role, WorkflowAgent, WorkflowBuilder, WorkflowContext, handler, response_handler, - tool, ) from getting_started.workflows.agents.workflow_as_agent_reflection_pattern import ( # noqa: E402 ReviewRequest, @@ -168,7 +166,7 @@ async def main() -> None: result=human_response, ) # Send the human review result back to the agent. - response = await agent.run(ChatMessage(role=Role.TOOL, contents=[human_review_function_result])) + response = await agent.run(ChatMessage("tool", [human_review_function_result])) print(f"📤 Agent Response: {response.messages[-1].text}") print("=" * 50) diff --git a/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern.py b/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern.py index 9aa98f7b96..577a892066 100644 --- a/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern.py +++ b/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern.py @@ -11,11 +11,9 @@ from agent_framework import ( ChatMessage, Content, Executor, - Role, WorkflowBuilder, WorkflowContext, handler, - tool, ) from agent_framework.openai import OpenAIChatClient from pydantic import BaseModel @@ -81,7 +79,7 @@ class Reviewer(Executor): # Construct review instructions and context. messages = [ ChatMessage( - role=Role.SYSTEM, + role="system", text=( "You are a reviewer for an AI agent. Provide feedback on the " "exchange between a user and the agent. Indicate approval only if:\n" @@ -98,7 +96,7 @@ class Reviewer(Executor): messages.extend(request.agent_messages) # Add explicit review instruction. - messages.append(ChatMessage(role=Role.USER, text="Please review the agent's responses.")) + messages.append(ChatMessage("user", ["Please review the agent's responses."])) print("Reviewer: Sending review request to LLM...") response = await self._chat_client.get_response(messages=messages, options={"response_format": _Response}) @@ -127,7 +125,7 @@ class Worker(Executor): print("Worker: Received user messages, generating response...") # Initialize chat with system prompt. - messages = [ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant.")] + messages = [ChatMessage("system", ["You are a helpful assistant."])] messages.extend(user_messages) print("Worker: Calling LLM to generate response...") @@ -162,7 +160,7 @@ class Worker(Executor): # Emit approved result to external consumer via AgentRunUpdateEvent. await ctx.add_event( - AgentRunUpdateEvent(self.id, data=AgentResponseUpdate(contents=contents, role=Role.ASSISTANT)) + AgentRunUpdateEvent(self.id, data=AgentResponseUpdate(contents=contents, role="assistant")) ) return @@ -170,9 +168,9 @@ class Worker(Executor): print("Worker: Regenerating response with feedback...") # Incorporate review feedback. - messages.append(ChatMessage(role=Role.SYSTEM, text=review.feedback)) + messages.append(ChatMessage("system", [review.feedback])) messages.append( - ChatMessage(role=Role.SYSTEM, text="Please incorporate the feedback and regenerate the response.") + ChatMessage("system", ["Please incorporate the feedback and regenerate the response."]) ) messages.extend(request.user_messages) diff --git a/python/samples/getting_started/workflows/agents/workflow_as_agent_with_thread.py b/python/samples/getting_started/workflows/agents/workflow_as_agent_with_thread.py index 5d145ef28f..0580fe45ab 100644 --- a/python/samples/getting_started/workflows/agents/workflow_as_agent_with_thread.py +++ b/python/samples/getting_started/workflows/agents/workflow_as_agent_with_thread.py @@ -78,7 +78,7 @@ async def main() -> None: response1 = await agent.run(query1, thread=thread) if response1.messages: for msg in response1.messages: - speaker = msg.author_name or msg.role.value + speaker = msg.author_name or msg.role print(f"[{speaker}]: {msg.text}") # Second turn: Reference the previous topic @@ -88,7 +88,7 @@ async def main() -> None: response2 = await agent.run(query2, thread=thread) if response2.messages: for msg in response2.messages: - speaker = msg.author_name or msg.role.value + speaker = msg.author_name or msg.role print(f"[{speaker}]: {msg.text}") # Third turn: Ask a follow-up question @@ -98,7 +98,7 @@ async def main() -> None: response3 = await agent.run(query3, thread=thread) if response3.messages: for msg in response3.messages: - speaker = msg.author_name or msg.role.value + speaker = msg.author_name or msg.role print(f"[{speaker}]: {msg.text}") # Show the accumulated conversation history @@ -108,7 +108,7 @@ async def main() -> None: if thread.message_store: history = await thread.message_store.list_messages() for i, msg in enumerate(history, start=1): - role = msg.role.value if hasattr(msg.role, "value") else str(msg.role) + role = msg.role if hasattr(msg.role, "value") else str(msg.role) speaker = msg.author_name or role text_preview = msg.text[:80] + "..." if len(msg.text) > 80 else msg.text print(f"{i:02d}. [{speaker}]: {text_preview}") diff --git a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py index a2628592ea..71cfff1cc9 100644 --- a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py +++ b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py @@ -16,7 +16,6 @@ from agent_framework import ( Executor, FileCheckpointStorage, RequestInfoEvent, - Role, Workflow, WorkflowBuilder, WorkflowCheckpoint, @@ -26,7 +25,6 @@ from agent_framework import ( get_checkpoint_summary, handler, response_handler, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -94,7 +92,7 @@ class BriefPreparer(Executor): # Hand the prompt to the writer agent. We always route through the # workflow context so the runtime can capture messages for checkpointing. await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=prompt)], should_respond=True), + AgentExecutorRequest(messages=[ChatMessage("user", text=prompt)], should_respond=True), target_id=self._agent_id, ) @@ -156,7 +154,7 @@ class ReviewGateway(Executor): f"Human guidance: {reply}" ) await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=prompt)], should_respond=True), + AgentExecutorRequest(messages=[ChatMessage("user", text=prompt)], should_respond=True), target_id=self._writer_id, ) diff --git a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py index bfa2484d63..a6f0a2431b 100644 --- a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py +++ b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py @@ -37,7 +37,6 @@ from agent_framework import ( WorkflowContext, WorkflowOutputEvent, handler, - tool, ) diff --git a/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py b/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py index 145504bdce..e35894b8db 100644 --- a/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py +++ b/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py @@ -106,7 +106,7 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> tuple[Workflow .with_checkpointing(checkpoint_storage) .with_termination_condition( # Terminate after 5 user messages for this demo - lambda conv: sum(1 for msg in conv if msg.role.value == "user") >= 5 + lambda conv: sum(1 for msg in conv if msg.role == "user") >= 5 ) .build() ) @@ -285,7 +285,7 @@ async def resume_with_responses( # Now safe to cast event.data to list[ChatMessage] conversation = cast(list[ChatMessage], event.data) for msg in conversation[-3:]: # Show last 3 messages - author = msg.author_name or msg.role.value + author = msg.author_name or msg.role text = msg.text[:100] + "..." if len(msg.text) > 100 else msg.text print(f" {author}: {text}") diff --git a/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py b/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py index d35fd5e41f..24dec9fb3e 100644 --- a/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py +++ b/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py @@ -24,7 +24,6 @@ from agent_framework import ( WorkflowStatusEvent, handler, response_handler, - tool, ) CHECKPOINT_DIR = Path(__file__).with_suffix("").parent / "tmp" / "sub_workflow_checkpoints" diff --git a/python/samples/getting_started/workflows/checkpoint/workflow_as_agent_checkpoint.py b/python/samples/getting_started/workflows/checkpoint/workflow_as_agent_checkpoint.py index c0647c72f7..c05ab2111e 100644 --- a/python/samples/getting_started/workflows/checkpoint/workflow_as_agent_checkpoint.py +++ b/python/samples/getting_started/workflows/checkpoint/workflow_as_agent_checkpoint.py @@ -31,7 +31,6 @@ from agent_framework import ( ChatMessageStore, InMemoryCheckpointStorage, SequentialBuilder, - tool, ) from agent_framework.openai import OpenAIChatClient @@ -70,7 +69,7 @@ async def basic_checkpointing() -> None: response = await agent.run(query, checkpoint_storage=checkpoint_storage) for msg in response.messages: - speaker = msg.author_name or msg.role.value + speaker = msg.author_name or msg.role print(f"[{speaker}]: {msg.text}") # Show checkpoints that were created diff --git a/python/samples/getting_started/workflows/composition/sub_workflow_basics.py b/python/samples/getting_started/workflows/composition/sub_workflow_basics.py index cb789850c4..826425a0ae 100644 --- a/python/samples/getting_started/workflows/composition/sub_workflow_basics.py +++ b/python/samples/getting_started/workflows/composition/sub_workflow_basics.py @@ -10,10 +10,9 @@ from agent_framework import ( WorkflowContext, WorkflowExecutor, handler, - tool, ) from typing_extensions import Never - + """ Sample: Sub-Workflows (Basics) diff --git a/python/samples/getting_started/workflows/composition/sub_workflow_parallel_requests.py b/python/samples/getting_started/workflows/composition/sub_workflow_parallel_requests.py index dadb4325d9..0959f591f0 100644 --- a/python/samples/getting_started/workflows/composition/sub_workflow_parallel_requests.py +++ b/python/samples/getting_started/workflows/composition/sub_workflow_parallel_requests.py @@ -16,7 +16,6 @@ from agent_framework import ( WorkflowExecutor, handler, response_handler, - tool, ) from typing_extensions import Never diff --git a/python/samples/getting_started/workflows/composition/sub_workflow_request_interception.py b/python/samples/getting_started/workflows/composition/sub_workflow_request_interception.py index e21c74039a..167ae2e950 100644 --- a/python/samples/getting_started/workflows/composition/sub_workflow_request_interception.py +++ b/python/samples/getting_started/workflows/composition/sub_workflow_request_interception.py @@ -14,7 +14,6 @@ from agent_framework import ( WorkflowOutputEvent, handler, response_handler, - tool, ) from typing_extensions import Never diff --git a/python/samples/getting_started/workflows/control-flow/edge_condition.py b/python/samples/getting_started/workflows/control-flow/edge_condition.py index 6d1a8ffb0f..cdb1d2fb03 100644 --- a/python/samples/getting_started/workflows/control-flow/edge_condition.py +++ b/python/samples/getting_started/workflows/control-flow/edge_condition.py @@ -9,12 +9,10 @@ from agent_framework import ( # Core chat primitives used to build requests AgentExecutorResponse, ChatAgent, # Output from an AgentExecutor ChatMessage, - Role, WorkflowBuilder, # Fluent builder for wiring executors and edges WorkflowContext, # Per-run context and event bus executor, # Decorator to declare a Python function as a workflow executor - tool, -) + ) from agent_framework.azure import AzureOpenAIChatClient # Thin client wrapper for Azure OpenAI chat models from azure.identity import AzureCliCredential # Uses your az CLI login for credentials from pydantic import BaseModel # Structured outputs for safer parsing @@ -125,7 +123,7 @@ async def to_email_assistant_request( """ # Bridge executor. Converts a structured DetectionResult into a ChatMessage and forwards it as a new request. detection = DetectionResult.model_validate_json(response.agent_response.text) - user_msg = ChatMessage(Role.USER, text=detection.email_content) + user_msg = ChatMessage("user", text=detection.email_content) await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True)) @@ -189,7 +187,7 @@ async def main() -> None: # Execute the workflow. Since the start is an AgentExecutor, pass an AgentExecutorRequest. # The workflow completes when it becomes idle (no more work to do). - request = AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=email)], should_respond=True) + request = AgentExecutorRequest(messages=[ChatMessage("user", text=email)], should_respond=True) events = await workflow.run(request) outputs = events.get_outputs() if outputs: diff --git a/python/samples/getting_started/workflows/control-flow/multi_selection_edge_group.py b/python/samples/getting_started/workflows/control-flow/multi_selection_edge_group.py index 44385bffca..65f6c9c77f 100644 --- a/python/samples/getting_started/workflows/control-flow/multi_selection_edge_group.py +++ b/python/samples/getting_started/workflows/control-flow/multi_selection_edge_group.py @@ -13,13 +13,11 @@ from agent_framework import ( AgentExecutorResponse, ChatAgent, ChatMessage, - Role, WorkflowBuilder, WorkflowContext, WorkflowEvent, WorkflowOutputEvent, executor, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -93,7 +91,7 @@ async def store_email(email_text: str, ctx: WorkflowContext[AgentExecutorRequest await ctx.set_shared_state(CURRENT_EMAIL_ID_KEY, new_email.email_id) await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=new_email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[ChatMessage("user", text=new_email.email_content)], should_respond=True) ) @@ -120,7 +118,7 @@ async def submit_to_email_assistant(analysis: AnalysisResult, ctx: WorkflowConte email: Email = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{analysis.email_id}") await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[ChatMessage("user", text=email.email_content)], should_respond=True) ) @@ -135,7 +133,7 @@ async def summarize_email(analysis: AnalysisResult, ctx: WorkflowContext[AgentEx # Only called for long NotSpam emails by selection_func email: Email = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{analysis.email_id}") await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[ChatMessage("user", text=email.email_content)], should_respond=True) ) diff --git a/python/samples/getting_started/workflows/control-flow/sequential_executors.py b/python/samples/getting_started/workflows/control-flow/sequential_executors.py index 0fedfcf1cd..e422009766 100644 --- a/python/samples/getting_started/workflows/control-flow/sequential_executors.py +++ b/python/samples/getting_started/workflows/control-flow/sequential_executors.py @@ -9,7 +9,6 @@ from agent_framework import ( WorkflowContext, WorkflowOutputEvent, handler, - tool, ) from typing_extensions import Never diff --git a/python/samples/getting_started/workflows/control-flow/simple_loop.py b/python/samples/getting_started/workflows/control-flow/simple_loop.py index d458589123..348a014f9f 100644 --- a/python/samples/getting_started/workflows/control-flow/simple_loop.py +++ b/python/samples/getting_started/workflows/control-flow/simple_loop.py @@ -10,11 +10,9 @@ from agent_framework import ( ChatMessage, Executor, ExecutorCompletedEvent, - Role, WorkflowBuilder, WorkflowContext, handler, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -97,7 +95,7 @@ class SubmitToJudgeAgent(Executor): f"Target: {self._target}\nGuess: {guess}\nResponse:" ) await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=prompt)], should_respond=True), + AgentExecutorRequest(messages=[ChatMessage("user", text=prompt)], should_respond=True), target_id=self._judge_agent_id, ) diff --git a/python/samples/getting_started/workflows/control-flow/switch_case_edge_group.py b/python/samples/getting_started/workflows/control-flow/switch_case_edge_group.py index f2090e4acc..3fe613e6f8 100644 --- a/python/samples/getting_started/workflows/control-flow/switch_case_edge_group.py +++ b/python/samples/getting_started/workflows/control-flow/switch_case_edge_group.py @@ -13,12 +13,10 @@ from agent_framework import ( # Core chat primitives used to form LLM requests ChatAgent, # Case entry for a switch-case edge group ChatMessage, Default, # Default branch when no cases match - Role, WorkflowBuilder, # Fluent builder for assembling the graph WorkflowContext, # Per-run context and event bus executor, # Decorator to turn a function into a workflow executor - tool, -) + ) from agent_framework.azure import AzureOpenAIChatClient # Thin client for Azure OpenAI chat models from azure.identity import AzureCliCredential # Uses your az CLI login for credentials from pydantic import BaseModel # Structured outputs with validation @@ -100,7 +98,7 @@ async def store_email(email_text: str, ctx: WorkflowContext[AgentExecutorRequest # Kick off the detector by forwarding the email as a user message to the spam_detection_agent. await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=new_email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[ChatMessage("user", text=new_email.email_content)], should_respond=True) ) @@ -121,7 +119,7 @@ async def submit_to_email_assistant(detection: DetectionResult, ctx: WorkflowCon # Load the original content from shared state using the id carried in DetectionResult. email: Email = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{detection.email_id}") await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[ChatMessage("user", text=email.email_content)], should_respond=True) ) diff --git a/python/samples/getting_started/workflows/declarative/customer_support/ticketing_plugin.py b/python/samples/getting_started/workflows/declarative/customer_support/ticketing_plugin.py index 8d1db72c2f..f25f1b473d 100644 --- a/python/samples/getting_started/workflows/declarative/customer_support/ticketing_plugin.py +++ b/python/samples/getting_started/workflows/declarative/customer_support/ticketing_plugin.py @@ -3,9 +3,9 @@ """Ticketing plugin for CustomerSupport workflow.""" import uuid +from collections.abc import Callable from dataclasses import dataclass from enum import Enum -from collections.abc import Callable # ANSI color codes MAGENTA = "\033[35m" diff --git a/python/samples/getting_started/workflows/declarative/function_tools/main.py b/python/samples/getting_started/workflows/declarative/function_tools/main.py index ea647e7f21..180175063e 100644 --- a/python/samples/getting_started/workflows/declarative/function_tools/main.py +++ b/python/samples/getting_started/workflows/declarative/function_tools/main.py @@ -10,8 +10,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Annotated, Any -from agent_framework import FileCheckpointStorage, RequestInfoEvent, WorkflowOutputEvent -from agent_framework import tool +from agent_framework import FileCheckpointStorage, RequestInfoEvent, WorkflowOutputEvent, tool from agent_framework.azure import AzureOpenAIChatClient from agent_framework_declarative import ExternalInputRequest, ExternalInputResponse, WorkflowFactory from azure.identity import AzureCliCredential @@ -38,17 +37,20 @@ MENU_ITEMS = [ MenuItem(category="Drink", name="Soda", price=1.95, is_special=False), ] + # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_menu() -> list[dict[str, Any]]: """Get all menu items.""" return [{"category": i.category, "name": i.name, "price": i.price} for i in MENU_ITEMS] + @tool(approval_mode="never_require") def get_specials() -> list[dict[str, Any]]: """Get today's specials.""" return [{"category": i.category, "name": i.name, "price": i.price} for i in MENU_ITEMS if i.is_special] + @tool(approval_mode="never_require") def get_item_price(name: Annotated[str, Field(description="Menu item name")]) -> str: """Get price of a menu item.""" diff --git a/python/samples/getting_started/workflows/human-in-the-loop/agents_with_approval_requests.py b/python/samples/getting_started/workflows/human-in-the-loop/agents_with_approval_requests.py index c49d2c1308..24d39f02ae 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/agents_with_approval_requests.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/agents_with_approval_requests.py @@ -13,9 +13,9 @@ from agent_framework import ( Executor, WorkflowBuilder, WorkflowContext, - tool, executor, handler, + tool, ) from agent_framework.openai import OpenAIChatClient diff --git a/python/samples/getting_started/workflows/human-in-the-loop/concurrent_request_info.py b/python/samples/getting_started/workflows/human-in-the-loop/concurrent_request_info.py index 5aca9f8848..752956d0f2 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/concurrent_request_info.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/concurrent_request_info.py @@ -29,11 +29,9 @@ from agent_framework import ( ChatMessage, ConcurrentBuilder, RequestInfoEvent, - Role, WorkflowOutputEvent, WorkflowRunState, WorkflowStatusEvent, - tool, ) from agent_framework._workflows._agent_executor import AgentExecutorResponse from agent_framework.azure import AzureOpenAIChatClient @@ -72,7 +70,7 @@ async def aggregate_with_synthesis(results: list[AgentExecutorResponse]) -> Any: # Check for human feedback in the conversation (will be last user message if present) if r.full_conversation: for msg in reversed(r.full_conversation): - if msg.role == Role.USER and msg.text and "perspectives" not in msg.text.lower(): + if msg.role == "user" and msg.text and "perspectives" not in msg.text.lower(): human_guidance = msg.text break except Exception: @@ -82,14 +80,14 @@ async def aggregate_with_synthesis(results: list[AgentExecutorResponse]) -> Any: guidance_text = f"\n\nHuman guidance: {human_guidance}" if human_guidance else "" system_msg = ChatMessage( - Role.SYSTEM, + "system", text=( "You are a synthesis expert. Consolidate the following analyst perspectives " "into one cohesive, balanced summary (3-4 sentences). If human guidance is provided, " "prioritize aspects as directed." ), ) - user_msg = ChatMessage(Role.USER, text="\n\n".join(expert_sections) + guidance_text) + user_msg = ChatMessage("user", text="\n\n".join(expert_sections) + guidance_text) response = await _chat_client.get_response([system_msg, user_msg]) return response.messages[-1].text if response.messages else "" @@ -174,7 +172,7 @@ async def main() -> None: else event.data.full_conversation ) for msg in recent: - name = msg.author_name or msg.role.value + name = msg.author_name or msg.role text = (msg.text or "")[:150] print(f" [{name}]: {text}...") print("-" * 40) diff --git a/python/samples/getting_started/workflows/human-in-the-loop/group_chat_request_info.py b/python/samples/getting_started/workflows/human-in-the-loop/group_chat_request_info.py index fcc1d1460c..5d36fbd13a 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/group_chat_request_info.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/group_chat_request_info.py @@ -35,7 +35,6 @@ from agent_framework import ( WorkflowOutputEvent, WorkflowRunState, WorkflowStatusEvent, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -164,7 +163,7 @@ async def main() -> None: if event.data: messages: list[ChatMessage] = event.data for msg in messages: - role = msg.role.value.capitalize() + role = msg.role.capitalize() name = msg.author_name or "unknown" text = (msg.text or "")[:200] print(f"[{role}][{name}]: {text}...") diff --git a/python/samples/getting_started/workflows/human-in-the-loop/guessing_game_with_human_input.py b/python/samples/getting_started/workflows/human-in-the-loop/guessing_game_with_human_input.py index 52a9d72901..dba7f56b66 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/guessing_game_with_human_input.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/guessing_game_with_human_input.py @@ -10,7 +10,6 @@ from agent_framework import ( ChatMessage, # Chat message structure Executor, # Base class for workflow executors RequestInfoEvent, # Event emitted when human input is requested - Role, # Enum of chat roles (user, assistant, system) WorkflowBuilder, # Fluent builder for assembling the graph WorkflowContext, # Per run context and event bus WorkflowOutputEvent, # Event emitted when workflow yields output @@ -18,8 +17,7 @@ from agent_framework import ( WorkflowStatusEvent, # Event emitted on run state changes handler, response_handler, # Decorator to expose an Executor method as a step - tool, -) + ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential from pydantic import BaseModel @@ -88,7 +86,7 @@ class TurnManager(Executor): - Input is a simple starter token (ignored here). - Output is an AgentExecutorRequest that triggers the agent to produce a guess. """ - user = ChatMessage(Role.USER, text="Start by making your first guess.") + user = ChatMessage("user", text="Start by making your first guess.") await ctx.send_message(AgentExecutorRequest(messages=[user], should_respond=True)) @handler @@ -138,7 +136,7 @@ class TurnManager(Executor): # Provide feedback to the agent to try again. # We keep the agent's output strictly JSON to ensure stable parsing on the next turn. user_msg = ChatMessage( - Role.USER, + "user", text=(f'Feedback: {reply}. Return ONLY a JSON object matching the schema {{"guess": }}.'), ) await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True)) diff --git a/python/samples/getting_started/workflows/human-in-the-loop/sequential_request_info.py b/python/samples/getting_started/workflows/human-in-the-loop/sequential_request_info.py index 401c24b5dd..afb19753e5 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/sequential_request_info.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/sequential_request_info.py @@ -32,7 +32,6 @@ from agent_framework import ( WorkflowOutputEvent, WorkflowRunState, WorkflowStatusEvent, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -109,7 +108,7 @@ async def main() -> None: else event.data.full_conversation ) for msg in recent: - name = msg.author_name or msg.role.value + name = msg.author_name or msg.role text = (msg.text or "")[:150] print(f" [{name}]: {text}...") print("-" * 40) @@ -132,7 +131,7 @@ async def main() -> None: if event.data: messages: list[ChatMessage] = event.data[-3:] for msg in messages: - role = msg.role.value if msg.role else "unknown" + role = msg.role if msg.role else "unknown" print(f"[{role}]: {msg.text}") workflow_complete = True diff --git a/python/samples/getting_started/workflows/observability/executor_io_observation.py b/python/samples/getting_started/workflows/observability/executor_io_observation.py index 54645f237d..0237f294f2 100644 --- a/python/samples/getting_started/workflows/observability/executor_io_observation.py +++ b/python/samples/getting_started/workflows/observability/executor_io_observation.py @@ -11,7 +11,6 @@ from agent_framework import ( WorkflowContext, WorkflowOutputEvent, handler, - tool, ) from typing_extensions import Never diff --git a/python/samples/getting_started/workflows/orchestration/concurrent_custom_agent_executors.py b/python/samples/getting_started/workflows/orchestration/concurrent_custom_agent_executors.py index caf97c7f8f..76203dba63 100644 --- a/python/samples/getting_started/workflows/orchestration/concurrent_custom_agent_executors.py +++ b/python/samples/getting_started/workflows/orchestration/concurrent_custom_agent_executors.py @@ -12,7 +12,6 @@ from agent_framework import ( Executor, WorkflowContext, handler, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential diff --git a/python/samples/getting_started/workflows/orchestration/concurrent_custom_aggregator.py b/python/samples/getting_started/workflows/orchestration/concurrent_custom_aggregator.py index def89043eb..1690c2baad 100644 --- a/python/samples/getting_started/workflows/orchestration/concurrent_custom_aggregator.py +++ b/python/samples/getting_started/workflows/orchestration/concurrent_custom_aggregator.py @@ -3,7 +3,7 @@ import asyncio from typing import Any -from agent_framework import ChatMessage, ConcurrentBuilder, Role +from agent_framework import ChatMessage, ConcurrentBuilder from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -66,13 +66,13 @@ async def main() -> None: # Ask the model to synthesize a concise summary of the experts' outputs system_msg = ChatMessage( - Role.SYSTEM, + "system", text=( "You are a helpful assistant that consolidates multiple domain expert outputs " "into one cohesive, concise summary with clear takeaways. Keep it under 200 words." ), ) - user_msg = ChatMessage(Role.USER, text="\n\n".join(expert_sections)) + user_msg = ChatMessage("user", text="\n\n".join(expert_sections)) response = await chat_client.get_response([system_msg, user_msg]) # Return the model's final assistant text as the completion result diff --git a/python/samples/getting_started/workflows/orchestration/concurrent_participant_factory.py b/python/samples/getting_started/workflows/orchestration/concurrent_participant_factory.py index aaa05a37a9..941456a823 100644 --- a/python/samples/getting_started/workflows/orchestration/concurrent_participant_factory.py +++ b/python/samples/getting_started/workflows/orchestration/concurrent_participant_factory.py @@ -8,11 +8,9 @@ from agent_framework import ( ChatMessage, ConcurrentBuilder, Executor, - Role, Workflow, WorkflowContext, handler, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -98,13 +96,13 @@ class SummarizationExecutor(Executor): # Ask the model to synthesize a concise summary of the experts' outputs system_msg = ChatMessage( - Role.SYSTEM, + "system", text=( "You are a helpful assistant that consolidates multiple domain expert outputs " "into one cohesive, concise summary with clear takeaways. Keep it under 200 words." ), ) - user_msg = ChatMessage(Role.USER, text="\n\n".join(expert_sections)) + user_msg = ChatMessage("user", text="\n\n".join(expert_sections)) response = await self.chat_client.get_response([system_msg, user_msg]) diff --git a/python/samples/getting_started/workflows/orchestration/group_chat_agent_manager.py b/python/samples/getting_started/workflows/orchestration/group_chat_agent_manager.py index 926c787aaa..cdc03a5ea5 100644 --- a/python/samples/getting_started/workflows/orchestration/group_chat_agent_manager.py +++ b/python/samples/getting_started/workflows/orchestration/group_chat_agent_manager.py @@ -7,9 +7,7 @@ from agent_framework import ( ChatAgent, ChatMessage, GroupChatBuilder, - Role, WorkflowOutputEvent, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -73,7 +71,7 @@ async def main() -> None: .participants([researcher, writer]) # Set a hard termination condition: stop after 4 assistant messages # The agent orchestrator will intelligently decide when to end before this limit but just in case - .with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == Role.ASSISTANT) >= 4) + .with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 4) .build() ) diff --git a/python/samples/getting_started/workflows/orchestration/group_chat_philosophical_debate.py b/python/samples/getting_started/workflows/orchestration/group_chat_philosophical_debate.py index 9be9192a57..de613dea2e 100644 --- a/python/samples/getting_started/workflows/orchestration/group_chat_philosophical_debate.py +++ b/python/samples/getting_started/workflows/orchestration/group_chat_philosophical_debate.py @@ -9,9 +9,7 @@ from agent_framework import ( ChatAgent, ChatMessage, GroupChatBuilder, - Role, WorkflowOutputEvent, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -214,7 +212,7 @@ Share your perspective authentically. Feel free to: GroupChatBuilder() .with_orchestrator(agent=moderator) .participants([farmer, developer, teacher, activist, spiritual_leader, artist, immigrant, doctor]) - .with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == Role.ASSISTANT) >= 10) + .with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 10) .build() ) diff --git a/python/samples/getting_started/workflows/orchestration/group_chat_simple_selector.py b/python/samples/getting_started/workflows/orchestration/group_chat_simple_selector.py index cf64ef0aca..1047cd6f22 100644 --- a/python/samples/getting_started/workflows/orchestration/group_chat_simple_selector.py +++ b/python/samples/getting_started/workflows/orchestration/group_chat_simple_selector.py @@ -9,7 +9,6 @@ from agent_framework import ( GroupChatBuilder, GroupChatState, WorkflowOutputEvent, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential diff --git a/python/samples/getting_started/workflows/orchestration/handoff_autonomous.py b/python/samples/getting_started/workflows/orchestration/handoff_autonomous.py index edab013700..e33b230ce7 100644 --- a/python/samples/getting_started/workflows/orchestration/handoff_autonomous.py +++ b/python/samples/getting_started/workflows/orchestration/handoff_autonomous.py @@ -14,7 +14,6 @@ from agent_framework import ( WorkflowEvent, WorkflowOutputEvent, resolve_agent_id, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -95,7 +94,7 @@ def _display_event(event: WorkflowEvent) -> None: conversation = cast(list[ChatMessage], event.data) print("\n=== Final Conversation (Autonomous with Iteration) ===") for message in conversation: - speaker = message.author_name or message.role.value + speaker = message.author_name or message.role text_preview = message.text[:200] + "..." if len(message.text) > 200 else message.text print(f"- {speaker}: {text_preview}") print(f"\nTotal messages: {len(conversation)}") @@ -131,7 +130,7 @@ async def main() -> None: ) .with_termination_condition( # Terminate after coordinator provides 5 assistant responses - lambda conv: sum(1 for msg in conv if msg.author_name == "coordinator" and msg.role.value == "assistant") + lambda conv: sum(1 for msg in conv if msg.author_name == "coordinator" and msg.role == "assistant") >= 5 ) .build() diff --git a/python/samples/getting_started/workflows/orchestration/handoff_participant_factory.py b/python/samples/getting_started/workflows/orchestration/handoff_participant_factory.py index dd4e4054c8..9107e217c6 100644 --- a/python/samples/getting_started/workflows/orchestration/handoff_participant_factory.py +++ b/python/samples/getting_started/workflows/orchestration/handoff_participant_factory.py @@ -131,7 +131,7 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]: if not message.text: # Skip messages without text (e.g., tool calls) continue - speaker = message.author_name or message.role.value + speaker = message.author_name or message.role print(f"- {speaker}: {message.text}") # HandoffSentEvent: Indicates a handoff has been initiated @@ -151,7 +151,7 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]: if isinstance(conversation, list): print("\n=== Final Conversation Snapshot ===") for message in conversation: - speaker = message.author_name or message.role.value + speaker = message.author_name or message.role print(f"- {speaker}: {message.text or [content.type for content in message.contents]}") print("===================================") @@ -183,7 +183,7 @@ def _print_handoff_agent_user_request(response: AgentResponse) -> None: if not message.text: # Skip messages without text (e.g., tool calls) continue - speaker = message.author_name or message.role.value + speaker = message.author_name or message.role print(f"- {speaker}: {message.text}") diff --git a/python/samples/getting_started/workflows/orchestration/handoff_simple.py b/python/samples/getting_started/workflows/orchestration/handoff_simple.py index 72ea035a4f..2e7f53a82d 100644 --- a/python/samples/getting_started/workflows/orchestration/handoff_simple.py +++ b/python/samples/getting_started/workflows/orchestration/handoff_simple.py @@ -126,7 +126,7 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]: if not message.text: # Skip messages without text (e.g., tool calls) continue - speaker = message.author_name or message.role.value + speaker = message.author_name or message.role print(f"- {speaker}: {message.text}") # HandoffSentEvent: Indicates a handoff has been initiated @@ -146,7 +146,7 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]: if isinstance(conversation, list): print("\n=== Final Conversation Snapshot ===") for message in conversation: - speaker = message.author_name or message.role.value + speaker = message.author_name or message.role print(f"- {speaker}: {message.text or [content.type for content in message.contents]}") print("===================================") @@ -178,7 +178,7 @@ def _print_handoff_agent_user_request(response: AgentResponse) -> None: if not message.text: # Skip messages without text (e.g., tool calls) continue - speaker = message.author_name or message.role.value + speaker = message.author_name or message.role print(f"- {speaker}: {message.text}") diff --git a/python/samples/getting_started/workflows/orchestration/handoff_with_code_interpreter_file.py b/python/samples/getting_started/workflows/orchestration/handoff_with_code_interpreter_file.py index 54f7f4504c..0c0616850b 100644 --- a/python/samples/getting_started/workflows/orchestration/handoff_with_code_interpreter_file.py +++ b/python/samples/getting_started/workflows/orchestration/handoff_with_code_interpreter_file.py @@ -41,7 +41,6 @@ from agent_framework import ( WorkflowEvent, WorkflowRunState, WorkflowStatusEvent, - tool, ) from azure.identity.aio import AzureCliCredential @@ -157,7 +156,7 @@ async def main() -> None: HandoffBuilder() .participants([triage, code_specialist]) .with_start_agent(triage) - .with_termination_condition(lambda conv: sum(1 for msg in conv if msg.role.value == "user") >= 2) + .with_termination_condition(lambda conv: sum(1 for msg in conv if msg.role == "user") >= 2) .build() ) diff --git a/python/samples/getting_started/workflows/orchestration/magentic.py b/python/samples/getting_started/workflows/orchestration/magentic.py index d153d41d9c..60746bc113 100644 --- a/python/samples/getting_started/workflows/orchestration/magentic.py +++ b/python/samples/getting_started/workflows/orchestration/magentic.py @@ -15,7 +15,6 @@ from agent_framework import ( MagenticOrchestratorEvent, MagenticProgressLedger, WorkflowOutputEvent, - tool, ) from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient diff --git a/python/samples/getting_started/workflows/orchestration/magentic_checkpoint.py b/python/samples/getting_started/workflows/orchestration/magentic_checkpoint.py index 3c68931a18..2dd6a1a170 100644 --- a/python/samples/getting_started/workflows/orchestration/magentic_checkpoint.py +++ b/python/samples/getting_started/workflows/orchestration/magentic_checkpoint.py @@ -16,7 +16,6 @@ from agent_framework import ( WorkflowOutputEvent, WorkflowRunState, WorkflowStatusEvent, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity._credentials import AzureCliCredential diff --git a/python/samples/getting_started/workflows/orchestration/magentic_human_plan_review.py b/python/samples/getting_started/workflows/orchestration/magentic_human_plan_review.py index bba6913a3b..1050463d01 100644 --- a/python/samples/getting_started/workflows/orchestration/magentic_human_plan_review.py +++ b/python/samples/getting_started/workflows/orchestration/magentic_human_plan_review.py @@ -12,7 +12,6 @@ from agent_framework import ( MagenticPlanReviewRequest, RequestInfoEvent, WorkflowOutputEvent, - tool, ) from agent_framework.openai import OpenAIChatClient diff --git a/python/samples/getting_started/workflows/orchestration/sequential_agents.py b/python/samples/getting_started/workflows/orchestration/sequential_agents.py index 64ccbc6150..59a9cb5bdd 100644 --- a/python/samples/getting_started/workflows/orchestration/sequential_agents.py +++ b/python/samples/getting_started/workflows/orchestration/sequential_agents.py @@ -3,7 +3,7 @@ import asyncio from typing import cast -from agent_framework import ChatMessage, Role, SequentialBuilder, WorkflowOutputEvent +from agent_framework import ChatMessage, SequentialBuilder, WorkflowOutputEvent from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -53,7 +53,7 @@ async def main() -> None: if outputs: print("===== Final Conversation =====") for i, msg in enumerate(outputs[-1], start=1): - name = msg.author_name or ("assistant" if msg.role == Role.ASSISTANT else "user") + name = msg.author_name or ("assistant" if msg.role == "assistant" else "user") print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}") """ diff --git a/python/samples/getting_started/workflows/orchestration/sequential_custom_executors.py b/python/samples/getting_started/workflows/orchestration/sequential_custom_executors.py index b29cec6d83..09454f8b12 100644 --- a/python/samples/getting_started/workflows/orchestration/sequential_custom_executors.py +++ b/python/samples/getting_started/workflows/orchestration/sequential_custom_executors.py @@ -7,11 +7,9 @@ from agent_framework import ( AgentExecutorResponse, ChatMessage, Executor, - Role, SequentialBuilder, WorkflowContext, handler, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -48,12 +46,12 @@ class Summarizer(Executor): the output must be `list[ChatMessage]`. """ if not agent_response.full_conversation: - await ctx.send_message([ChatMessage(role=Role.ASSISTANT, text="No conversation to summarize.")]) + await ctx.send_message([ChatMessage("assistant", ["No conversation to summarize."])]) return - users = sum(1 for m in agent_response.full_conversation if m.role == Role.USER) - assistants = sum(1 for m in agent_response.full_conversation if m.role == Role.ASSISTANT) - summary = ChatMessage(role=Role.ASSISTANT, text=f"Summary -> users:{users} assistants:{assistants}") + users = sum(1 for m in agent_response.full_conversation if m.role == "user") + assistants = sum(1 for m in agent_response.full_conversation if m.role == "assistant") + summary = ChatMessage("assistant", [f"Summary -> users:{users} assistants:{assistants}"]) final_conversation = list(agent_response.full_conversation) + [summary] await ctx.send_message(final_conversation) @@ -78,7 +76,7 @@ async def main() -> None: print("===== Final Conversation =====") messages: list[ChatMessage] | Any = outputs[0] for i, msg in enumerate(messages, start=1): - name = msg.author_name or ("assistant" if msg.role == Role.ASSISTANT else "user") + name = msg.author_name or ("assistant" if msg.role == "assistant" else "user") print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}") """ diff --git a/python/samples/getting_started/workflows/orchestration/sequential_participant_factory.py b/python/samples/getting_started/workflows/orchestration/sequential_participant_factory.py index 6cf87bf21c..8b78a38926 100644 --- a/python/samples/getting_started/workflows/orchestration/sequential_participant_factory.py +++ b/python/samples/getting_started/workflows/orchestration/sequential_participant_factory.py @@ -6,12 +6,10 @@ from agent_framework import ( ChatAgent, ChatMessage, Executor, - Role, SequentialBuilder, Workflow, WorkflowContext, handler, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -64,7 +62,7 @@ async def run_workflow(workflow: Workflow, query: str) -> None: if outputs: messages: list[ChatMessage] = outputs[0] for message in messages: - name = message.author_name or ("assistant" if message.role == Role.ASSISTANT else "user") + name = message.author_name or ("assistant" if message.role == "assistant" else "user") print(f"{name}: {message.text}") else: raise RuntimeError("No outputs received from the workflow.") diff --git a/python/samples/getting_started/workflows/parallelism/fan_out_fan_in_edges.py b/python/samples/getting_started/workflows/parallelism/fan_out_fan_in_edges.py index 36c2ca24f6..f2ed5ad677 100644 --- a/python/samples/getting_started/workflows/parallelism/fan_out_fan_in_edges.py +++ b/python/samples/getting_started/workflows/parallelism/fan_out_fan_in_edges.py @@ -11,13 +11,11 @@ from agent_framework import ( # Core chat primitives to build LLM requests Executor, # Base class for custom Python executors ExecutorCompletedEvent, ExecutorInvokedEvent, - Role, # Enum of chat roles (user, assistant, system) WorkflowBuilder, # Fluent builder for wiring the workflow graph WorkflowContext, # Per run context and event bus WorkflowOutputEvent, # Event emitted when workflow yields output handler, # Decorator to mark an Executor method as invokable - tool, -) + ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential # Uses your az CLI login for credentials from typing_extensions import Never @@ -47,7 +45,7 @@ class DispatchToExperts(Executor): @handler async def dispatch(self, prompt: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None: # Wrap the incoming prompt as a user message for each expert and request a response. - initial_message = ChatMessage(Role.USER, text=prompt) + initial_message = ChatMessage("user", text=prompt) await ctx.send_message(AgentExecutorRequest(messages=[initial_message], should_respond=True)) diff --git a/python/samples/getting_started/workflows/parallelism/map_reduce_and_visualization.py b/python/samples/getting_started/workflows/parallelism/map_reduce_and_visualization.py index d98c6cb78b..9b46e74bd2 100644 --- a/python/samples/getting_started/workflows/parallelism/map_reduce_and_visualization.py +++ b/python/samples/getting_started/workflows/parallelism/map_reduce_and_visualization.py @@ -14,8 +14,7 @@ from agent_framework import ( WorkflowOutputEvent, # Event emitted when workflow yields output WorkflowViz, # Utility to visualize a workflow graph handler, # Decorator to expose an Executor method as a step - tool, -) + ) from typing_extensions import Never """ diff --git a/python/samples/getting_started/workflows/state-management/shared_states_with_agents.py b/python/samples/getting_started/workflows/state-management/shared_states_with_agents.py index 700dcb1b95..3a243f54ab 100644 --- a/python/samples/getting_started/workflows/state-management/shared_states_with_agents.py +++ b/python/samples/getting_started/workflows/state-management/shared_states_with_agents.py @@ -11,11 +11,9 @@ from agent_framework import ( AgentExecutorResponse, ChatAgent, ChatMessage, - Role, WorkflowBuilder, WorkflowContext, executor, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -105,7 +103,7 @@ async def store_email(email_text: str, ctx: WorkflowContext[AgentExecutorRequest await ctx.set_shared_state(CURRENT_EMAIL_ID_KEY, new_email.email_id) await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=new_email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[ChatMessage("user", text=new_email.email_content)], should_respond=True) ) @@ -136,7 +134,7 @@ async def submit_to_email_assistant(detection: DetectionResult, ctx: WorkflowCon # Load the original content by id from shared state and forward it to the assistant. email: Email = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{detection.email_id}") await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[ChatMessage("user", text=email.email_content)], should_respond=True) ) diff --git a/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py b/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py index b43e01916f..4e202026fb 100644 --- a/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py +++ b/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py @@ -97,7 +97,7 @@ def _print_output(event: WorkflowOutputEvent) -> None: print("Workflow completed. Aggregated results from both agents:") for msg in messages: if msg.text: - print(f"- {msg.author_name or msg.role.value}: {msg.text}") + print(f"- {msg.author_name or msg.role}: {msg.text}") async def main() -> None: diff --git a/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py b/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py index 7712873943..30c6b2358f 100644 --- a/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py +++ b/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py @@ -116,7 +116,7 @@ async def main() -> None: print("\n" + "-" * 60) print("Workflow completed. Final conversation:") for msg in output: - role = msg.role.value if hasattr(msg.role, "value") else msg.role + role = msg.role if hasattr(msg.role, "value") else msg.role text = msg.text[:200] + "..." if len(msg.text) > 200 else msg.text print(f" [{role}]: {text}") else: diff --git a/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py b/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py index 877bb13038..68b68c4a7a 100644 --- a/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py +++ b/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py @@ -9,12 +9,10 @@ from agent_framework import ( ChatAgent, ChatMessage, Executor, - Role, WorkflowBuilder, WorkflowContext, WorkflowViz, handler, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -41,7 +39,7 @@ class DispatchToExperts(Executor): @handler async def dispatch(self, prompt: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None: # Wrap the incoming prompt as a user message for each expert and request a response. - initial_message = ChatMessage(Role.USER, text=prompt) + initial_message = ChatMessage("user", text=prompt) await ctx.send_message(AgentExecutorRequest(messages=[initial_message], should_respond=True)) diff --git a/python/samples/semantic-kernel-migration/orchestrations/handoff.py b/python/samples/semantic-kernel-migration/orchestrations/handoff.py index bd4cfccec4..a90c8acf14 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/handoff.py +++ b/python/samples/semantic-kernel-migration/orchestrations/handoff.py @@ -13,7 +13,6 @@ from agent_framework import ( RequestInfoEvent, WorkflowEvent, WorkflowOutputEvent, - tool, ) from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -269,7 +268,7 @@ async def run_agent_framework_example(initial_task: str, scripted_responses: Seq text = message.text or "" if not text.strip(): continue - speaker = message.author_name or message.role.value + speaker = message.author_name or message.role lines.append(f"{speaker}: {text}") return "\n".join(lines) diff --git a/python/samples/semantic-kernel-migration/orchestrations/sequential.py b/python/samples/semantic-kernel-migration/orchestrations/sequential.py index 0a2bafb3bb..3b66ab2538 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/sequential.py +++ b/python/samples/semantic-kernel-migration/orchestrations/sequential.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Sequence from typing import cast -from agent_framework import ChatMessage, Role, SequentialBuilder, WorkflowOutputEvent +from agent_framework import ChatMessage, SequentialBuilder, WorkflowOutputEvent from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential from semantic_kernel.agents import Agent, ChatCompletionAgent, SequentialOrchestration @@ -109,7 +109,7 @@ def _format_conversation(conversation: list[ChatMessage]) -> None: print("===== Agent Framework Sequential =====") for index, message in enumerate(conversation, start=1): - name = message.author_name or ("assistant" if message.role == Role.ASSISTANT else "user") + name = message.author_name or ("assistant" if message.role == "assistant" else "user") print(f"{'-' * 60}\n{index:02d} [{name}]\n{message.text}") print() diff --git a/python/samples/semantic-kernel-migration/processes/nested_process.py b/python/samples/semantic-kernel-migration/processes/nested_process.py index e649103703..884ee6f4b0 100644 --- a/python/samples/semantic-kernel-migration/processes/nested_process.py +++ b/python/samples/semantic-kernel-migration/processes/nested_process.py @@ -19,7 +19,6 @@ from agent_framework import ( WorkflowExecutor, WorkflowOutputEvent, handler, - tool, ) from pydantic import BaseModel, Field diff --git a/python/tests/samples/getting_started/test_agent_samples.py b/python/tests/samples/getting_started/test_agent_samples.py index e1a8595193..1042dafae7 100644 --- a/python/tests/samples/getting_started/test_agent_samples.py +++ b/python/tests/samples/getting_started/test_agent_samples.py @@ -16,9 +16,6 @@ from samples.getting_started.agents.azure_ai.azure_ai_with_function_tools import from samples.getting_started.agents.azure_ai.azure_ai_with_function_tools import ( tools_on_run_level as azure_ai_with_function_tools_run, ) -from samples.getting_started.agents.azure_ai.azure_ai_with_local_mcp import ( - main as azure_ai_with_local_mcp, -) from samples.getting_started.agents.azure_ai.azure_ai_basic import ( main as azure_ai_basic, @@ -32,6 +29,9 @@ from samples.getting_started.agents.azure_ai.azure_ai_with_existing_agent import from samples.getting_started.agents.azure_ai.azure_ai_with_explicit_settings import ( main as azure_ai_with_explicit_settings, ) +from samples.getting_started.agents.azure_ai.azure_ai_with_local_mcp import ( + main as azure_ai_with_local_mcp, +) from samples.getting_started.agents.azure_ai.azure_ai_with_thread import ( main as azure_ai_with_thread, )