mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: added ChatClientBase with function calling (#147)
* added ChatClientBase with function calling * streaming update * fixed typing * test setup * small update * src setup * removed src, updated test naming * fixed test command * alolow args * updated test run * added unit test folder to azure * added init and unit test to azure * added other cross tests * restructured * reset test run * fix name * removed always * updated test * extend pytest.xml locations * run surface always * added decorators for FC and marked tests * fixed mypy settings and added tests * fix override import * removed import
This commit is contained in:
committed by
GitHub
Unverified
parent
daf4788868
commit
3449902b03
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
|
||||
__all__ = ["__version__"]
|
||||
@@ -0,0 +1,75 @@
|
||||
[project]
|
||||
name = "agent-framework-azure"
|
||||
description = "Azure integrations for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "SK-Support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "0.1.0b1"
|
||||
license = {file = "../../LICENSE"}
|
||||
urls.homepage = "https://learn.microsoft.com/en-us/semantic-kernel/overview/"
|
||||
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 :: 5 - Production/Stable",
|
||||
"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",
|
||||
"Framework :: Pydantic :: 2",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework",
|
||||
"agent-framework-openai"
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = 'tests'
|
||||
addopts = "-ra -q -r fEX"
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
filterwarnings = []
|
||||
timeout = 120
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
|
||||
[tool.pyright]
|
||||
extend = "../../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
|
||||
disallow_any_unimported = true
|
||||
|
||||
[tool.bandit]
|
||||
targets = ["agent_framework"]
|
||||
exclude_dirs = ["tests"]
|
||||
|
||||
[tool.poe]
|
||||
executor.type = "uv"
|
||||
include = "../../shared_tasks.toml"
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-name = "agent_framework.azure"
|
||||
module-root = ""
|
||||
namespace = true
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7.19,<0.8.0"]
|
||||
build-backend = "uv_build"
|
||||
@@ -0,0 +1,32 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from pytest import mark
|
||||
|
||||
|
||||
@mark.xfail(reason="Not solved")
|
||||
def test_self():
|
||||
try:
|
||||
from agent_framework.azure import __version__
|
||||
except ImportError:
|
||||
__version__ = None
|
||||
|
||||
assert __version__ is not None
|
||||
|
||||
|
||||
@mark.xfail(reason="Not solved")
|
||||
def test_openai():
|
||||
try:
|
||||
from agent_framework.openai import __version__
|
||||
except ImportError:
|
||||
__version__ = None
|
||||
assert __version__ is not None
|
||||
|
||||
|
||||
def test_agent_framework():
|
||||
try:
|
||||
from agent_framework import TextContent
|
||||
except ImportError:
|
||||
TextContent = None
|
||||
assert TextContent is not None
|
||||
text = TextContent("Hello, world!")
|
||||
assert text is not None
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python Debugger: Current File",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
|
||||
_IMPORTS = {
|
||||
"get_logger": "._logging",
|
||||
"Agent": "._agents",
|
||||
"AgentThread": "._agents",
|
||||
"AITool": "._tools",
|
||||
"ai_function": "._tools",
|
||||
"AIContent": "._types",
|
||||
"AIContents": "._types",
|
||||
"TextContent": "._types",
|
||||
"TextReasoningContent": "._types",
|
||||
"DataContent": "._types",
|
||||
"UriContent": "._types",
|
||||
"UsageContent": "._types",
|
||||
"UsageDetails": "._types",
|
||||
"FunctionCallContent": "._types",
|
||||
"FunctionResultContent": "._types",
|
||||
"ChatFinishReason": "._types",
|
||||
"ChatMessage": "._types",
|
||||
"ChatResponse": "._types",
|
||||
"StructuredResponse": "._types",
|
||||
"ChatResponseUpdate": "._types",
|
||||
"ChatRole": "._types",
|
||||
"ErrorContent": "._types",
|
||||
"GeneratedEmbeddings": "._types",
|
||||
"ChatOptions": "._types",
|
||||
"ChatToolMode": "._types",
|
||||
"ChatClient": "._clients",
|
||||
"ChatClientBase": "._clients",
|
||||
"use_tool_calling": "._clients",
|
||||
"EmbeddingGenerator": "._clients",
|
||||
"InputGuardrail": ".guard_rails",
|
||||
"OutputGuardrail": ".guard_rails",
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
if name == "__version__":
|
||||
return __version__
|
||||
if name in _IMPORTS:
|
||||
submod_name = _IMPORTS[name]
|
||||
module = importlib.import_module(submod_name, package=__name__)
|
||||
return getattr(module, name)
|
||||
raise AttributeError(f"module {__name__} has no attribute {name}")
|
||||
|
||||
|
||||
def __dir__():
|
||||
return [*list(_IMPORTS.keys()), "__version__"]
|
||||
@@ -0,0 +1,65 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from . import __version__ # type: ignore[attr-defined]
|
||||
from ._agents import Agent, AgentThread
|
||||
from ._clients import ChatClient, ChatClientBase, EmbeddingGenerator, use_tool_calling
|
||||
from ._logging import get_logger
|
||||
from ._tools import AITool, ai_function
|
||||
from ._types import (
|
||||
AIContent,
|
||||
AIContents,
|
||||
ChatFinishReason,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
ChatRole,
|
||||
ChatToolMode,
|
||||
DataContent,
|
||||
ErrorContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
GeneratedEmbeddings,
|
||||
StructuredResponse,
|
||||
TextContent,
|
||||
TextReasoningContent,
|
||||
UriContent,
|
||||
UsageContent,
|
||||
UsageDetails,
|
||||
)
|
||||
from .guard_rails import InputGuardrail, OutputGuardrail
|
||||
|
||||
__all__ = [
|
||||
"AIContent",
|
||||
"AIContents",
|
||||
"AITool",
|
||||
"Agent",
|
||||
"AgentThread",
|
||||
"ChatClient",
|
||||
"ChatClientBase",
|
||||
"ChatFinishReason",
|
||||
"ChatMessage",
|
||||
"ChatOptions",
|
||||
"ChatResponse",
|
||||
"ChatResponseUpdate",
|
||||
"ChatRole",
|
||||
"ChatToolMode",
|
||||
"DataContent",
|
||||
"EmbeddingGenerator",
|
||||
"ErrorContent",
|
||||
"FunctionCallContent",
|
||||
"FunctionResultContent",
|
||||
"GeneratedEmbeddings",
|
||||
"InputGuardrail",
|
||||
"OutputGuardrail",
|
||||
"StructuredResponse",
|
||||
"TextContent",
|
||||
"TextReasoningContent",
|
||||
"UriContent",
|
||||
"UsageContent",
|
||||
"UsageDetails",
|
||||
"__version__",
|
||||
"ai_function",
|
||||
"get_logger",
|
||||
"use_tool_calling",
|
||||
]
|
||||
@@ -0,0 +1,148 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections.abc import AsyncIterable, Sequence
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
from ._pydantic import AFBaseModel
|
||||
from ._types import ChatMessage, ChatResponse, ChatResponseUpdate
|
||||
|
||||
# region AgentThread
|
||||
|
||||
|
||||
class AgentThread(AFBaseModel):
|
||||
"""Base class for agent threads."""
|
||||
|
||||
id: str | None = None
|
||||
|
||||
async def create(self) -> str | None:
|
||||
"""Starts the thread and returns the thread ID."""
|
||||
# If the thread ID is already set, we're done, just return the Id.
|
||||
if self.id is not None:
|
||||
return self.id
|
||||
|
||||
# Otherwise, create the thread.
|
||||
self.id = await self._create()
|
||||
return self.id
|
||||
|
||||
async def delete(self) -> None:
|
||||
"""Ends the current thread."""
|
||||
await self._delete()
|
||||
self.id = None
|
||||
|
||||
async def on_new_message(
|
||||
self,
|
||||
new_messages: ChatMessage | Sequence[ChatMessage],
|
||||
) -> None:
|
||||
"""Invoked when a new message has been contributed to the chat by any participant."""
|
||||
# If the thread is not created yet, create it.
|
||||
if self.id is None:
|
||||
await self.create()
|
||||
|
||||
await self._on_new_message(new_messages=new_messages)
|
||||
|
||||
@abstractmethod
|
||||
async def _create(self) -> str:
|
||||
"""Starts the thread and returns the thread ID."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def _delete(self) -> None:
|
||||
"""Ends the current thread."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def _on_new_message(
|
||||
self,
|
||||
new_messages: ChatMessage | Sequence[ChatMessage],
|
||||
) -> None:
|
||||
"""Invoked when a new message has been contributed to the chat by any participant."""
|
||||
...
|
||||
|
||||
|
||||
# region Agent Protocol
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Agent(Protocol):
|
||||
"""A protocol for an agent that can be invoked."""
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""Returns the ID of the agent."""
|
||||
...
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
"""Returns the name of the agent."""
|
||||
...
|
||||
|
||||
@property
|
||||
def description(self) -> str | None:
|
||||
"""Returns the description of the agent."""
|
||||
...
|
||||
|
||||
@property
|
||||
def instructions(self) -> str | None:
|
||||
"""Returns the instructions for the agent."""
|
||||
...
|
||||
|
||||
async def run(
|
||||
self,
|
||||
messages: str | ChatMessage | list[ChatMessage] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
"""Get a response from the agent.
|
||||
|
||||
This method returns the final result of the agent's execution
|
||||
as a single ChatResponse object. The caller is blocked until
|
||||
the final result is available.
|
||||
|
||||
Note: For streaming responses, use the run_stream method, which returns
|
||||
intermediate steps and the final result as a stream of ChatResponseUpdate
|
||||
objects. Streaming only the final result is not feasible because the timing of
|
||||
the final result's availability is unknown, and blocking the caller until then
|
||||
is undesirable in streaming scenarios.
|
||||
|
||||
Args:
|
||||
messages: The message(s) to send to the agent.
|
||||
thread: The conversation thread associated with the message(s).
|
||||
kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
An agent response item.
|
||||
"""
|
||||
...
|
||||
|
||||
def run_stream(
|
||||
self,
|
||||
messages: str | ChatMessage | list[ChatMessage] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
"""Run the agent as a stream.
|
||||
|
||||
This method will return the intermediate steps and final results of the
|
||||
agent's execution as a stream of ChatResponseUpdate objects to the caller.
|
||||
|
||||
To get the intermediate steps of the agent's execution as fully formed messages,
|
||||
use the on_intermediate_message callback.
|
||||
|
||||
Note: A ChatResponseUpdate object contains a chunk of a message.
|
||||
|
||||
Args:
|
||||
messages: The message(s) to send to the agent.
|
||||
thread: The conversation thread associated with the message(s).
|
||||
kwargs: Additional keyword arguments.
|
||||
|
||||
Yields:
|
||||
An agent response item.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_new_thread(self) -> AgentThread:
|
||||
"""Creates a new conversation thread for the agent."""
|
||||
...
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import threading
|
||||
from asyncio import Future
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
|
||||
# from https://github.com/microsoft/autogen/blob/main/python/packages/autogen-core/src/autogen_core/_cancellation_token.py
|
||||
class CancellationToken:
|
||||
"""A token used to cancel pending async calls."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._cancelled: bool = False
|
||||
self._lock: threading.Lock = threading.Lock()
|
||||
self._callbacks: list[Callable[[], None]] = []
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""Cancel pending async calls linked to this cancellation token."""
|
||||
with self._lock:
|
||||
if not self._cancelled:
|
||||
self._cancelled = True
|
||||
for callback in self._callbacks:
|
||||
callback()
|
||||
|
||||
def is_cancelled(self) -> bool:
|
||||
"""Check if the CancellationToken has been used."""
|
||||
with self._lock:
|
||||
return self._cancelled
|
||||
|
||||
def add_callback(self, callback: Callable[[], None]) -> None:
|
||||
"""Attach a callback that will be called when cancel is invoked."""
|
||||
with self._lock:
|
||||
if self._cancelled:
|
||||
callback()
|
||||
else:
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def link_future(self, future: Future[Any]) -> Future[Any]:
|
||||
"""Link a pending async call to a token to allow its cancellation."""
|
||||
with self._lock:
|
||||
if self._cancelled:
|
||||
future.cancel()
|
||||
else:
|
||||
|
||||
def _cancel() -> None:
|
||||
future.cancel()
|
||||
|
||||
self._callbacks.append(_cancel)
|
||||
return future
|
||||
@@ -0,0 +1,530 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable, MutableSequence, Sequence
|
||||
from functools import wraps
|
||||
from typing import Annotated, Any, Generic, Literal, Protocol, TypeVar, runtime_checkable
|
||||
|
||||
from pydantic import BaseModel, PrivateAttr, StringConstraints
|
||||
|
||||
from ._logging import get_logger
|
||||
from ._pydantic import AFBaseModel
|
||||
from ._tools import AIFunction, AITool
|
||||
from ._types import (
|
||||
AIContents,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
ChatToolMode,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
GeneratedEmbeddings,
|
||||
)
|
||||
|
||||
TInput = TypeVar("TInput", contravariant=True)
|
||||
TEmbedding = TypeVar("TEmbedding")
|
||||
TInnerGetResponse = TypeVar("TInnerGetResponse", bound=Callable[..., Awaitable[ChatResponse]])
|
||||
TInnerGetStreamingResponse = TypeVar(
|
||||
"TInnerGetStreamingResponse", bound=Callable[..., AsyncIterable[ChatResponseUpdate]]
|
||||
)
|
||||
|
||||
TChatClientBase = TypeVar("TChatClientBase", bound="ChatClientBase")
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
# region: Tool Calling Functions and Decorators
|
||||
|
||||
|
||||
def _merge_function_results(
|
||||
messages: list[ChatMessage],
|
||||
) -> ChatMessage:
|
||||
"""Combine multiple function result content types to one chat message content type.
|
||||
|
||||
This method combines the FunctionResultContent items from separate ChatMessageContent messages,
|
||||
and is used in the event that the `context.terminate = True` condition is met.
|
||||
"""
|
||||
contents: list[Any] = []
|
||||
for message in messages:
|
||||
contents.extend([item for item in message.contents if isinstance(item, FunctionResultContent)])
|
||||
|
||||
return ChatMessage(
|
||||
role="tool",
|
||||
contents=contents,
|
||||
)
|
||||
|
||||
|
||||
async def _auto_invoke_function(
|
||||
function_call_content: FunctionCallContent,
|
||||
custom_args: dict[str, Any] | None = None,
|
||||
*,
|
||||
tool_map: dict[str, AIFunction[BaseModel, Any]],
|
||||
sequence_index: int | None = None,
|
||||
request_index: int | None = None,
|
||||
) -> AIContents:
|
||||
"""Invoke a function call requested by the agent, applying filters that are defined in the agent."""
|
||||
tool: AIFunction[BaseModel, Any] | None = tool_map.get(function_call_content.name)
|
||||
if tool is None:
|
||||
raise KeyError(f"No tool or function named '{function_call_content.name}'")
|
||||
|
||||
parsed_args: dict[str, Any] = dict(function_call_content.parse_arguments() or {})
|
||||
|
||||
# Merge with user-supplied args; right-hand side dominates, so parsed args win on conflicts.
|
||||
merged_args: dict[str, Any] = (custom_args or {}) | parsed_args
|
||||
args = tool.input_model.model_validate(merged_args)
|
||||
exception = None
|
||||
try:
|
||||
function_result = await tool.invoke(arguments=args)
|
||||
except Exception as ex:
|
||||
exception = ex
|
||||
function_result = None
|
||||
return FunctionResultContent(
|
||||
call_id=function_call_content.call_id,
|
||||
exception=exception,
|
||||
result=function_result,
|
||||
)
|
||||
|
||||
|
||||
def _tool_to_json_schema_spec(tool: AITool) -> dict[str, Any]:
|
||||
"""Convert a AITool to the JSON Schema function specification format."""
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": tool.parameters(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _prepare_tools_and_tool_choice(chat_options: ChatOptions) -> None:
|
||||
"""Prepare the tools and tool choice for the chat options."""
|
||||
chat_tool_mode: ChatToolMode | None = chat_options.tool_choice # type: ignore
|
||||
if chat_tool_mode is None or chat_tool_mode == ChatToolMode.NONE:
|
||||
chat_options.tools = None
|
||||
chat_options.tool_choice = ChatToolMode.NONE.mode
|
||||
return
|
||||
chat_options.tools = [
|
||||
(_tool_to_json_schema_spec(t) if isinstance(t, AITool) else t) for t in chat_options.tools or []
|
||||
]
|
||||
chat_options.tool_choice = chat_tool_mode.mode
|
||||
|
||||
|
||||
def _tool_call_non_streaming(func: TInnerGetResponse) -> TInnerGetResponse:
|
||||
"""Decorate the internal _inner_get_response method to enable tool calls.
|
||||
|
||||
Remarks:
|
||||
Relies on a class that has the _tool_map attribute for the executable tools to call.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(
|
||||
self: "ChatClientBase",
|
||||
*,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
response: ChatResponse | None = None
|
||||
fcc_messages: list[ChatMessage] = []
|
||||
for attempt_idx in range(self.maximum_iterations_per_request):
|
||||
response = await func(self, messages=messages, chat_options=chat_options)
|
||||
# if there are function calls, we will handle them first
|
||||
function_calls = [it for it in response.messages[0].contents if isinstance(it, FunctionCallContent)]
|
||||
if function_calls:
|
||||
# Run all function calls concurrently
|
||||
results = await asyncio.gather(*[
|
||||
_auto_invoke_function(
|
||||
function_call,
|
||||
custom_args=kwargs,
|
||||
tool_map=self._tool_map,
|
||||
sequence_index=seq_idx,
|
||||
request_index=attempt_idx,
|
||||
)
|
||||
for seq_idx, function_call in enumerate(function_calls)
|
||||
])
|
||||
# add a single ChatMessage to the response with the results
|
||||
response.messages.append(ChatMessage(role="tool", contents=results))
|
||||
# response should contain 2 messages after this,
|
||||
# one with function call contents
|
||||
# and one with function result contents
|
||||
# the amount and call_id's should match
|
||||
# this runs in every but the first run
|
||||
# we need to keep track of all function call messages
|
||||
fcc_messages.extend(response.messages)
|
||||
# and add them as additional context to the messages
|
||||
messages.extend(response.messages)
|
||||
continue
|
||||
# If we reach this point, it means there were no function calls to handle,
|
||||
# we'll add the previous function call and responses
|
||||
# to the front of the list, so that the final response is the last one
|
||||
# TODO (eavanvalkenburg): control this behavior?
|
||||
if fcc_messages:
|
||||
for msg in reversed(fcc_messages):
|
||||
response.messages.insert(0, msg)
|
||||
return response
|
||||
|
||||
# Failsafe: give up on tools, ask model for plain answer
|
||||
chat_options.tool_choice = "none"
|
||||
_prepare_tools_and_tool_choice(chat_options=chat_options)
|
||||
response = await func(self, messages=messages, chat_options=chat_options)
|
||||
if fcc_messages:
|
||||
for msg in reversed(fcc_messages):
|
||||
response.messages.insert(0, msg)
|
||||
return response
|
||||
|
||||
return wrapper # type: ignore[reportReturnType, return-value]
|
||||
|
||||
|
||||
def _tool_call_streaming(func: TInnerGetStreamingResponse) -> TInnerGetStreamingResponse:
|
||||
"""Decorate the internal _inner_get_response method to enable tool calls.
|
||||
|
||||
Remarks:
|
||||
Relies on a class that has the _tool_map attribute for the executable tools to call.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(
|
||||
self: "ChatClientBase",
|
||||
*,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
for attempt_idx in range(self.maximum_iterations_per_request):
|
||||
function_call_returned = False
|
||||
all_messages: list[ChatResponseUpdate] = []
|
||||
async for update in func(self, messages=messages, chat_options=chat_options):
|
||||
if update.contents and any(isinstance(item, FunctionCallContent) for item in update.contents):
|
||||
all_messages.append(update)
|
||||
function_call_returned = True
|
||||
yield update
|
||||
|
||||
if not function_call_returned:
|
||||
return
|
||||
|
||||
# There is one FunctionCallContent response stream in the messages, combining now to create
|
||||
# the full completion depending on the prompt, the message may contain both function call
|
||||
# content and others
|
||||
response: ChatResponse = ChatResponse.from_chat_response_updates(all_messages)
|
||||
function_calls = [item for item in response.messages[0].contents if isinstance(item, FunctionCallContent)]
|
||||
messages.append(response.messages[0])
|
||||
|
||||
if function_calls:
|
||||
# Run all function calls concurrently
|
||||
results = await asyncio.gather(*[
|
||||
_auto_invoke_function(
|
||||
function_call,
|
||||
custom_args=kwargs,
|
||||
tool_map=self._tool_map,
|
||||
sequence_index=seq_idx,
|
||||
request_index=attempt_idx,
|
||||
)
|
||||
for seq_idx, function_call in enumerate(function_calls)
|
||||
])
|
||||
yield ChatResponseUpdate(contents=results, role="tool")
|
||||
response.messages.append(ChatMessage(role="tool", contents=results))
|
||||
messages.extend(response.messages)
|
||||
continue
|
||||
|
||||
# Failsafe: give up on tools, ask model for plain answer
|
||||
chat_options.tool_choice = "none"
|
||||
_prepare_tools_and_tool_choice(chat_options=chat_options)
|
||||
async for update in func(self, messages=messages, chat_options=chat_options, **kwargs):
|
||||
yield update
|
||||
|
||||
return wrapper # type: ignore[reportReturnType, return-value]
|
||||
|
||||
|
||||
def use_tool_calling(cls: type[TChatClientBase]) -> type[TChatClientBase]:
|
||||
inner_response = getattr(cls, "_inner_get_response", None)
|
||||
if inner_response is not None:
|
||||
cls._inner_get_response = _tool_call_non_streaming(inner_response) # type: ignore
|
||||
inner_streaming_response = getattr(cls, "_inner_get_streaming_response", None)
|
||||
if inner_streaming_response is not None:
|
||||
cls._inner_get_streaming_response = _tool_call_streaming(inner_streaming_response) # type: ignore
|
||||
return cls
|
||||
|
||||
|
||||
# region: ChatClient Protocol
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ChatClient(Protocol):
|
||||
"""A protocol for a chat client that can generate responses."""
|
||||
|
||||
async def get_response(
|
||||
self,
|
||||
messages: str | ChatMessage | Sequence[ChatMessage],
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
"""Sends input and returns the response.
|
||||
|
||||
Args:
|
||||
messages: The sequence of input messages to send.
|
||||
**kwargs: Additional options for the request, such as ai_model_id, temperature, etc.
|
||||
See `ChatOptions` for more details.
|
||||
|
||||
Returns:
|
||||
The response messages generated by the client.
|
||||
|
||||
Raises:
|
||||
ValueError: If the input message sequence is `None`.
|
||||
"""
|
||||
...
|
||||
|
||||
async def get_streaming_response(
|
||||
self,
|
||||
messages: str | ChatMessage | Sequence[ChatMessage],
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
"""Sends input messages and streams the response.
|
||||
|
||||
Args:
|
||||
messages: The sequence of input messages to send.
|
||||
**kwargs: Additional options for the request, such as ai_model_id, temperature, etc.
|
||||
See `ChatOptions` for more details.
|
||||
|
||||
Yields:
|
||||
An async iterable of chat response updates containing the content of the response messages
|
||||
generated by the client.
|
||||
|
||||
Raises:
|
||||
ValueError: If the input message sequence is `None`.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class ChatClientBase(AFBaseModel, ABC):
|
||||
"""Base class for chat clients."""
|
||||
|
||||
ai_model_id: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
|
||||
maximum_iterations_per_request: int = 10
|
||||
_tool_map: dict[str, AIFunction[BaseModel, Any]] = PrivateAttr(default_factory=dict) # type: ignore
|
||||
|
||||
# region Internal methods to be implemented by the derived classes
|
||||
|
||||
@abstractmethod
|
||||
async def _inner_get_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
"""Send a chat request to the AI service.
|
||||
|
||||
Args:
|
||||
messages: The chat messages to send.
|
||||
chat_options: The options for the request.
|
||||
kwargs: Any additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
The chat response contents representing the response(s).
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def _inner_get_streaming_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
"""Send a streaming chat request to the AI service.
|
||||
|
||||
Args:
|
||||
messages: The chat messages to send.
|
||||
chat_options: The chat_options for the request.
|
||||
kwargs: Any additional keyword arguments.
|
||||
|
||||
Yields:
|
||||
ChatResponseUpdate: The streaming chat message contents.
|
||||
"""
|
||||
# Below is needed for mypy: https://mypy.readthedocs.io/en/stable/more_types.html#asynchronous-iterators
|
||||
if False:
|
||||
yield
|
||||
await asyncio.sleep(0) # pragma: no cover
|
||||
# This is a no-op, but it allows the method to be async and return an AsyncIterable.
|
||||
# The actual implementation should yield ChatResponseUpdate instances as needed.
|
||||
|
||||
# endregion
|
||||
|
||||
# region Public method
|
||||
|
||||
async def get_response(
|
||||
self,
|
||||
messages: str | ChatMessage | list[ChatMessage],
|
||||
*,
|
||||
model: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
temperature: float | None = None,
|
||||
top_p: float | None = None,
|
||||
tool_choice: ChatToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = None,
|
||||
tools: Sequence[AITool] | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
user: str | None = None,
|
||||
stop: str | Sequence[str] | None = None,
|
||||
frequency_penalty: float | None = None,
|
||||
logit_bias: dict[str | int, float] | None = None,
|
||||
presence_penalty: float | None = None,
|
||||
seed: int | None = None,
|
||||
store: bool | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
"""Get a response from a chat client.
|
||||
|
||||
Args:
|
||||
messages: the message or messages to send to the model
|
||||
model: the model to use for the request
|
||||
max_tokens: the maximum number of tokens to generate
|
||||
temperature: the sampling temperature to use
|
||||
top_p: the nucleus sampling probability to use
|
||||
tool_choice: the tool choice for the request
|
||||
tools: the tools to use for the request
|
||||
response_format: the format of the response
|
||||
user: the user to associate with the request
|
||||
stop: the stop sequence(s) for the request
|
||||
frequency_penalty: the frequency penalty to use
|
||||
logit_bias: the logit bias to use
|
||||
presence_penalty: the presence penalty to use
|
||||
seed: the random seed to use
|
||||
store: whether to store the response
|
||||
metadata: additional metadata to include in the request
|
||||
additional_properties: additional properties to include in the request
|
||||
kwargs: any additional keyword arguments,
|
||||
will only be passed to functions that are called.
|
||||
|
||||
Returns:
|
||||
A chat response from the model.
|
||||
"""
|
||||
if tools is not None:
|
||||
self._tool_map = {tool.name: tool for tool in tools if isinstance(tool, AIFunction)}
|
||||
chat_options = ChatOptions(
|
||||
ai_model_id=model,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
tool_choice=tool_choice,
|
||||
tools=tools,
|
||||
response_format=response_format,
|
||||
user=user,
|
||||
stop=stop,
|
||||
frequency_penalty=frequency_penalty,
|
||||
logit_bias=logit_bias,
|
||||
presence_penalty=presence_penalty,
|
||||
seed=seed,
|
||||
store=store,
|
||||
metadata=metadata,
|
||||
additional_properties=additional_properties or {},
|
||||
)
|
||||
if isinstance(messages, str):
|
||||
messages = [ChatMessage(role="user", text=messages)]
|
||||
if isinstance(messages, ChatMessage):
|
||||
messages = [messages]
|
||||
_prepare_tools_and_tool_choice(chat_options=chat_options)
|
||||
return await self._inner_get_response(messages=messages, chat_options=chat_options, **kwargs)
|
||||
|
||||
async def get_streaming_response(
|
||||
self,
|
||||
messages: str | ChatMessage | list[ChatMessage],
|
||||
*,
|
||||
model: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
temperature: float | None = None,
|
||||
top_p: float | None = None,
|
||||
tool_choice: ChatToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = None,
|
||||
tools: Sequence[AITool] | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
user: str | None = None,
|
||||
stop: str | Sequence[str] | None = None,
|
||||
frequency_penalty: float | None = None,
|
||||
logit_bias: dict[str | int, float] | None = None,
|
||||
presence_penalty: float | None = None,
|
||||
seed: int | None = None,
|
||||
store: bool | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
"""Get a streaming response from a chat client.
|
||||
|
||||
Args:
|
||||
messages: the message or messages to send to the model
|
||||
model: the model to use for the request
|
||||
max_tokens: the maximum number of tokens to generate
|
||||
temperature: the sampling temperature to use
|
||||
top_p: the nucleus sampling probability to use
|
||||
tool_choice: the tool choice for the request
|
||||
tools: the tools to use for the request
|
||||
response_format: the format of the response
|
||||
user: the user to associate with the request
|
||||
stop: the stop sequence(s) for the request
|
||||
frequency_penalty: the frequency penalty to use
|
||||
logit_bias: the logit bias to use
|
||||
presence_penalty: the presence penalty to use
|
||||
seed: the random seed to use
|
||||
store: whether to store the response
|
||||
metadata: additional metadata to include in the request
|
||||
additional_properties: additional properties to include in the request
|
||||
kwargs: any additional keyword arguments
|
||||
|
||||
Yields:
|
||||
A stream representing the response(s) from the LLM.
|
||||
"""
|
||||
if tools is not None:
|
||||
self._tool_map = {tool.name: tool for tool in tools if isinstance(tool, AIFunction)}
|
||||
chat_options = ChatOptions(
|
||||
ai_model_id=model,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
tool_choice=tool_choice,
|
||||
tools=tools,
|
||||
response_format=response_format,
|
||||
user=user,
|
||||
stop=stop,
|
||||
frequency_penalty=frequency_penalty,
|
||||
logit_bias=logit_bias,
|
||||
presence_penalty=presence_penalty,
|
||||
seed=seed,
|
||||
store=store,
|
||||
metadata=metadata,
|
||||
additional_properties=additional_properties or {},
|
||||
**kwargs,
|
||||
)
|
||||
if isinstance(messages, str):
|
||||
messages = [ChatMessage(role="user", text=messages)]
|
||||
if isinstance(messages, ChatMessage):
|
||||
messages = [messages]
|
||||
_prepare_tools_and_tool_choice(chat_options=chat_options)
|
||||
async for update in self._inner_get_streaming_response(messages=messages, chat_options=chat_options, **kwargs):
|
||||
yield update
|
||||
|
||||
|
||||
# region: Embedding Client
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class EmbeddingGenerator(Protocol, Generic[TInput, TEmbedding]):
|
||||
"""A protocol for an embedding generator that can create embeddings from input data."""
|
||||
|
||||
async def generate(
|
||||
self,
|
||||
input_data: Sequence[TInput],
|
||||
**kwargs: Any,
|
||||
) -> GeneratedEmbeddings[TEmbedding]:
|
||||
"""Generates an embedding for the given input data.
|
||||
|
||||
Args:
|
||||
input_data: The input data to generate an embedding for.
|
||||
**kwargs: Additional options for the request.
|
||||
|
||||
Returns:
|
||||
The generated embedding, this acts like a list, but has additional metadata and usage details.
|
||||
|
||||
"""
|
||||
...
|
||||
@@ -0,0 +1,24 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
|
||||
from .exceptions import AgentFrameworkException
|
||||
|
||||
logging.basicConfig(
|
||||
format="[%(asctime)s - %(pathname)s:%(lineno)d - %(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
|
||||
def get_logger(name: str = "agent_framework") -> logging.Logger:
|
||||
"""Get a logger with the specified name, defaulting to 'agent_framework'.
|
||||
|
||||
Args:
|
||||
name (str): The name of the logger. Defaults to 'agent_framework'.
|
||||
|
||||
Returns:
|
||||
logging.Logger: The configured logger instance.
|
||||
"""
|
||||
if not name.startswith("agent_framework"):
|
||||
raise AgentFrameworkException("Logger name must start with 'agent_framework'.")
|
||||
return logging.getLogger(name)
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class AFBaseModel(BaseModel):
|
||||
"""Base class for all pydantic models in the Agent Framework."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True, validate_assignment=True)
|
||||
@@ -0,0 +1,139 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from typing import Any, Generic, Protocol, TypeVar, runtime_checkable
|
||||
|
||||
from pydantic import BaseModel, create_model
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AITool(Protocol):
|
||||
"""Represents a tool that can be specified to an AI service."""
|
||||
|
||||
name: str
|
||||
"""The name of the tool."""
|
||||
description: str | None = None
|
||||
"""A description of the tool, suitable for use in describing the purpose to a model."""
|
||||
additional_properties: dict[str, Any] | None = None
|
||||
"""Additional properties associated with the tool."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the tool."""
|
||||
...
|
||||
|
||||
def parameters(self) -> Mapping[str, Any]:
|
||||
"""Return the parameters of the tool as a JSON schema."""
|
||||
...
|
||||
|
||||
|
||||
ArgsT = TypeVar("ArgsT", bound=BaseModel)
|
||||
ReturnT = TypeVar("ReturnT")
|
||||
|
||||
|
||||
class AIFunction(AITool, Generic[ArgsT, ReturnT]):
|
||||
"""A tool that represents a function that can be called by an AI service."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[..., Awaitable[ReturnT] | ReturnT],
|
||||
name: str,
|
||||
description: str,
|
||||
input_model: type[ArgsT],
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize a FunctionTool.
|
||||
|
||||
Args:
|
||||
func: The function to wrap.
|
||||
name: The name of the tool.
|
||||
description: A description of the tool.
|
||||
input_model: A Pydantic model that defines the input parameters for the function.
|
||||
**kwargs: Additional properties to set on the tool.
|
||||
stored in additional_properties.
|
||||
"""
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.input_model = input_model
|
||||
self.additional_properties: dict[str, Any] | None = kwargs
|
||||
self._func = func
|
||||
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
"""Return the parameter json schemas of the input model."""
|
||||
return self.input_model.model_json_schema()
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> ReturnT | Awaitable[ReturnT]:
|
||||
"""Call the wrapped function with the provided arguments."""
|
||||
return self._func(*args, **kwargs)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"AIFunction(name={self.name}, description={self.description})"
|
||||
|
||||
async def invoke(
|
||||
self,
|
||||
*,
|
||||
arguments: ArgsT | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ReturnT:
|
||||
"""Run the AI function with the provided arguments as a Pydantic model.
|
||||
|
||||
Args:
|
||||
arguments: A Pydantic model instance containing the arguments for the function.
|
||||
kwargs: keyword arguments to pass to the function, will not be used if `args` is provided.
|
||||
"""
|
||||
if arguments is not None:
|
||||
if not isinstance(arguments, self.input_model):
|
||||
raise TypeError(f"Expected {self.input_model.__name__}, got {type(arguments).__name__}")
|
||||
kwargs = arguments.model_dump(exclude_none=True)
|
||||
res = self.__call__(**kwargs)
|
||||
if inspect.isawaitable(res):
|
||||
return await res
|
||||
return res
|
||||
|
||||
|
||||
def ai_function(
|
||||
func: Callable[..., ReturnT | Awaitable[ReturnT]] | None = None,
|
||||
*,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
) -> AIFunction[Any, ReturnT] | Callable[[Callable[..., ReturnT | Awaitable[ReturnT]]], AIFunction[Any, ReturnT]]:
|
||||
"""Decorate a function to turn it into a AIFunction that can be passed to models.
|
||||
|
||||
Args:
|
||||
func: The function to wrap. If None, returns a decorator.
|
||||
name: The name of the tool. Defaults to the function's name.
|
||||
description: A description of the tool. Defaults to the function's docstring.
|
||||
additional_properties: Additional properties to set on the tool.
|
||||
|
||||
"""
|
||||
|
||||
def wrapper(f: Callable[..., ReturnT | Awaitable[ReturnT]]) -> AIFunction[Any, ReturnT]:
|
||||
tool_name: str = name or getattr(f, "__name__", "unknown_function") # type: ignore[assignment]
|
||||
tool_desc: str = description or (f.__doc__ or "")
|
||||
sig = inspect.signature(f)
|
||||
fields = {
|
||||
pname: (
|
||||
param.annotation if param.annotation is not inspect.Parameter.empty else str,
|
||||
param.default if param.default is not inspect.Parameter.empty else ...,
|
||||
)
|
||||
for pname, param in sig.parameters.items()
|
||||
if pname not in {"self", "cls"}
|
||||
}
|
||||
input_model: Any = create_model(f"{tool_name}_input", **fields) # type: ignore[call-overload]
|
||||
if not issubclass(input_model, BaseModel):
|
||||
raise TypeError(f"Input model for {tool_name} must be a subclass of BaseModel, got {input_model}")
|
||||
|
||||
return functools.update_wrapper( # type: ignore[return-value]
|
||||
AIFunction[Any, ReturnT](
|
||||
func=f,
|
||||
name=tool_name,
|
||||
description=tool_desc,
|
||||
input_model=input_model,
|
||||
**(additional_properties if additional_properties is not None else {}),
|
||||
),
|
||||
f,
|
||||
)
|
||||
|
||||
return wrapper(func) if func else wrapper
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
class AgentFrameworkException(Exception):
|
||||
"""Base class for exceptions in the Agent Framework."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AgentException(AgentFrameworkException):
|
||||
"""Base class for all agent exceptions."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AgentExecutionException(AgentException):
|
||||
"""An error occurred while executing the agent."""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
from typing import Generic, Protocol, TypeVar, runtime_checkable
|
||||
|
||||
TInput = TypeVar("TInput")
|
||||
TResponse = TypeVar("TResponse")
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class InputGuardrail(Protocol, Generic[TInput]):
|
||||
"""A protocol for input guardrails that can validate and transform input messages."""
|
||||
|
||||
def __call__(self, message: TInput) -> TInput:
|
||||
"""Validate and possibly transform the input message."""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class OutputGuardrail(Protocol, Generic[TResponse]):
|
||||
"""A protocol for output guardrails that can validate and transform output messages."""
|
||||
|
||||
def __call__(self, message: TResponse) -> TResponse:
|
||||
"""Validate and possibly transform the output message."""
|
||||
...
|
||||
@@ -0,0 +1,122 @@
|
||||
[project]
|
||||
name = "agent-framework"
|
||||
description = "Microsoft Agent Framework for building AI Agents with Python."
|
||||
authors = [{ name = "Microsoft", email = "SK-Support@microsoft.com"}]
|
||||
readme = "../../README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "0.1.0b1"
|
||||
license = {file = "../../LICENSE"}
|
||||
urls.homepage = "https://learn.microsoft.com/en-us/semantic-kernel/overview/"
|
||||
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 :: 5 - Production/Stable",
|
||||
"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",
|
||||
"Framework :: Pydantic :: 2",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-azure",
|
||||
"agent-framework-openai",
|
||||
"pydantic>=2.11.7",
|
||||
"typing-extensions>=4.14.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
prerelease = "if-necessary-or-explicit"
|
||||
dev-dependencies = [
|
||||
"pre-commit >= 3.7",
|
||||
"ruff>=0.11.8",
|
||||
"pytest>=8.4.1",
|
||||
"pytest-asyncio>=1.0.0",
|
||||
"pytest-cov>=6.2.1",
|
||||
"pytest-xdist[psutil]>=3.8.0",
|
||||
"pytest-timeout>=2.3.1",
|
||||
"mypy>=1.16.1",
|
||||
"pyright>=1.1.402",
|
||||
|
||||
#tasks
|
||||
"poethepoet>=0.36.0",
|
||||
"rich",
|
||||
"tomli",
|
||||
"tomli-w",
|
||||
"markdownify",
|
||||
# Documentation
|
||||
"myst-nb==1.1.2",
|
||||
"pydata-sphinx-theme==0.16.0",
|
||||
"sphinx-copybutton",
|
||||
"sphinx-design",
|
||||
"sphinx",
|
||||
"sphinxcontrib-apidoc",
|
||||
"autodoc_pydantic~=2.2",
|
||||
"pygments",
|
||||
"sphinxext-rediraffe",
|
||||
"opentelemetry-instrumentation-openai",
|
||||
"markdown-it-py[linkify]",
|
||||
# Documentation tooling
|
||||
"diskcache",
|
||||
"redis",
|
||||
"sphinx-autobuild",
|
||||
]
|
||||
environments = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform == 'linux'",
|
||||
"sys_platform == 'win32'"
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = 'tests'
|
||||
addopts = "-ra -q -r fEX"
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
filterwarnings = []
|
||||
timeout = 120
|
||||
|
||||
[tool.uv-dynamic-versioning]
|
||||
fallback-version = "0.0.0"
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
|
||||
[tool.pyright]
|
||||
extend = "../../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
|
||||
disallow_any_unimported = true
|
||||
|
||||
[tool.bandit]
|
||||
targets = ["agent_framework"]
|
||||
exclude_dirs = ["tests"]
|
||||
|
||||
[tool.poe]
|
||||
executor.type = "uv"
|
||||
include = "../../shared_tasks.toml"
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-name = "agent_framework"
|
||||
module-root = ""
|
||||
namespace = true
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7.19,<0.8.0"]
|
||||
build-backend = "uv_build"
|
||||
@@ -0,0 +1,115 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from collections.abc import AsyncIterable, Sequence
|
||||
from typing import Any, TypeVar
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pytest import fixture
|
||||
|
||||
from agent_framework import Agent, AgentThread, ChatMessage, ChatResponse, ChatResponseUpdate, ChatRole, TextContent
|
||||
|
||||
TThreadType = TypeVar("TThreadType", bound=AgentThread)
|
||||
|
||||
|
||||
# Mock AgentThread implementation for testing
|
||||
class MockAgentThread(AgentThread):
|
||||
async def _create(self) -> str:
|
||||
return str(uuid4())
|
||||
|
||||
async def _delete(self) -> None:
|
||||
pass
|
||||
|
||||
async def _on_new_message(self, new_messages: ChatMessage | Sequence[ChatMessage]) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# Mock Agent implementation for testing
|
||||
class MockAgent(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid4()))
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
instructions: str | None = None
|
||||
|
||||
async def run(
|
||||
self,
|
||||
messages: ChatMessage | str | list[ChatMessage] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
return ChatResponse(messages=[ChatMessage(role=ChatRole.ASSISTANT, contents=[TextContent("Response")])])
|
||||
|
||||
async def run_stream(
|
||||
self,
|
||||
messages: str | ChatMessage | list[ChatMessage] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent("Response")])
|
||||
|
||||
def get_new_thread(self) -> AgentThread:
|
||||
return MockAgentThread()
|
||||
|
||||
|
||||
@fixture
|
||||
def agent_thread() -> AgentThread:
|
||||
return MockAgentThread()
|
||||
|
||||
|
||||
@fixture
|
||||
def agent() -> Agent:
|
||||
return MockAgent()
|
||||
|
||||
|
||||
def test_agent_thread_type(agent_thread: AgentThread) -> None:
|
||||
assert isinstance(agent_thread, AgentThread)
|
||||
|
||||
|
||||
async def test_agent_thread_id_property(agent_thread: AgentThread) -> None:
|
||||
assert agent_thread.id is None
|
||||
await agent_thread.create()
|
||||
assert isinstance(agent_thread.id, str)
|
||||
|
||||
|
||||
async def test_agent_thread_create(agent_thread: AgentThread) -> None:
|
||||
thread_id = await agent_thread.create()
|
||||
assert thread_id == agent_thread.id
|
||||
assert isinstance(thread_id, str)
|
||||
|
||||
|
||||
async def test_agent_thread_create_already_exists(agent_thread: AgentThread) -> None:
|
||||
thread_id = await agent_thread.create()
|
||||
same_id = await agent_thread.create()
|
||||
assert thread_id == same_id
|
||||
|
||||
|
||||
async def test_agent_thread_delete_already_deleted(agent_thread: AgentThread) -> None:
|
||||
await agent_thread.delete()
|
||||
await agent_thread.delete() # Should not raise error
|
||||
|
||||
|
||||
async def test_agent_thread_on_new_message_creates_thread(agent_thread: AgentThread) -> None:
|
||||
message = ChatMessage(role=ChatRole.USER, contents=[TextContent("Hello")])
|
||||
await agent_thread.on_new_message(message)
|
||||
assert agent_thread.id is not None
|
||||
|
||||
|
||||
def test_agent_type(agent: Agent) -> None:
|
||||
assert isinstance(agent, Agent)
|
||||
|
||||
|
||||
async def test_agent_run(agent: Agent) -> None:
|
||||
response = await agent.run("test")
|
||||
assert response.messages[0].role == ChatRole.ASSISTANT
|
||||
assert response.messages[0].text == "Response"
|
||||
|
||||
|
||||
async def test_agent_run_stream(agent: Agent) -> None:
|
||||
async def collect_updates(updates: AsyncIterable[ChatResponseUpdate]) -> list[ChatResponseUpdate]:
|
||||
return [u async for u in updates]
|
||||
|
||||
updates = await collect_updates(agent.run_stream(messages="test"))
|
||||
assert len(updates) == 1
|
||||
assert updates[0].text == "Response"
|
||||
@@ -0,0 +1,307 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, MutableSequence, Sequence
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
from pytest import fixture
|
||||
|
||||
from agent_framework import (
|
||||
ChatClient,
|
||||
ChatClientBase,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
ChatRole,
|
||||
EmbeddingGenerator,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
GeneratedEmbeddings,
|
||||
TextContent,
|
||||
ai_function,
|
||||
use_tool_calling,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore
|
||||
else:
|
||||
from typing_extensions import override # type: ignore[import]
|
||||
|
||||
|
||||
class MockChatClient:
|
||||
"""Simple implementation of a chat client."""
|
||||
|
||||
async def get_response(
|
||||
self,
|
||||
messages: ChatMessage | Sequence[ChatMessage],
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
# Implement the method
|
||||
|
||||
return ChatResponse(messages=ChatMessage(role="assistant", text="test response"))
|
||||
|
||||
async def get_streaming_response(
|
||||
self,
|
||||
messages: ChatMessage | Sequence[ChatMessage],
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
# Implement the method
|
||||
yield ChatResponseUpdate(text=TextContent(text="test streaming response"), role="assistant")
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="another update")], role="assistant")
|
||||
|
||||
|
||||
@use_tool_calling
|
||||
class MockChatClientBase(ChatClientBase):
|
||||
"""Mock implementation of the ChatClientBase."""
|
||||
|
||||
run_responses: list[ChatResponse] = Field(default_factory=list)
|
||||
streaming_responses: list[list[ChatResponseUpdate]] = Field(default_factory=list)
|
||||
|
||||
@override
|
||||
async def _inner_get_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
"""Send a chat request to the AI service.
|
||||
|
||||
Args:
|
||||
messages: The chat messages to send.
|
||||
chat_options: The options for the request.
|
||||
kwargs: Any additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
The chat response contents representing the response(s).
|
||||
"""
|
||||
if not self.run_responses or chat_options.tool_choice == "none":
|
||||
return ChatResponse(messages=ChatMessage(role="assistant", text=f"test response - {messages[0].text}"))
|
||||
return self.run_responses.pop(0)
|
||||
|
||||
@override
|
||||
async def _inner_get_streaming_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
if not self.streaming_responses or chat_options.tool_choice == "none":
|
||||
yield ChatResponseUpdate(text=f"update - {messages[0].text}", role="assistant")
|
||||
return
|
||||
response = self.streaming_responses.pop(0)
|
||||
for update in response:
|
||||
yield update
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
class MockEmbeddingGenerator:
|
||||
"""Simple implementation of an embedding generator."""
|
||||
|
||||
async def generate(
|
||||
self,
|
||||
input_data: Sequence[str],
|
||||
**kwargs: Any,
|
||||
) -> GeneratedEmbeddings[list[float]]:
|
||||
# Implement the method
|
||||
embeddings = GeneratedEmbeddings[list[float]]()
|
||||
for i, _ in enumerate(input_data):
|
||||
embeddings.append([0.0 * 1, 0.1 * 1, 0.2 * 1, 0.3 * i, 0.4 * i])
|
||||
return embeddings
|
||||
|
||||
|
||||
@fixture
|
||||
def chat_client() -> MockChatClient:
|
||||
return MockChatClient()
|
||||
|
||||
|
||||
@fixture
|
||||
def chat_client_base() -> MockChatClientBase:
|
||||
return MockChatClientBase(ai_model_id="test")
|
||||
|
||||
|
||||
@fixture
|
||||
def embedding_generator() -> MockEmbeddingGenerator:
|
||||
gen: EmbeddingGenerator[str, list[float]] = MockEmbeddingGenerator()
|
||||
return gen
|
||||
|
||||
|
||||
def test_chat_client_type(chat_client: MockChatClient):
|
||||
assert isinstance(chat_client, ChatClient)
|
||||
|
||||
|
||||
async def test_chat_client_get_response(chat_client: MockChatClient):
|
||||
response = await chat_client.get_response(ChatMessage(role="user", text="Hello"))
|
||||
assert response.text == "test response"
|
||||
assert response.messages[0].role == ChatRole.ASSISTANT
|
||||
|
||||
|
||||
async def test_chat_client_get_streaming_response(chat_client: MockChatClient):
|
||||
async for update in chat_client.get_streaming_response(ChatMessage(role="user", text="Hello")):
|
||||
assert update.text == "test streaming response" or update.text == "another update"
|
||||
assert update.role == ChatRole.ASSISTANT
|
||||
|
||||
|
||||
def test_embedding_generator_type(embedding_generator: MockEmbeddingGenerator):
|
||||
assert isinstance(embedding_generator, EmbeddingGenerator)
|
||||
|
||||
|
||||
async def test_embedding_generator_generate(embedding_generator: MockEmbeddingGenerator):
|
||||
input_data = ["Hello", "world"]
|
||||
embeddings = await embedding_generator.generate(input_data)
|
||||
assert len(embeddings) == len(input_data)
|
||||
for emb in embeddings:
|
||||
assert len(emb) == 5
|
||||
|
||||
|
||||
def test_base_client(chat_client_base: MockChatClientBase):
|
||||
assert isinstance(chat_client_base, ChatClientBase)
|
||||
assert isinstance(chat_client_base, ChatClient)
|
||||
|
||||
|
||||
async def test_base_client_get_response(chat_client_base: MockChatClientBase):
|
||||
response = await chat_client_base.get_response(ChatMessage(role="user", text="Hello"))
|
||||
assert response.messages[0].role == ChatRole.ASSISTANT
|
||||
assert response.messages[0].text == "test response - Hello"
|
||||
|
||||
|
||||
async def test_base_client_get_streaming_response(chat_client_base: MockChatClientBase):
|
||||
async for update in chat_client_base.get_streaming_response(ChatMessage(role="user", text="Hello")):
|
||||
assert update.text == "update - Hello" or update.text == "another update"
|
||||
|
||||
|
||||
async def test_base_client_with_function_calling(chat_client_base: MockChatClientBase):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1": "value1"}')],
|
||||
)
|
||||
),
|
||||
ChatResponse(messages=ChatMessage(role="assistant", text="done")),
|
||||
]
|
||||
response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[ai_func])
|
||||
assert exec_counter == 1
|
||||
assert len(response.messages) == 3
|
||||
assert response.messages[0].role == ChatRole.ASSISTANT
|
||||
assert isinstance(response.messages[0].contents[0], FunctionCallContent)
|
||||
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 == ChatRole.TOOL
|
||||
assert isinstance(response.messages[1].contents[0], FunctionResultContent)
|
||||
assert response.messages[1].contents[0].call_id == "1"
|
||||
assert response.messages[1].contents[0].result == "Processed value1"
|
||||
assert response.messages[2].role == ChatRole.ASSISTANT
|
||||
assert response.messages[2].text == "done"
|
||||
|
||||
|
||||
async def test_base_client_with_function_calling_disabled(chat_client_base: MockChatClientBase):
|
||||
chat_client_base.maximum_iterations_per_request = 0
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1": "value1"}')],
|
||||
)
|
||||
),
|
||||
ChatResponse(messages=ChatMessage(role="assistant", text="done")),
|
||||
]
|
||||
response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[ai_func])
|
||||
assert exec_counter == 0
|
||||
assert len(response.messages) == 1
|
||||
assert response.messages[0].role == ChatRole.ASSISTANT
|
||||
assert response.messages[0].text == "test response - hello"
|
||||
|
||||
|
||||
async def test_base_client_with_streaming_function_calling(chat_client_base: MockChatClientBase):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
chat_client_base.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1":')],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='"value1"}')],
|
||||
role="assistant",
|
||||
),
|
||||
],
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[TextContent(text="Processed value1")],
|
||||
role="assistant",
|
||||
)
|
||||
],
|
||||
]
|
||||
updates = []
|
||||
async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[ai_func]):
|
||||
updates.append(update)
|
||||
assert len(updates) == 4 # two updates with the function call, the function result and the final text
|
||||
assert updates[0].contents[0].call_id == "1"
|
||||
assert updates[1].contents[0].call_id == "1"
|
||||
assert updates[2].contents[0].call_id == "1"
|
||||
assert updates[3].text == "Processed value1"
|
||||
assert exec_counter == 1
|
||||
|
||||
|
||||
async def test_base_client_with_streaming_function_calling_disabled(chat_client_base: MockChatClientBase):
|
||||
chat_client_base.maximum_iterations_per_request = 0
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
chat_client_base.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1":')],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='"value1"}')],
|
||||
role="assistant",
|
||||
),
|
||||
],
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[TextContent(text="Processed value1")],
|
||||
role="assistant",
|
||||
)
|
||||
],
|
||||
]
|
||||
updates = []
|
||||
async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[ai_func]):
|
||||
updates.append(update)
|
||||
assert len(updates) == 1
|
||||
assert exec_counter == 0
|
||||
@@ -0,0 +1,21 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from pytest import mark
|
||||
|
||||
|
||||
@mark.xfail(reason="Not solved")
|
||||
def test_openai():
|
||||
try:
|
||||
from agent_framework.openai import __version__
|
||||
except ImportError:
|
||||
__version__ = None
|
||||
assert __version__ is not None
|
||||
|
||||
|
||||
@mark.xfail(reason="Not solved")
|
||||
def test_azure():
|
||||
try:
|
||||
from agent_framework.azure import __version__
|
||||
except ImportError:
|
||||
__version__ = None
|
||||
assert __version__ is not None
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_framework import get_logger
|
||||
from agent_framework.exceptions import AgentFrameworkException
|
||||
|
||||
|
||||
def test_get_logger():
|
||||
"""Test that the logger is created with the correct name."""
|
||||
logger = get_logger()
|
||||
assert logger.name == "agent_framework"
|
||||
|
||||
|
||||
def test_get_logger_custom_name():
|
||||
"""Test that the logger can be created with a custom name."""
|
||||
custom_name = "agent_framework.custom"
|
||||
logger = get_logger(custom_name)
|
||||
assert logger.name == custom_name
|
||||
|
||||
|
||||
def test_get_logger_invalid_name():
|
||||
"""Test that an exception is raised for an invalid logger name."""
|
||||
with pytest.raises(AgentFrameworkException, match="Logger name must start with 'agent_framework'."):
|
||||
get_logger("invalid_name")
|
||||
|
||||
|
||||
def test_log(caplog):
|
||||
"""Test that the logger can log messages and adheres to the expected format."""
|
||||
logger = get_logger()
|
||||
with caplog.at_level("DEBUG"):
|
||||
logger.debug("This is a debug message")
|
||||
assert len(caplog.records) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == "DEBUG"
|
||||
assert record.message == "This is a debug message"
|
||||
assert record.name == "agent_framework"
|
||||
assert record.pathname.endswith("test_logging.py")
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from agent_framework import AITool, ai_function
|
||||
|
||||
|
||||
def test_ai_function_decorator():
|
||||
"""Test the ai_function decorator."""
|
||||
|
||||
@ai_function(name="test_tool", description="A test tool")
|
||||
def test_tool(x: int, y: int) -> int:
|
||||
"""A simple function that adds two numbers."""
|
||||
return x + y
|
||||
|
||||
assert isinstance(test_tool, AITool)
|
||||
assert test_tool.name == "test_tool"
|
||||
assert test_tool.description == "A test tool"
|
||||
assert test_tool.parameters() == {
|
||||
"properties": {"x": {"title": "X", "type": "integer"}, "y": {"title": "Y", "type": "integer"}},
|
||||
"required": ["x", "y"],
|
||||
"title": "test_tool_input",
|
||||
"type": "object",
|
||||
}
|
||||
assert test_tool(1, 2) == 3
|
||||
|
||||
|
||||
async def test_ai_function_decorator_with_async():
|
||||
"""Test the ai_function decorator with an async function."""
|
||||
|
||||
@ai_function(name="async_test_tool", description="An async test tool")
|
||||
async def async_test_tool(x: int, y: int) -> int:
|
||||
"""An async function that adds two numbers."""
|
||||
return x + y
|
||||
|
||||
assert isinstance(async_test_tool, AITool)
|
||||
assert async_test_tool.name == "async_test_tool"
|
||||
assert async_test_tool.description == "An async test tool"
|
||||
assert async_test_tool.parameters() == {
|
||||
"properties": {"x": {"title": "X", "type": "integer"}, "y": {"title": "Y", "type": "integer"}},
|
||||
"required": ["x", "y"],
|
||||
"title": "async_test_tool_input",
|
||||
"type": "object",
|
||||
}
|
||||
assert (await async_test_tool(1, 2)) == 3
|
||||
@@ -0,0 +1,482 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from collections.abc import MutableSequence
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pytest import mark, raises
|
||||
|
||||
from agent_framework import (
|
||||
AIContent,
|
||||
AIContents,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
ChatRole,
|
||||
ChatToolMode,
|
||||
DataContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
GeneratedEmbeddings,
|
||||
StructuredResponse,
|
||||
TextContent,
|
||||
TextReasoningContent,
|
||||
UriContent,
|
||||
UsageDetails,
|
||||
)
|
||||
|
||||
# region: TextContent
|
||||
|
||||
|
||||
def test_text_content_positional():
|
||||
"""Test the TextContent class to ensure it initializes correctly and inherits from AIContent."""
|
||||
# Create an instance of TextContent
|
||||
content = TextContent("Hello, world!", raw_representation="Hello, world!", additional_properties={"version": 1})
|
||||
|
||||
# Check the type and content
|
||||
assert content.type == "text"
|
||||
assert content.text == "Hello, world!"
|
||||
assert content.raw_representation == "Hello, world!"
|
||||
assert content.additional_properties["version"] == 1
|
||||
# Ensure the instance is of type AIContent
|
||||
assert isinstance(content, AIContent)
|
||||
with raises(ValidationError):
|
||||
content.type = "ai"
|
||||
|
||||
|
||||
def test_text_content_keyword():
|
||||
"""Test the TextContent class to ensure it initializes correctly and inherits from AIContent."""
|
||||
# Create an instance of TextContent
|
||||
content = TextContent(
|
||||
text="Hello, world!", raw_representation="Hello, world!", additional_properties={"version": 1}
|
||||
)
|
||||
|
||||
# Check the type and content
|
||||
assert content.type == "text"
|
||||
assert content.text == "Hello, world!"
|
||||
assert content.raw_representation == "Hello, world!"
|
||||
assert content.additional_properties["version"] == 1
|
||||
# Ensure the instance is of type AIContent
|
||||
assert isinstance(content, AIContent)
|
||||
with raises(ValidationError):
|
||||
content.type = "ai"
|
||||
|
||||
|
||||
# region: DataContent
|
||||
|
||||
|
||||
def test_data_content_bytes():
|
||||
"""Test the DataContent class to ensure it initializes correctly."""
|
||||
# Create an instance of DataContent
|
||||
content = DataContent(data=b"test", media_type="application/octet-stream", additional_properties={"version": 1})
|
||||
|
||||
# Check the type and content
|
||||
assert content.type == "data"
|
||||
assert content.uri == "data:application/octet-stream;base64,dGVzdA=="
|
||||
assert content.additional_properties["version"] == 1
|
||||
|
||||
# Ensure the instance is of type AIContent
|
||||
assert isinstance(content, AIContent)
|
||||
|
||||
|
||||
def test_data_content_uri():
|
||||
"""Test the DataContent class to ensure it initializes correctly with a URI."""
|
||||
# Create an instance of DataContent with a URI
|
||||
content = DataContent(uri="data:application/octet-stream;base64,dGVzdA==", additional_properties={"version": 1})
|
||||
|
||||
# Check the type and content
|
||||
assert content.type == "data"
|
||||
assert content.uri == "data:application/octet-stream;base64,dGVzdA=="
|
||||
assert content.additional_properties["version"] == 1
|
||||
|
||||
# Ensure the instance is of type AIContent
|
||||
assert isinstance(content, AIContent)
|
||||
|
||||
|
||||
def test_data_content_invalid():
|
||||
"""Test the DataContent class to ensure it raises an error for invalid initialization."""
|
||||
# Attempt to create an instance of DataContent with invalid data
|
||||
# not a proper uri
|
||||
with raises(ValidationError):
|
||||
DataContent(uri="invalid_uri")
|
||||
# unknown media type
|
||||
with raises(ValidationError):
|
||||
DataContent(uri="data:application/random;base64,dGVzdA==")
|
||||
# not valid base64 data
|
||||
|
||||
with raises(ValidationError):
|
||||
DataContent(uri="data:application/json;base64,dGVzdA&")
|
||||
|
||||
|
||||
def test_data_content_empty():
|
||||
"""Test the DataContent class to ensure it raises an error for empty data."""
|
||||
# Attempt to create an instance of DataContent with empty data
|
||||
with raises(ValidationError):
|
||||
DataContent(data=b"", media_type="application/octet-stream")
|
||||
|
||||
# Attempt to create an instance of DataContent with empty URI
|
||||
with raises(ValidationError):
|
||||
DataContent(uri="")
|
||||
|
||||
|
||||
# region: UriContent
|
||||
|
||||
|
||||
def test_uri_content():
|
||||
"""Test the UriContent class to ensure it initializes correctly."""
|
||||
content = UriContent(uri="http://example.com", media_type="image/jpg", additional_properties={"version": 1})
|
||||
|
||||
# Check the type and content
|
||||
assert content.type == "uri"
|
||||
assert content.uri == "http://example.com"
|
||||
assert content.media_type == "image/jpg"
|
||||
assert content.additional_properties["version"] == 1
|
||||
|
||||
# Ensure the instance is of type AIContent
|
||||
assert isinstance(content, AIContent)
|
||||
|
||||
|
||||
# region: FunctionCallContent
|
||||
|
||||
|
||||
def test_function_call_content():
|
||||
"""Test the FunctionCallContent class to ensure it initializes correctly."""
|
||||
content = FunctionCallContent(call_id="1", name="example_function", arguments={"param1": "value1"})
|
||||
|
||||
# Check the type and content
|
||||
assert content.type == "function_call"
|
||||
assert content.name == "example_function"
|
||||
assert content.arguments == {"param1": "value1"}
|
||||
|
||||
# Ensure the instance is of type AIContent
|
||||
assert isinstance(content, AIContent)
|
||||
|
||||
|
||||
# region: FunctionResultContent
|
||||
|
||||
|
||||
def test_function_result_content():
|
||||
"""Test the FunctionResultContent class to ensure it initializes correctly."""
|
||||
content = FunctionResultContent(call_id="1", result={"param1": "value1"})
|
||||
|
||||
# Check the type and content
|
||||
assert content.type == "function_result"
|
||||
assert content.result == {"param1": "value1"}
|
||||
|
||||
# Ensure the instance is of type AIContent
|
||||
assert isinstance(content, AIContent)
|
||||
|
||||
|
||||
# region: UsageDetails
|
||||
|
||||
|
||||
def test_usage_details():
|
||||
usage = UsageDetails(input_token_count=5, output_token_count=10, total_token_count=15)
|
||||
assert usage.input_token_count == 5
|
||||
assert usage.output_token_count == 10
|
||||
assert usage.total_token_count == 15
|
||||
assert usage.additional_counts == {}
|
||||
|
||||
|
||||
def test_usage_details_addition():
|
||||
usage1 = UsageDetails(
|
||||
input_token_count=5,
|
||||
output_token_count=10,
|
||||
total_token_count=15,
|
||||
test1=10,
|
||||
test2=20,
|
||||
)
|
||||
usage2 = UsageDetails(
|
||||
input_token_count=3,
|
||||
output_token_count=6,
|
||||
total_token_count=9,
|
||||
test1=10,
|
||||
test3=30,
|
||||
)
|
||||
|
||||
combined_usage = usage1 + usage2
|
||||
assert combined_usage.input_token_count == 8
|
||||
assert combined_usage.output_token_count == 16
|
||||
assert combined_usage.total_token_count == 24
|
||||
assert combined_usage.additional_counts["test1"] == 20
|
||||
assert combined_usage.additional_counts["test2"] == 20
|
||||
assert combined_usage.additional_counts["test3"] == 30
|
||||
|
||||
|
||||
def test_usage_details_fail():
|
||||
with raises(ValidationError):
|
||||
UsageDetails(input_token_count=5, output_token_count=10, total_token_count=15, wrong_type="42.923")
|
||||
|
||||
|
||||
def test_usage_details_additional_counts():
|
||||
usage = UsageDetails(input_token_count=5, output_token_count=10, total_token_count=15, **{"test": 1})
|
||||
assert usage.additional_counts["test"] == 1
|
||||
|
||||
|
||||
# region: AIContent Serialization
|
||||
|
||||
|
||||
@mark.parametrize(
|
||||
"content_type, args",
|
||||
[
|
||||
(TextContent, {"text": "Hello, world!"}),
|
||||
(DataContent, {"data": b"Hello, world!", "media_type": "text/plain"}),
|
||||
(UriContent, {"uri": "http://example.com", "media_type": "text/html"}),
|
||||
(FunctionCallContent, {"call_id": "1", "name": "example_function", "arguments": {}}),
|
||||
(FunctionResultContent, {"call_id": "1", "result": {}}),
|
||||
],
|
||||
)
|
||||
def test_ai_content_serialization(content_type: type[AIContent], args: dict):
|
||||
content = content_type(**args)
|
||||
serialized = content.model_dump()
|
||||
deserialized = content_type.model_validate(serialized)
|
||||
assert deserialized == content
|
||||
|
||||
class TestModel(BaseModel):
|
||||
content: AIContents
|
||||
|
||||
test_item = TestModel.model_validate({"content": serialized})
|
||||
|
||||
assert isinstance(test_item.content, content_type)
|
||||
|
||||
|
||||
# region: ChatMessage
|
||||
|
||||
|
||||
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?")
|
||||
|
||||
# Check the type and content
|
||||
assert message.role == ChatRole.USER
|
||||
assert len(message.contents) == 1
|
||||
assert isinstance(message.contents[0], TextContent)
|
||||
assert message.contents[0].text == "Hello, how are you?"
|
||||
assert message.text == "Hello, how are you?"
|
||||
|
||||
# Ensure the instance is of type AIContent
|
||||
assert isinstance(message.contents[0], AIContent)
|
||||
|
||||
|
||||
def test_chat_message_contents():
|
||||
"""Test the ChatMessage class to ensure it initializes correctly with contents."""
|
||||
# Create a ChatMessage with a role and multiple contents
|
||||
content1 = TextContent("Hello, how are you?")
|
||||
content2 = TextContent("I'm fine, thank you!")
|
||||
message = ChatMessage(role="user", contents=[content1, content2])
|
||||
|
||||
# Check the type and content
|
||||
assert message.role == ChatRole.USER
|
||||
assert len(message.contents) == 2
|
||||
assert isinstance(message.contents[0], TextContent)
|
||||
assert isinstance(message.contents[1], TextContent)
|
||||
assert message.contents[0].text == "Hello, how are you?"
|
||||
assert message.contents[1].text == "I'm fine, thank you!"
|
||||
assert message.text == "Hello, how are you?\nI'm fine, thank you!"
|
||||
|
||||
|
||||
# region: ChatResponse
|
||||
|
||||
|
||||
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!")
|
||||
|
||||
# Create a ChatResponse with the message
|
||||
response = ChatResponse(messages=message)
|
||||
|
||||
# Check the type and content
|
||||
assert response.messages[0].role == ChatRole.ASSISTANT
|
||||
assert response.messages[0].text == "I'm doing well, thank you!"
|
||||
assert isinstance(response.messages[0], ChatMessage)
|
||||
|
||||
|
||||
# region: StructuredResponse
|
||||
|
||||
|
||||
def test_structured_response():
|
||||
"""Test the StructuredResponse class to ensure it initializes correctly with a value."""
|
||||
|
||||
class ResponseModel(BaseModel):
|
||||
content: str
|
||||
action: str
|
||||
|
||||
# Create a StructuredResponse with a value
|
||||
response = StructuredResponse[ResponseModel](
|
||||
value=ResponseModel(content="Hello, world!", action="test"),
|
||||
text="{'content': 'Hello, world!', 'action': 'test'}",
|
||||
)
|
||||
|
||||
# Check the type and content
|
||||
assert response.value == ResponseModel(content="Hello, world!", action="test")
|
||||
assert isinstance(response, StructuredResponse)
|
||||
|
||||
|
||||
# region: ChatResponseUpdate
|
||||
|
||||
|
||||
def test_chat_response_update():
|
||||
"""Test the ChatResponseUpdate class to ensure it initializes correctly with a message."""
|
||||
# Create a ChatMessage
|
||||
message = TextContent(text="I'm doing well, thank you!")
|
||||
|
||||
# Create a ChatResponseUpdate with the message
|
||||
response_update = ChatResponseUpdate(contents=[message])
|
||||
|
||||
# Check the type and content
|
||||
assert response_update.contents[0].text == "I'm doing well, thank you!"
|
||||
assert isinstance(response_update.contents[0], TextContent)
|
||||
|
||||
|
||||
def test_chat_response_updates_to_chat_response_one():
|
||||
"""Test converting ChatResponseUpdate to ChatResponse."""
|
||||
# Create a ChatMessage
|
||||
message1 = TextContent("I'm doing well, ")
|
||||
message2 = TextContent("thank you!")
|
||||
|
||||
# Create a ChatResponseUpdate with the message
|
||||
response_updates = [
|
||||
ChatResponseUpdate(text=message1, message_id="1"),
|
||||
ChatResponseUpdate(text=message2, message_id="1"),
|
||||
]
|
||||
|
||||
# Convert to ChatResponse
|
||||
chat_response = ChatResponse.from_chat_response_updates(response_updates)
|
||||
|
||||
# Check the type and content
|
||||
assert len(chat_response.messages) == 1
|
||||
assert chat_response.text == "I'm doing well, \nthank you!"
|
||||
assert isinstance(chat_response.messages[0], ChatMessage)
|
||||
assert len(chat_response.messages[0].contents) == 1
|
||||
assert chat_response.messages[0].message_id == "1"
|
||||
|
||||
|
||||
def test_chat_response_updates_to_chat_response_two():
|
||||
"""Test converting ChatResponseUpdate to ChatResponse."""
|
||||
# Create a ChatMessage
|
||||
message1 = TextContent("I'm doing well, ")
|
||||
message2 = TextContent("thank you!")
|
||||
|
||||
# Create a ChatResponseUpdate with the message
|
||||
response_updates = [
|
||||
ChatResponseUpdate(text=message1, message_id="1"),
|
||||
ChatResponseUpdate(text=message2, message_id="2"),
|
||||
]
|
||||
|
||||
# Convert to ChatResponse
|
||||
chat_response = ChatResponse.from_chat_response_updates(response_updates)
|
||||
|
||||
# Check the type and content
|
||||
assert len(chat_response.messages) == 2
|
||||
assert chat_response.text == "I'm doing well, \nthank you!"
|
||||
assert isinstance(chat_response.messages[0], ChatMessage)
|
||||
assert chat_response.messages[0].message_id == "1"
|
||||
assert isinstance(chat_response.messages[1], ChatMessage)
|
||||
assert chat_response.messages[1].message_id == "2"
|
||||
|
||||
|
||||
def test_chat_response_updates_to_chat_response_multiple():
|
||||
"""Test converting ChatResponseUpdate to ChatResponse."""
|
||||
# Create a ChatMessage
|
||||
message1 = TextContent("I'm doing well, ")
|
||||
message2 = TextContent("thank you!")
|
||||
|
||||
# Create a ChatResponseUpdate with the message
|
||||
response_updates = [
|
||||
ChatResponseUpdate(text=message1, message_id="1"),
|
||||
ChatResponseUpdate(contents=[TextReasoningContent(text="Additional context")], message_id="1"),
|
||||
ChatResponseUpdate(text=message2, message_id="1"),
|
||||
]
|
||||
|
||||
# Convert to ChatResponse
|
||||
chat_response = ChatResponse.from_chat_response_updates(response_updates)
|
||||
|
||||
# Check the type and content
|
||||
assert len(chat_response.messages) == 1
|
||||
assert chat_response.text == "I'm doing well, \nthank you!"
|
||||
assert isinstance(chat_response.messages[0], ChatMessage)
|
||||
assert len(chat_response.messages[0].contents) == 3
|
||||
assert chat_response.messages[0].message_id == "1"
|
||||
|
||||
|
||||
def test_chat_response_updates_to_chat_response_multiple_multiple():
|
||||
"""Test converting ChatResponseUpdate to ChatResponse."""
|
||||
# Create a ChatMessage
|
||||
message1 = TextContent("I'm doing well, ")
|
||||
message2 = TextContent("thank you!")
|
||||
|
||||
# Create a ChatResponseUpdate with the message
|
||||
response_updates = [
|
||||
ChatResponseUpdate(text=message1, message_id="1"),
|
||||
ChatResponseUpdate(text=message2, message_id="1"),
|
||||
ChatResponseUpdate(contents=[TextReasoningContent(text="Additional context")], message_id="1"),
|
||||
ChatResponseUpdate(contents=[TextContent(text="More context")], message_id="1"),
|
||||
ChatResponseUpdate(text="Final part", message_id="1"),
|
||||
]
|
||||
|
||||
# Convert to ChatResponse
|
||||
chat_response = ChatResponse.from_chat_response_updates(response_updates)
|
||||
|
||||
# Check the type and content
|
||||
assert len(chat_response.messages) == 1
|
||||
assert chat_response.text == "I'm doing well, \nthank you!\nMore context\nFinal part"
|
||||
assert isinstance(chat_response.messages[0], ChatMessage)
|
||||
assert len(chat_response.messages[0].contents) == 3
|
||||
assert chat_response.messages[0].message_id == "1"
|
||||
|
||||
|
||||
# region: ChatToolMode
|
||||
|
||||
|
||||
def test_chat_tool_mode():
|
||||
"""Test the ChatToolMode class to ensure it initializes correctly."""
|
||||
# Create instances of ChatToolMode
|
||||
auto_mode = ChatToolMode.AUTO
|
||||
required_any = ChatToolMode.REQUIRED_ANY
|
||||
required_mode = ChatToolMode.REQUIRED("example_function")
|
||||
none_mode = ChatToolMode.NONE
|
||||
|
||||
# Check the type and content
|
||||
assert auto_mode.mode == "auto"
|
||||
assert auto_mode.required_function_name is None
|
||||
assert required_any.mode == "required"
|
||||
assert required_any.required_function_name is None
|
||||
assert required_mode.mode == "required"
|
||||
assert required_mode.required_function_name == "example_function"
|
||||
assert none_mode.mode == "none"
|
||||
assert none_mode.required_function_name is None
|
||||
|
||||
# Ensure the instances are of type ChatToolMode
|
||||
assert isinstance(auto_mode, ChatToolMode)
|
||||
assert isinstance(required_any, ChatToolMode)
|
||||
assert isinstance(required_mode, ChatToolMode)
|
||||
assert isinstance(none_mode, ChatToolMode)
|
||||
|
||||
assert ChatToolMode.REQUIRED("example_function") == ChatToolMode.REQUIRED("example_function")
|
||||
|
||||
|
||||
def test_chat_tool_mode_from_dict():
|
||||
"""Test creating ChatToolMode from a dictionary."""
|
||||
mode_dict = {"mode": "required", "required_function_name": "example_function"}
|
||||
mode = ChatToolMode(**mode_dict)
|
||||
|
||||
# Check the type and content
|
||||
assert mode.mode == "required"
|
||||
assert mode.required_function_name == "example_function"
|
||||
|
||||
# Ensure the instance is of type ChatToolMode
|
||||
assert isinstance(mode, ChatToolMode)
|
||||
|
||||
|
||||
def test_generated_embeddings():
|
||||
"""Test the GeneratedEmbeddings class to ensure it initializes correctly."""
|
||||
# Create an instance of GeneratedEmbeddings
|
||||
embeddings = GeneratedEmbeddings(embeddings=[[0.1, 0.2, 0.3]])
|
||||
|
||||
# Check the type and content
|
||||
assert embeddings.embeddings == [[0.1, 0.2, 0.3]]
|
||||
|
||||
# Ensure the instance is of type GeneratedEmbeddings
|
||||
assert isinstance(embeddings, GeneratedEmbeddings)
|
||||
assert issubclass(GeneratedEmbeddings, MutableSequence)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
from agent_framework import __version__
|
||||
|
||||
|
||||
def test_version():
|
||||
assert __version__ is not None
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
|
||||
__all__ = ["__version__"]
|
||||
@@ -0,0 +1,74 @@
|
||||
[project]
|
||||
name = "agent-framework-openai"
|
||||
description = "OpenAI integrations for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "SK-Support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "0.1.0b1"
|
||||
license = {file = "../../LICENSE"}
|
||||
urls.homepage = "https://learn.microsoft.com/en-us/semantic-kernel/overview/"
|
||||
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 :: 5 - Production/Stable",
|
||||
"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",
|
||||
"Framework :: Pydantic :: 2",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework",
|
||||
"openai>=1.93.0",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = 'tests'
|
||||
addopts = "-ra -q -r fEX"
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
filterwarnings = []
|
||||
timeout = 120
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
|
||||
[tool.pyright]
|
||||
extend = "../../pyproject.toml"
|
||||
exclude = ['tests', ".venv"]
|
||||
|
||||
[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
|
||||
disallow_any_unimported = true
|
||||
|
||||
[tool.bandit]
|
||||
targets = ["agent_framework"]
|
||||
exclude_dirs = ["tests"]
|
||||
|
||||
[tool.poe]
|
||||
executor.type = "uv"
|
||||
include = "../../shared_tasks.toml"
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-name = "agent_framework.openai"
|
||||
module-root = ""
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7.19,<0.8.0"]
|
||||
build-backend = "uv_build"
|
||||
@@ -0,0 +1,24 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
from pytest import mark
|
||||
|
||||
|
||||
@mark.xfail(reason="Not solved")
|
||||
def test_self():
|
||||
try:
|
||||
from agent_framework.openai import __version__
|
||||
except ImportError:
|
||||
__version__ = None
|
||||
|
||||
assert __version__ is not None
|
||||
|
||||
|
||||
def test_agent_framework():
|
||||
try:
|
||||
from agent_framework import TextContent
|
||||
except ImportError:
|
||||
TextContent = None
|
||||
assert TextContent is not None
|
||||
text = TextContent("Hello, world!")
|
||||
assert text is not None
|
||||
Reference in New Issue
Block a user