mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: fix: @ai_function doesn't properly handle 'self' param (#2266)
* Fixes Python: @ai_function doesn't properly handle 'self' param Fixes #1343 * fix for declaration only funcs * fix mypy
This commit is contained in:
@@ -1305,26 +1305,20 @@ async def test_approved_function_call_successful_execution(chat_client_base: Cha
|
||||
assert success_result.result == "Success value1"
|
||||
|
||||
|
||||
async def test_declaration_only_tool_not_executed(chat_client_base: ChatClientProtocol):
|
||||
"""Test that declaration_only tools are not executed."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="declaration_func")
|
||||
def declaration_func_inner(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Result {arg1}"
|
||||
|
||||
# Create a new AIFunction with declaration_only set
|
||||
async def test_declaration_only_tool(chat_client_base: ChatClientProtocol):
|
||||
"""Test that declaration_only tools without implementation (func=None) are not executed."""
|
||||
from agent_framework import AIFunction
|
||||
|
||||
# Create a truly declaration-only function with no implementation
|
||||
declaration_func = AIFunction(
|
||||
name="declaration_func",
|
||||
func=declaration_func_inner,
|
||||
additional_properties={"declaration_only": True},
|
||||
func=None,
|
||||
description="A declaration-only function for testing",
|
||||
input_model={"type": "object", "properties": {"arg1": {"type": "string"}}, "required": ["arg1"]},
|
||||
)
|
||||
# Set declaration_only on the instance
|
||||
object.__setattr__(declaration_func, "_declaration_only", True)
|
||||
|
||||
# Verify it's marked as declaration_only
|
||||
assert declaration_func.declaration_only is True
|
||||
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
@@ -1338,8 +1332,6 @@ async def test_declaration_only_tool_not_executed(chat_client_base: ChatClientPr
|
||||
|
||||
response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[declaration_func])
|
||||
|
||||
# Function should NOT be executed
|
||||
assert exec_counter == 0
|
||||
# Should have the function call in messages but not a result
|
||||
function_calls = [
|
||||
content
|
||||
@@ -1349,6 +1341,15 @@ async def test_declaration_only_tool_not_executed(chat_client_base: ChatClientPr
|
||||
]
|
||||
assert len(function_calls) >= 1
|
||||
|
||||
# Should not have a function result
|
||||
function_results = [
|
||||
content
|
||||
for msg in response.messages
|
||||
for content in msg.contents
|
||||
if isinstance(content, FunctionResultContent) and content.call_id == "1"
|
||||
]
|
||||
assert len(function_results) == 0
|
||||
|
||||
|
||||
async def test_multiple_function_calls_parallel_execution(chat_client_base: ChatClientProtocol):
|
||||
"""Test that multiple function calls are executed in parallel."""
|
||||
|
||||
@@ -104,6 +104,136 @@ async def test_ai_function_decorator_with_async():
|
||||
assert (await async_test_tool(1, 2)) == 3
|
||||
|
||||
|
||||
def test_ai_function_decorator_in_class():
|
||||
"""Test the ai_function decorator."""
|
||||
|
||||
class my_tools:
|
||||
@ai_function(name="test_tool", description="A test tool")
|
||||
def test_tool(self, x: int, y: int) -> int:
|
||||
"""A simple function that adds two numbers."""
|
||||
return x + y
|
||||
|
||||
test_tool = my_tools().test_tool
|
||||
|
||||
assert isinstance(test_tool, ToolProtocol)
|
||||
assert isinstance(test_tool, AIFunction)
|
||||
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_shared_state():
|
||||
"""Test that decorated methods maintain shared state across multiple calls and tool usage."""
|
||||
|
||||
class StatefulCounter:
|
||||
"""A class that maintains a counter and provides decorated methods to interact with it."""
|
||||
|
||||
def __init__(self, initial_value: int = 0):
|
||||
self.counter = initial_value
|
||||
self.operation_log: list[str] = []
|
||||
|
||||
@ai_function(name="increment", description="Increment the counter")
|
||||
def increment(self, amount: int) -> str:
|
||||
"""Increment the counter by the given amount."""
|
||||
self.counter += amount
|
||||
self.operation_log.append(f"increment({amount})")
|
||||
return f"Counter incremented by {amount}. New value: {self.counter}"
|
||||
|
||||
@ai_function(name="get_value", description="Get the current counter value")
|
||||
def get_value(self) -> str:
|
||||
"""Get the current counter value."""
|
||||
self.operation_log.append("get_value()")
|
||||
return f"Current counter value: {self.counter}"
|
||||
|
||||
@ai_function(name="multiply", description="Multiply the counter")
|
||||
def multiply(self, factor: int) -> str:
|
||||
"""Multiply the counter by the given factor."""
|
||||
self.counter *= factor
|
||||
self.operation_log.append(f"multiply({factor})")
|
||||
return f"Counter multiplied by {factor}. New value: {self.counter}"
|
||||
|
||||
# Create a single instance with shared state
|
||||
counter_instance = StatefulCounter(initial_value=10)
|
||||
|
||||
# Get the decorated methods - these will be used by different "agents" or tools
|
||||
increment_tool = counter_instance.increment
|
||||
get_value_tool = counter_instance.get_value
|
||||
multiply_tool = counter_instance.multiply
|
||||
|
||||
# Verify they are AIFunction instances
|
||||
assert isinstance(increment_tool, AIFunction)
|
||||
assert isinstance(get_value_tool, AIFunction)
|
||||
assert isinstance(multiply_tool, AIFunction)
|
||||
|
||||
# Tool 1 (increment) is used
|
||||
result1 = increment_tool(5)
|
||||
assert result1 == "Counter incremented by 5. New value: 15"
|
||||
assert counter_instance.counter == 15
|
||||
|
||||
# Tool 2 (get_value) sees the state change from tool 1
|
||||
result2 = get_value_tool()
|
||||
assert result2 == "Current counter value: 15"
|
||||
assert counter_instance.counter == 15
|
||||
|
||||
# Tool 3 (multiply) modifies the shared state
|
||||
result3 = multiply_tool(3)
|
||||
assert result3 == "Counter multiplied by 3. New value: 45"
|
||||
assert counter_instance.counter == 45
|
||||
|
||||
# Tool 2 (get_value) sees the state change from tool 3
|
||||
result4 = get_value_tool()
|
||||
assert result4 == "Current counter value: 45"
|
||||
assert counter_instance.counter == 45
|
||||
|
||||
# Tool 1 (increment) sees the current state and modifies it
|
||||
result5 = increment_tool(10)
|
||||
assert result5 == "Counter incremented by 10. New value: 55"
|
||||
assert counter_instance.counter == 55
|
||||
|
||||
# Verify the operation log shows all operations in order
|
||||
assert counter_instance.operation_log == [
|
||||
"increment(5)",
|
||||
"get_value()",
|
||||
"multiply(3)",
|
||||
"get_value()",
|
||||
"increment(10)",
|
||||
]
|
||||
|
||||
# Verify the parameters don't include 'self'
|
||||
assert increment_tool.parameters() == {
|
||||
"properties": {"amount": {"title": "Amount", "type": "integer"}},
|
||||
"required": ["amount"],
|
||||
"title": "increment_input",
|
||||
"type": "object",
|
||||
}
|
||||
assert multiply_tool.parameters() == {
|
||||
"properties": {"factor": {"title": "Factor", "type": "integer"}},
|
||||
"required": ["factor"],
|
||||
"title": "multiply_input",
|
||||
"type": "object",
|
||||
}
|
||||
assert get_value_tool.parameters() == {
|
||||
"properties": {},
|
||||
"title": "get_value_input",
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
# Test with invoke method as well (simulating agent execution)
|
||||
result6 = await increment_tool.invoke(amount=5)
|
||||
assert result6 == "Counter incremented by 5. New value: 60"
|
||||
assert counter_instance.counter == 60
|
||||
|
||||
result7 = await get_value_tool.invoke()
|
||||
assert result7 == "Current counter value: 60"
|
||||
assert counter_instance.counter == 60
|
||||
|
||||
|
||||
async def test_ai_function_invoke_telemetry_enabled(span_exporter: InMemorySpanExporter):
|
||||
"""Test the ai_function invoke method with telemetry enabled."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user