Python: Fix Http Schema (#2112)

* Rename to threadid

* Respond in plain text

* Make snake-case

* Add http prefix

* rename to wait-for-response

* Add query param check

* address comments
This commit is contained in:
Laveesh Rohra
2025-11-12 09:56:19 -08:00
committed by GitHub
Unverified
parent ebab25b196
commit ff28066c9c
24 changed files with 692 additions and 436 deletions
@@ -45,9 +45,9 @@ class TestSampleSingleAgent:
"""Test sending a simple message with JSON payload."""
response = SampleTestHelper.post_json(
f"{self.base_url}/run",
{"message": "Tell me a short joke about cloud computing.", "sessionId": "test-simple-json"},
{"message": "Tell me a short joke about cloud computing.", "thread_id": "test-simple-json"},
)
# Agent can return 200 (immediate) or 202 (async with wait_for_completion=false)
# Agent can return 200 (immediate) or 202 (async with wait_for_response=false)
assert response.status_code in [200, 202]
data = response.json()
@@ -58,37 +58,35 @@ class TestSampleSingleAgent:
assert data["message_count"] >= 1
else:
# Async response - check we got correlation info
assert "correlationId" in data or "sessionId" in data
assert "correlation_id" in data or "thread_id" in data
def test_simple_message_plain_text(self) -> None:
"""Test sending a message with plain text payload."""
response = SampleTestHelper.post_text(f"{self.base_url}/run", "Tell me a short joke about networking.")
assert response.status_code in [200, 202]
data = response.json()
if response.status_code == 200:
assert data["status"] == "success"
assert "response" in data
# Agent responded with plain text when the request body was text/plain.
assert response.text.strip()
assert response.headers.get("x-ms-thread-id") is not None
def test_session_key_in_query(self) -> None:
"""Test using sessionKey in query parameter."""
def test_thread_id_in_query(self) -> None:
"""Test using thread_id in query parameter."""
response = SampleTestHelper.post_text(
f"{self.base_url}/run?sessionKey=test-query-session", "Tell me a short joke about weather in Texas."
f"{self.base_url}/run?thread_id=test-query-thread", "Tell me a short joke about weather in Texas."
)
assert response.status_code in [200, 202]
data = response.json()
if response.status_code == 200:
assert data["status"] == "success"
assert response.text.strip()
assert response.headers.get("x-ms-thread-id") == "test-query-thread"
def test_conversation_continuity(self) -> None:
"""Test conversation context is maintained across requests."""
session_id = "test-continuity"
thread_id = "test-continuity"
# First message
response1 = SampleTestHelper.post_json(
f"{self.base_url}/run",
{"message": "Tell me a short joke about weather in Seattle.", "sessionId": session_id},
{"message": "Tell me a short joke about weather in Seattle.", "thread_id": thread_id},
)
assert response1.status_code in [200, 202]
@@ -98,7 +96,7 @@ class TestSampleSingleAgent:
# Second message in same session
response2 = SampleTestHelper.post_json(
f"{self.base_url}/run", {"message": "What about San Francisco?", "sessionId": session_id}
f"{self.base_url}/run", {"message": "What about San Francisco?", "thread_id": thread_id}
)
assert response2.status_code == 200
data2 = response2.json()
@@ -107,7 +105,7 @@ class TestSampleSingleAgent:
# In async mode, we can't easily test message count
# Just verify we can make multiple calls
response2 = SampleTestHelper.post_json(
f"{self.base_url}/run", {"message": "What about Texas?", "sessionId": session_id}
f"{self.base_url}/run", {"message": "What about Texas?", "thread_id": thread_id}
)
assert response2.status_code == 202
@@ -38,21 +38,26 @@ class TestSampleMultiAgent:
def test_weather_agent(self) -> None:
"""Test WeatherAgent endpoint."""
response = SampleTestHelper.post_json(
f"{self.weather_base_url}/run", {"message": "What is the weather in Seattle?"}
f"{self.weather_base_url}/run",
{"message": "What is the weather in Seattle?"},
)
assert response.status_code == 202
assert response.status_code == 200
data = response.json()
assert data["status"] == "accepted"
assert data["status"] == "success"
assert "response" in data
def test_math_agent(self) -> None:
"""Test MathAgent endpoint."""
response = SampleTestHelper.post_json(
f"{self.math_base_url}/run", {"message": "Calculate a 20% tip on a $50 bill"}
f"{self.math_base_url}/run",
{"message": "Calculate a 20% tip on a $50 bill", "wait_for_response": False},
)
assert response.status_code == 202
data = response.json()
assert data["status"] == "accepted"
assert "response" in data
assert "correlation_id" in data
assert "thread_id" in data
if __name__ == "__main__":
@@ -14,6 +14,8 @@ Usage:
uv run pytest packages/azurefunctions/tests/integration_tests/test_03_callbacks.py -v
"""
from typing import Any
import pytest
import requests
@@ -39,39 +41,60 @@ class TestSampleCallbacks:
"""Provide the callback agent base URL for each test."""
self.base_url = f"{base_url}/api/agents/CallbackAgent"
@staticmethod
def _wait_for_callback_events(base_url: str, thread_id: str) -> list[dict[str, Any]]:
events: list[dict[str, Any]] = []
response = SampleTestHelper.get(f"{base_url}/callbacks/{thread_id}")
if response.status_code == 200:
events = response.json()
return events
def test_agent_with_callbacks(self) -> None:
"""Test agent execution with callback tracking."""
conversation_id = "test-callback"
thread_id = "test-callback"
response = SampleTestHelper.post_json(
f"{self.base_url}/run", {"message": "Tell me about Python", "conversationId": conversation_id}
f"{self.base_url}/run",
{"message": "Tell me about Python", "thread_id": thread_id},
)
assert response.status_code == 202
assert response.status_code == 200
data = response.json()
assert data["status"] == "accepted"
assert data["status"] == "success"
events = self._wait_for_callback_events(self.base_url, thread_id)
assert events
assert any(event.get("event_type") == "final" for event in events)
def test_get_callbacks(self) -> None:
"""Test retrieving callback events."""
conversation_id = "test-callback-retrieve"
thread_id = "test-callback-retrieve"
# Send a message first
SampleTestHelper.post_json(f"{self.base_url}/run", {"message": "Hello", "conversationId": conversation_id})
SampleTestHelper.post_json(
f"{self.base_url}/run",
{"message": "Hello", "thread_id": thread_id, "wait_for_response": False},
)
# Get callbacks
response = SampleTestHelper.get(f"{self.base_url}/callbacks/{conversation_id}")
response = SampleTestHelper.get(f"{self.base_url}/callbacks/{thread_id}")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_delete_callbacks(self) -> None:
"""Test clearing callback events."""
conversation_id = "test-callback-delete"
thread_id = "test-callback-delete"
# Send a message first
SampleTestHelper.post_json(f"{self.base_url}/run", {"message": "Test", "conversationId": conversation_id})
SampleTestHelper.post_json(
f"{self.base_url}/run",
{"message": "Test", "thread_id": thread_id, "wait_for_response": False},
)
# Delete callbacks
response = requests.delete(f"{self.base_url}/callbacks/{conversation_id}", timeout=TIMEOUT)
response = requests.delete(f"{self.base_url}/callbacks/{thread_id}", timeout=TIMEOUT)
assert response.status_code == 204
+163 -39
View File
@@ -12,8 +12,8 @@ import pytest
from agent_framework import AgentRunResponse, ChatMessage
from agent_framework_azurefunctions import AgentFunctionApp
from agent_framework_azurefunctions._app import WAIT_FOR_RESPONSE_FIELD, WAIT_FOR_RESPONSE_HEADER
from agent_framework_azurefunctions._entities import AgentEntity, AgentState, create_agent_entity
from agent_framework_azurefunctions._errors import IncomingRequestError
TFunc = TypeVar("TFunc", bound=Callable[..., Any])
@@ -150,6 +150,38 @@ class TestAgentFunctionAppSetup:
# Verify agent is registered
assert "TestAgent" in app.agents
def test_http_function_name_uses_prefix_format(self) -> None:
"""Ensure function names follow the prefix-agent naming convention."""
mock_agent = Mock()
mock_agent.name = "Agent 42"
captured_names: list[str] = []
def capture_function_name(
self: AgentFunctionApp, name: str, *args: Any, **kwargs: Any
) -> Callable[[TFunc], TFunc]:
def decorator(func: TFunc) -> TFunc:
captured_names.append(name)
return func
return decorator
def passthrough_decorator(*args: Any, **kwargs: Any) -> Callable[[TFunc], TFunc]:
def decorator(func: TFunc) -> TFunc:
return func
return decorator
with (
patch.object(AgentFunctionApp, "function_name", new=capture_function_name),
patch.object(AgentFunctionApp, "route", new=passthrough_decorator),
patch.object(AgentFunctionApp, "durable_client_input", new=passthrough_decorator),
patch.object(AgentFunctionApp, "entity_trigger", new=passthrough_decorator),
):
AgentFunctionApp(agents=[mock_agent])
assert captured_names == ["http-Agent_42"]
def test_setup_skips_http_trigger_when_disabled(self) -> None:
"""Test that HTTP trigger is not created when disabled."""
mock_agent = Mock()
@@ -236,8 +268,8 @@ class TestAgentFunctionAppSetup:
assert "Agent2" in app2.agents
class TestWaitForCompletionAndCorrelationId:
"""Tests for wait_for_completion flag and correlation ID handling."""
class TestWaitForResponseAndCorrelationId:
"""Tests for wait_for_response flag and correlation ID handling."""
def _create_app(self) -> AgentFunctionApp:
mock_agent = Mock()
@@ -255,21 +287,35 @@ class TestWaitForCompletionAndCorrelationId:
request.params = params or {}
return request
def test_wait_for_completion_header_true(self) -> None:
"""Test that the wait-for-completion header is honored."""
def test_wait_for_response_header_true(self) -> None:
"""Test that the wait-for-response header is honored."""
app = self._create_app()
request = self._make_request(headers={"X-Wait-For-Completion": "true"})
request = self._make_request(headers={WAIT_FOR_RESPONSE_HEADER: "true"})
assert app._should_wait_for_completion(request, {}) is True
assert app._should_wait_for_response(request, {}) is True
def test_wait_for_completion_body_variants(self) -> None:
"""Test that multiple payload spellings are accepted."""
def test_wait_for_response_body_snake_case(self) -> None:
"""Test that payload controls wait_for_response."""
app = self._create_app()
request = self._make_request()
assert app._should_wait_for_completion(request, {"wait_for_completion": "true"}) is True
assert app._should_wait_for_completion(request, {"waitForCompletion": "1"}) is True
assert app._should_wait_for_completion(request, {"WaitForCompletion": "no"}) is False
assert app._should_wait_for_response(request, {WAIT_FOR_RESPONSE_FIELD: "true"}) is True
assert app._should_wait_for_response(request, {WAIT_FOR_RESPONSE_FIELD: "false"}) is False
assert app._should_wait_for_response(request, {WAIT_FOR_RESPONSE_FIELD: "0"}) is False
def test_wait_for_response_query_parameter(self) -> None:
"""Test that query parameter controls wait_for_response."""
app = self._create_app()
request = self._make_request(params={WAIT_FOR_RESPONSE_FIELD: "true"})
assert app._should_wait_for_response(request, {}) is True
def test_wait_for_response_query_precedence(self) -> None:
"""Test that query parameter overrides body value."""
app = self._create_app()
request = self._make_request(params={WAIT_FOR_RESPONSE_FIELD: "false"})
assert app._should_wait_for_response(request, {WAIT_FOR_RESPONSE_FIELD: "true"}) is False
class TestAgentEntityOperations:
@@ -287,13 +333,13 @@ class TestAgentEntityOperations:
result = await entity.run_agent(
mock_context,
{"message": "Test message", "conversation_id": "test-conv-123", "correlation_id": "corr-app-entity-1"},
{"message": "Test message", "thread_id": "test-conv-123", "correlation_id": "corr-app-entity-1"},
)
assert result["status"] == "success"
assert result["response"] == "Test response"
assert result["message"] == "Test message"
assert result["conversation_id"] == "test-conv-123"
assert result["thread_id"] == "test-conv-123"
assert entity.state.message_count == 1
async def test_entity_stores_conversation_history(self) -> None:
@@ -308,7 +354,7 @@ class TestAgentEntityOperations:
# Send first message
await entity.run_agent(
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-app-entity-2"}
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-app-entity-2"}
)
history = entity.state.conversation_history
@@ -337,12 +383,12 @@ class TestAgentEntityOperations:
assert entity.state.message_count == 0
await entity.run_agent(
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-app-entity-3a"}
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-app-entity-3a"}
)
assert entity.state.message_count == 1
await entity.run_agent(
mock_context, {"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-app-entity-3b"}
mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-app-entity-3b"}
)
assert entity.state.message_count == 2
@@ -391,7 +437,7 @@ class TestAgentEntityFactory:
mock_context.operation_name = "run_agent"
mock_context.get_input.return_value = {
"message": "Test message",
"conversation_id": "conv-123",
"thread_id": "conv-123",
"correlation_id": "corr-app-factory-1",
}
mock_context.get_state.return_value = None
@@ -478,7 +524,7 @@ class TestErrorHandling:
mock_context = Mock()
result = await entity.run_agent(
mock_context, {"message": "Test message", "conversation_id": "conv-1", "correlation_id": "corr-app-error-1"}
mock_context, {"message": "Test message", "thread_id": "conv-1", "correlation_id": "corr-app-error-1"}
)
assert result["status"] == "error"
@@ -521,48 +567,67 @@ class TestIncomingRequestParsing:
app = self._create_app()
request = Mock()
request.headers = {}
request.params = {}
request.get_json.side_effect = ValueError("Invalid JSON")
request.get_body.return_value = b"Plain text message"
req_body, message = app._parse_incoming_request(request)
req_body, message, response_format = app._parse_incoming_request(request)
assert req_body == {}
assert message == "Plain text message"
def test_parse_plain_text_requires_content(self) -> None:
"""Test that plain-text requests require message content."""
assert response_format == "text"
def test_parse_plain_text_trims_whitespace(self) -> None:
"""Plain-text parser returns an empty string when the body contains only whitespace."""
app = self._create_app()
request = Mock()
request.headers = {}
request.params = {}
request.get_json.side_effect = ValueError("Invalid JSON")
request.get_body.return_value = b" "
with pytest.raises(IncomingRequestError) as exc_info:
app._parse_incoming_request(request)
req_body, message, response_format = app._parse_incoming_request(request)
assert "Message is required" in str(exc_info.value)
assert req_body == {}
assert message == ""
assert response_format == "text"
def test_extract_session_key_from_query_params(self) -> None:
"""Test session key extraction from query parameters."""
def test_accept_header_prefers_json(self) -> None:
"""Test that the Accept header can force JSON responses for plain-text bodies."""
app = self._create_app()
request = Mock()
request.params = {"sessionId": "query-session"}
request.headers = {"accept": "application/json"}
request.params = {}
request.get_json.side_effect = ValueError("Invalid JSON")
request.get_body.return_value = b"Plain text message"
_, message, response_format = app._parse_incoming_request(request)
assert message == "Plain text message"
assert response_format == "json"
def test_extract_thread_id_from_query_params(self) -> None:
"""Test thread identifier extraction from query parameters."""
app = self._create_app()
request = Mock()
request.params = {"thread_id": "query-thread"}
req_body = {}
session_key = app._resolve_session_key(request, req_body)
thread_id = app._resolve_thread_id(request, req_body)
assert session_key == "query-session"
assert thread_id == "query-thread"
class TestHttpRunRoute:
"""Tests for the HTTP run route behavior."""
async def test_http_run_accepts_plain_text(self) -> None:
"""Test that the HTTP handler accepts plain-text requests."""
mock_agent = Mock()
mock_agent.name = "HttpAgent"
@staticmethod
def _get_run_handler(agent: Mock) -> Callable[[func.HttpRequest, Any], Awaitable[func.HttpResponse]]:
captured_handlers: dict[str | None, Callable[..., Awaitable[func.HttpResponse]]] = {}
def capture_decorator(*args: Any, **kwargs: Any) -> Callable[[TFunc], TFunc]:
@@ -585,13 +650,20 @@ class TestHttpRunRoute:
patch.object(AgentFunctionApp, "durable_client_input", new=capture_decorator),
patch.object(AgentFunctionApp, "entity_trigger", new=capture_decorator),
):
AgentFunctionApp(agents=[mock_agent], enable_health_check=False)
AgentFunctionApp(agents=[agent], enable_health_check=False)
run_route = f"agents/{mock_agent.name}/run"
handler = captured_handlers[run_route]
run_route = f"agents/{agent.name}/run"
return captured_handlers[run_route]
async def test_http_run_accepts_plain_text(self) -> None:
"""Test that the HTTP handler accepts plain-text requests."""
mock_agent = Mock()
mock_agent.name = "HttpAgent"
handler = self._get_run_handler(mock_agent)
request = Mock()
request.headers = {}
request.headers = {WAIT_FOR_RESPONSE_HEADER: "false"}
request.params = {}
request.route_params = {}
request.get_json.side_effect = ValueError("Invalid JSON")
@@ -602,12 +674,64 @@ class TestHttpRunRoute:
response = await handler(request, client)
assert response.status_code == 202
assert response.mimetype == "text/plain"
assert response.headers.get("x-ms-thread-id") is not None
assert response.get_body().decode("utf-8") == "Agent request accepted"
signal_args = client.signal_entity.call_args[0]
run_request = signal_args[2]
assert run_request["message"] == "Plain text via HTTP"
assert run_request["role"] == "user"
assert "thread_id" in run_request
async def test_http_run_accept_header_returns_json(self) -> None:
"""Test that Accept header requesting JSON results in JSON response."""
mock_agent = Mock()
mock_agent.name = "HttpAgentJson"
handler = self._get_run_handler(mock_agent)
request = Mock()
request.headers = {WAIT_FOR_RESPONSE_HEADER: "false", "Accept": "application/json"}
request.params = {}
request.route_params = {}
request.get_json.side_effect = ValueError("Invalid JSON")
request.get_body.return_value = b"Plain text via HTTP"
client = AsyncMock()
response = await handler(request, client)
assert response.status_code == 202
assert response.mimetype == "application/json"
assert response.headers.get("x-ms-thread-id") is None
body = response.get_body().decode("utf-8")
assert '"status": "accepted"' in body
async def test_http_run_rejects_empty_message(self) -> None:
"""Test that the HTTP handler rejects empty messages with a 400 response."""
mock_agent = Mock()
mock_agent.name = "HttpAgentEmpty"
handler = self._get_run_handler(mock_agent)
request = Mock()
request.headers = {WAIT_FOR_RESPONSE_HEADER: "false"}
request.params = {}
request.route_params = {}
request.get_json.side_effect = ValueError("Invalid JSON")
request.get_body.return_value = b" "
client = AsyncMock()
response = await handler(request, client)
assert response.status_code == 400
assert response.mimetype == "text/plain"
assert response.headers.get("x-ms-thread-id") is not None
assert response.get_body().decode("utf-8") == "Message is required"
client.signal_entity.assert_not_called()
if __name__ == "__main__":
@@ -12,11 +12,11 @@ from typing import Any, TypeVar
from unittest.mock import AsyncMock, Mock, patch
import pytest
from agent_framework import AgentRunResponse, AgentRunResponseUpdate, ChatMessage
from agent_framework import AgentRunResponse, AgentRunResponseUpdate, ChatMessage, Role
from pydantic import BaseModel
from agent_framework_azurefunctions._entities import AgentEntity, create_agent_entity
from agent_framework_azurefunctions._models import ChatRole, RunRequest
from agent_framework_azurefunctions._models import RunRequest
from agent_framework_azurefunctions._state import AgentState
TFunc = TypeVar("TFunc", bound=Callable[..., Any])
@@ -112,7 +112,7 @@ class TestAgentEntityRunAgent:
mock_context = Mock()
result = await entity.run_agent(
mock_context, {"message": "Test message", "conversation_id": "conv-123", "correlation_id": "corr-entity-1"}
mock_context, {"message": "Test message", "thread_id": "conv-123", "correlation_id": "corr-entity-1"}
)
# Verify agent.run was called
@@ -130,7 +130,7 @@ class TestAgentEntityRunAgent:
assert result["status"] == "success"
assert result["response"] == "Test response"
assert result["message"] == "Test message"
assert result["conversation_id"] == "conv-123"
assert result["thread_id"] == "conv-123"
async def test_run_agent_streaming_callbacks_invoked(self) -> None:
"""Ensure streaming updates trigger callbacks and run() is not used."""
@@ -157,7 +157,7 @@ class TestAgentEntityRunAgent:
mock_context,
{
"message": "Tell me something",
"conversation_id": "session-1",
"thread_id": "session-1",
"correlation_id": "corr-stream-1",
},
)
@@ -175,7 +175,7 @@ class TestAgentEntityRunAgent:
context = recorded_call.args[1]
assert context.agent_name == "StreamingAgent"
assert context.correlation_id == "corr-stream-1"
assert context.conversation_id == "session-1"
assert context.thread_id == "session-1"
assert context.request_message == "Tell me something"
final_call = callback.response_mock.await_args
@@ -183,7 +183,7 @@ class TestAgentEntityRunAgent:
final_response, final_context = final_call.args
assert final_context.agent_name == "StreamingAgent"
assert final_context.correlation_id == "corr-stream-1"
assert final_context.conversation_id == "session-1"
assert final_context.thread_id == "session-1"
assert final_context.request_message == "Tell me something"
assert getattr(final_response, "text", "").strip()
@@ -204,7 +204,7 @@ class TestAgentEntityRunAgent:
mock_context,
{
"message": "Hi",
"conversation_id": "session-2",
"thread_id": "session-2",
"correlation_id": "corr-final-1",
},
)
@@ -220,7 +220,7 @@ class TestAgentEntityRunAgent:
final_context = final_call.args[1]
assert final_context.agent_name == "NonStreamingAgent"
assert final_context.correlation_id == "corr-final-1"
assert final_context.conversation_id == "session-2"
assert final_context.thread_id == "session-2"
assert final_context.request_message == "Hi"
async def test_run_agent_updates_conversation_history(self) -> None:
@@ -233,7 +233,7 @@ class TestAgentEntityRunAgent:
mock_context = Mock()
await entity.run_agent(
mock_context, {"message": "User message", "conversation_id": "conv-1", "correlation_id": "corr-entity-2"}
mock_context, {"message": "User message", "thread_id": "conv-1", "correlation_id": "corr-entity-2"}
)
# Should have 2 entries: user message + assistant response
@@ -260,17 +260,17 @@ class TestAgentEntityRunAgent:
assert entity.state.message_count == 0
await entity.run_agent(
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-3a"}
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-3a"}
)
assert entity.state.message_count == 1
await entity.run_agent(
mock_context, {"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-3b"}
mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-3b"}
)
assert entity.state.message_count == 2
await entity.run_agent(
mock_context, {"message": "Message 3", "conversation_id": "conv-1", "correlation_id": "corr-entity-3c"}
mock_context, {"message": "Message 3", "thread_id": "conv-1", "correlation_id": "corr-entity-3c"}
)
assert entity.state.message_count == 3
@@ -283,27 +283,27 @@ class TestAgentEntityRunAgent:
mock_context = Mock()
await entity.run_agent(
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-4a"}
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-4a"}
)
assert entity.state.last_response == "Response 1"
mock_agent.run = AsyncMock(return_value=_agent_response("Response 2"))
await entity.run_agent(
mock_context, {"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-4b"}
mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-4b"}
)
assert entity.state.last_response == "Response 2"
async def test_run_agent_with_none_conversation_id(self) -> None:
"""Test run_agent with a None conversation identifier."""
async def test_run_agent_with_none_thread_id(self) -> None:
"""Test run_agent with a None thread identifier."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = AgentEntity(mock_agent)
mock_context = Mock()
with pytest.raises(ValueError, match="conversation_id"):
with pytest.raises(ValueError, match="thread_id"):
await entity.run_agent(
mock_context, {"message": "Message", "conversation_id": None, "correlation_id": "corr-entity-5"}
mock_context, {"message": "Message", "thread_id": None, "correlation_id": "corr-entity-5"}
)
async def test_run_agent_handles_response_without_text_attribute(self) -> None:
@@ -322,7 +322,7 @@ class TestAgentEntityRunAgent:
mock_context = Mock()
result = await entity.run_agent(
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-6"}
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-6"}
)
# Should handle gracefully
@@ -338,7 +338,7 @@ class TestAgentEntityRunAgent:
mock_context = Mock()
result = await entity.run_agent(
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-7"}
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-7"}
)
assert result["status"] == "success"
@@ -354,13 +354,13 @@ class TestAgentEntityRunAgent:
# Send multiple messages
await entity.run_agent(
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-8a"}
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-8a"}
)
await entity.run_agent(
mock_context, {"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-8b"}
mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-8b"}
)
await entity.run_agent(
mock_context, {"message": "Message 3", "conversation_id": "conv-1", "correlation_id": "corr-entity-8c"}
mock_context, {"message": "Message 3", "thread_id": "conv-1", "correlation_id": "corr-entity-8c"}
)
history = entity.state.conversation_history
@@ -421,10 +421,10 @@ class TestAgentEntityReset:
# Have a conversation
await entity.run_agent(
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-10a"}
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-10a"}
)
await entity.run_agent(
mock_context, {"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-10b"}
mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-10b"}
)
# Verify state before reset
@@ -463,7 +463,7 @@ class TestCreateAgentEntity:
mock_context.operation_name = "run_agent"
mock_context.get_input.return_value = {
"message": "Test message",
"conversation_id": "conv-123",
"thread_id": "conv-123",
"correlation_id": "corr-entity-factory",
}
mock_context.get_state.return_value = None
@@ -591,7 +591,7 @@ class TestErrorHandling:
mock_context = Mock()
result = await entity.run_agent(
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-error-1"}
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-error-1"}
)
assert result["status"] == "error"
@@ -608,7 +608,7 @@ class TestErrorHandling:
mock_context = Mock()
result = await entity.run_agent(
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-error-2"}
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-error-2"}
)
assert result["status"] == "error"
@@ -624,7 +624,7 @@ class TestErrorHandling:
mock_context = Mock()
result = await entity.run_agent(
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-error-3"}
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-error-3"}
)
assert result["status"] == "error"
@@ -659,12 +659,12 @@ class TestErrorHandling:
result = await entity.run_agent(
mock_context,
{"message": "Test message", "conversation_id": "conv-123", "correlation_id": "corr-entity-error-4"},
{"message": "Test message", "thread_id": "conv-123", "correlation_id": "corr-entity-error-4"},
)
# Even on error, message info should be preserved
assert result["message"] == "Test message"
assert result["conversation_id"] == "conv-123"
assert result["thread_id"] == "conv-123"
assert result["status"] == "error"
@@ -680,7 +680,7 @@ class TestConversationHistory:
mock_context = Mock()
await entity.run_agent(
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-1"}
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-history-1"}
)
# Check both user and assistant messages have timestamps
@@ -701,19 +701,19 @@ class TestConversationHistory:
mock_agent.run = AsyncMock(return_value=_agent_response("Response 1"))
await entity.run_agent(
mock_context,
{"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-2a"},
{"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-history-2a"},
)
mock_agent.run = AsyncMock(return_value=_agent_response("Response 2"))
await entity.run_agent(
mock_context,
{"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-2b"},
{"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-history-2b"},
)
mock_agent.run = AsyncMock(return_value=_agent_response("Response 3"))
await entity.run_agent(
mock_context,
{"message": "Message 3", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-2c"},
{"message": "Message 3", "thread_id": "conv-1", "correlation_id": "corr-entity-history-2c"},
)
# Verify order
@@ -735,11 +735,11 @@ class TestConversationHistory:
await entity.run_agent(
mock_context,
{"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-3a"},
{"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-history-3a"},
)
await entity.run_agent(
mock_context,
{"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-3b"},
{"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-history-3b"},
)
# Check role alternation
@@ -763,8 +763,8 @@ class TestRunRequestSupport:
request = RunRequest(
message="Test message",
conversation_id="conv-123",
role=ChatRole.USER,
thread_id="conv-123",
role=Role.USER,
enable_tool_calls=True,
correlation_id="corr-runreq-1",
)
@@ -774,7 +774,7 @@ class TestRunRequestSupport:
assert result["status"] == "success"
assert result["response"] == "Response"
assert result["message"] == "Test message"
assert result["conversation_id"] == "conv-123"
assert result["thread_id"] == "conv-123"
async def test_run_agent_with_dict_request(self) -> None:
"""Test run_agent with a dictionary request."""
@@ -786,7 +786,7 @@ class TestRunRequestSupport:
request_dict = {
"message": "Test message",
"conversation_id": "conv-456",
"thread_id": "conv-456",
"role": "system",
"enable_tool_calls": False,
"correlation_id": "corr-runreq-2",
@@ -796,7 +796,7 @@ class TestRunRequestSupport:
assert result["status"] == "success"
assert result["message"] == "Test message"
assert result["conversation_id"] == "conv-456"
assert result["thread_id"] == "conv-456"
async def test_run_agent_with_string_raises_without_correlation(self) -> None:
"""Test that run_agent rejects legacy string input without correlation ID."""
@@ -820,8 +820,8 @@ class TestRunRequestSupport:
# Send as system role
request = RunRequest(
message="System message",
conversation_id="conv-runreq-3",
role=ChatRole.SYSTEM,
thread_id="conv-runreq-3",
role=Role.SYSTEM,
correlation_id="corr-runreq-3",
)
@@ -843,7 +843,7 @@ class TestRunRequestSupport:
request = RunRequest(
message="What is the answer?",
conversation_id="conv-runreq-4",
thread_id="conv-runreq-4",
response_format=EntityStructuredResponse,
correlation_id="corr-runreq-4",
)
@@ -864,7 +864,7 @@ class TestRunRequestSupport:
mock_context = Mock()
request = RunRequest(
message="Test", conversation_id="conv-runreq-5", enable_tool_calls=False, correlation_id="corr-runreq-5"
message="Test", thread_id="conv-runreq-5", enable_tool_calls=False, correlation_id="corr-runreq-5"
)
result = await entity.run_agent(mock_context, request)
@@ -884,7 +884,7 @@ class TestRunRequestSupport:
mock_context.operation_name = "run_agent"
mock_context.get_input.return_value = {
"message": "Test message",
"conversation_id": "conv-789",
"thread_id": "conv-789",
"role": "user",
"enable_tool_calls": True,
"correlation_id": "corr-runreq-6",
@@ -1,34 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for data models (AgentSessionId, RunRequest, AgentResponse, ChatRole)."""
"""Unit tests for data models (AgentSessionId, RunRequest, AgentResponse)."""
import azure.durable_functions as df
import pytest
from agent_framework import Role
from pydantic import BaseModel
from agent_framework_azurefunctions._models import AgentResponse, AgentSessionId, ChatRole, RunRequest
from agent_framework_azurefunctions._models import AgentResponse, AgentSessionId, RunRequest
class ModuleStructuredResponse(BaseModel):
value: int
class TestChatRole:
"""Test suite for ChatRole enum."""
def test_chat_role_values(self) -> None:
"""Test that ChatRole has correct values."""
assert ChatRole.USER == "user"
assert ChatRole.SYSTEM == "system"
assert ChatRole.ASSISTANT == "assistant"
def test_chat_role_is_string(self) -> None:
"""Test that ChatRole values are strings."""
assert isinstance(ChatRole.USER.value, str)
assert isinstance(ChatRole.SYSTEM.value, str)
assert isinstance(ChatRole.ASSISTANT.value, str)
class TestAgentSessionId:
"""Test suite for AgentSessionId."""
@@ -164,49 +149,55 @@ class TestRunRequest:
def test_init_with_defaults(self) -> None:
"""Test RunRequest initialization with defaults."""
request = RunRequest(message="Hello", conversation_id="conv-default")
request = RunRequest(message="Hello", thread_id="thread-default")
assert request.message == "Hello"
assert request.role == ChatRole.USER
assert request.role == Role.USER
assert request.response_format is None
assert request.enable_tool_calls is True
assert request.conversation_id == "conv-default"
assert request.thread_id == "thread-default"
def test_init_with_all_fields(self) -> None:
"""Test RunRequest initialization with all fields."""
schema = ModuleStructuredResponse
request = RunRequest(
message="Hello",
conversation_id="conv-123",
role=ChatRole.SYSTEM,
thread_id="thread-123",
role=Role.SYSTEM,
response_format=schema,
enable_tool_calls=False,
)
assert request.message == "Hello"
assert request.role == ChatRole.SYSTEM
assert request.role == Role.SYSTEM
assert request.response_format is schema
assert request.enable_tool_calls is False
assert request.conversation_id == "conv-123"
assert request.thread_id == "thread-123"
def test_init_coerces_string_role(self) -> None:
"""Ensure string role values are coerced into Role instances."""
request = RunRequest(message="Hello", thread_id="thread-str-role", role="system") # type: ignore[arg-type]
assert request.role == Role.SYSTEM
def test_to_dict_with_defaults(self) -> None:
"""Test to_dict with default values."""
request = RunRequest(message="Test message", conversation_id="conv-to-dict")
request = RunRequest(message="Test message", thread_id="thread-to-dict")
data = request.to_dict()
assert data["message"] == "Test message"
assert data["enable_tool_calls"] is True
assert data["role"] == "user"
assert "response_format" not in data or data["response_format"] is None
assert data["conversation_id"] == "conv-to-dict"
assert data["thread_id"] == "thread-to-dict"
def test_to_dict_with_all_fields(self) -> None:
"""Test to_dict with all fields."""
schema = ModuleStructuredResponse
request = RunRequest(
message="Hello",
conversation_id="conv-456",
role=ChatRole.ASSISTANT,
thread_id="thread-456",
role=Role.ASSISTANT,
response_format=schema,
enable_tool_calls=False,
)
@@ -218,17 +209,17 @@ class TestRunRequest:
assert data["response_format"]["module"] == schema.__module__
assert data["response_format"]["qualname"] == schema.__qualname__
assert data["enable_tool_calls"] is False
assert data["conversation_id"] == "conv-456"
assert data["thread_id"] == "thread-456"
def test_from_dict_with_defaults(self) -> None:
"""Test from_dict with minimal data."""
data = {"message": "Hello", "conversation_id": "conv-from-dict"}
data = {"message": "Hello", "thread_id": "thread-from-dict"}
request = RunRequest.from_dict(data)
assert request.message == "Hello"
assert request.role == ChatRole.USER
assert request.role == Role.USER
assert request.enable_tool_calls is True
assert request.conversation_id == "conv-from-dict"
assert request.thread_id == "thread-from-dict"
def test_from_dict_with_all_fields(self) -> None:
"""Test from_dict with all fields."""
@@ -241,38 +232,39 @@ class TestRunRequest:
"qualname": ModuleStructuredResponse.__qualname__,
},
"enable_tool_calls": False,
"conversation_id": "conv-789",
"thread_id": "thread-789",
}
request = RunRequest.from_dict(data)
assert request.message == "Test"
assert request.role == ChatRole.SYSTEM
assert request.role == Role.SYSTEM
assert request.response_format is ModuleStructuredResponse
assert request.enable_tool_calls is False
assert request.conversation_id == "conv-789"
assert request.thread_id == "thread-789"
def test_from_dict_invalid_role_defaults_to_user(self) -> None:
"""Test from_dict with invalid role defaults to USER."""
data = {"message": "Test", "role": "invalid_role", "conversation_id": "conv-invalid-role"}
def test_from_dict_with_unknown_role_preserves_value(self) -> None:
"""Test from_dict keeps custom roles intact."""
data = {"message": "Test", "role": "reviewer", "thread_id": "thread-with-custom-role"}
request = RunRequest.from_dict(data)
assert request.role == ChatRole.USER
assert request.role.value == "reviewer"
assert request.role != Role.USER
def test_from_dict_empty_message(self) -> None:
"""Test from_dict with empty message."""
data = {"conversation_id": "conv-empty"}
data = {"thread_id": "thread-empty"}
request = RunRequest.from_dict(data)
assert request.message == ""
assert request.role == ChatRole.USER
assert request.conversation_id == "conv-empty"
assert request.role == Role.USER
assert request.thread_id == "thread-empty"
def test_round_trip_dict_conversion(self) -> None:
"""Test round-trip to_dict and from_dict."""
original = RunRequest(
message="Test message",
conversation_id="conv-123",
role=ChatRole.SYSTEM,
thread_id="thread-123",
role=Role.SYSTEM,
response_format=ModuleStructuredResponse,
enable_tool_calls=False,
)
@@ -284,13 +276,13 @@ class TestRunRequest:
assert restored.role == original.role
assert restored.response_format is ModuleStructuredResponse
assert restored.enable_tool_calls == original.enable_tool_calls
assert restored.conversation_id == original.conversation_id
assert restored.thread_id == original.thread_id
def test_round_trip_with_pydantic_response_format(self) -> None:
"""Ensure Pydantic response formats serialize and deserialize properly."""
original = RunRequest(
message="Structured",
conversation_id="conv-pydantic",
thread_id="thread-pydantic",
response_format=ModuleStructuredResponse,
)
@@ -305,14 +297,14 @@ class TestRunRequest:
def test_init_with_correlation_id(self) -> None:
"""Test RunRequest initialization with correlation_id."""
request = RunRequest(message="Test message", conversation_id="conv-corr-init", correlation_id="corr-123")
request = RunRequest(message="Test message", thread_id="thread-corr-init", correlation_id="corr-123")
assert request.message == "Test message"
assert request.correlation_id == "corr-123"
def test_to_dict_with_correlation_id(self) -> None:
"""Test to_dict includes correlation_id."""
request = RunRequest(message="Test", conversation_id="conv-corr-to-dict", correlation_id="corr-456")
request = RunRequest(message="Test", thread_id="thread-corr-to-dict", correlation_id="corr-456")
data = request.to_dict()
assert data["message"] == "Test"
@@ -320,19 +312,19 @@ class TestRunRequest:
def test_from_dict_with_correlation_id(self) -> None:
"""Test from_dict with correlation_id."""
data = {"message": "Test", "correlation_id": "corr-789", "conversation_id": "conv-corr-from-dict"}
data = {"message": "Test", "correlation_id": "corr-789", "thread_id": "thread-corr-from-dict"}
request = RunRequest.from_dict(data)
assert request.message == "Test"
assert request.correlation_id == "corr-789"
assert request.conversation_id == "conv-corr-from-dict"
assert request.thread_id == "thread-corr-from-dict"
def test_round_trip_with_correlation_id(self) -> None:
"""Test round-trip to_dict and from_dict with correlation_id."""
original = RunRequest(
message="Test message",
conversation_id="conv-123",
role=ChatRole.SYSTEM,
thread_id="thread-123",
role=Role.SYSTEM,
correlation_id="corr-123",
)
@@ -342,7 +334,7 @@ class TestRunRequest:
assert restored.message == original.message
assert restored.role == original.role
assert restored.correlation_id == original.correlation_id
assert restored.conversation_id == original.conversation_id
assert restored.thread_id == original.thread_id
class TestAgentResponse:
@@ -351,12 +343,12 @@ class TestAgentResponse:
def test_init_with_required_fields(self) -> None:
"""Test AgentResponse initialization with required fields."""
response = AgentResponse(
response="Test response", message="Test message", conversation_id="conv-123", status="success"
response="Test response", message="Test message", thread_id="thread-123", status="success"
)
assert response.response == "Test response"
assert response.message == "Test message"
assert response.conversation_id == "conv-123"
assert response.thread_id == "thread-123"
assert response.status == "success"
assert response.message_count == 0
assert response.error is None
@@ -369,7 +361,7 @@ class TestAgentResponse:
response = AgentResponse(
response=None,
message="What is the answer?",
conversation_id="conv-456",
thread_id="thread-456",
status="success",
message_count=5,
error=None,
@@ -384,13 +376,13 @@ class TestAgentResponse:
def test_to_dict_with_text_response(self) -> None:
"""Test to_dict with text response."""
response = AgentResponse(
response="Text response", message="Message", conversation_id="conv-1", status="success", message_count=3
response="Text response", message="Message", thread_id="thread-1", status="success", message_count=3
)
data = response.to_dict()
assert data["response"] == "Text response"
assert data["message"] == "Message"
assert data["conversation_id"] == "conv-1"
assert data["thread_id"] == "thread-1"
assert data["status"] == "success"
assert data["message_count"] == 3
assert "structured_response" not in data
@@ -403,7 +395,7 @@ class TestAgentResponse:
response = AgentResponse(
response=None,
message="Question",
conversation_id="conv-2",
thread_id="thread-2",
status="success",
structured_response=structured,
)
@@ -417,7 +409,7 @@ class TestAgentResponse:
response = AgentResponse(
response=None,
message="Failed message",
conversation_id="conv-3",
thread_id="thread-3",
status="error",
error="Something went wrong",
error_type="ValueError",
@@ -434,7 +426,7 @@ class TestAgentResponse:
response = AgentResponse(
response="Text response",
message="Message",
conversation_id="conv-4",
thread_id="thread-4",
status="success",
structured_response=structured,
)
@@ -452,26 +444,26 @@ class TestModelIntegration:
def test_run_request_with_session_id(self) -> None:
"""Test using RunRequest with AgentSessionId."""
session_id = AgentSessionId.with_random_key("AgentEntity")
request = RunRequest(message="Test message", conversation_id=str(session_id))
request = RunRequest(message="Test message", thread_id=str(session_id))
assert request.conversation_id is not None
assert request.conversation_id == str(session_id)
assert request.conversation_id.startswith("@AgentEntity@")
assert request.thread_id is not None
assert request.thread_id == str(session_id)
assert request.thread_id.startswith("@AgentEntity@")
def test_response_from_run_request(self) -> None:
"""Test creating AgentResponse from RunRequest."""
request = RunRequest(message="What is 2+2?", conversation_id="conv-123", role=ChatRole.USER)
request = RunRequest(message="What is 2+2?", thread_id="thread-123", role=Role.USER)
response = AgentResponse(
response="4",
message=request.message,
conversation_id=request.conversation_id,
thread_id=request.thread_id,
status="success",
message_count=1,
)
assert response.message == request.message
assert response.conversation_id == request.conversation_id
assert response.thread_id == request.thread_id
if __name__ == "__main__":
@@ -129,8 +129,8 @@ class TestDurableAIAgent:
assert request["enable_tool_calls"] is True
assert "correlation_id" in request
assert request["correlation_id"] == "correlation-guid"
assert "conversation_id" in request
assert request["conversation_id"] == "thread-guid"
assert "thread_id" in request
assert request["thread_id"] == "thread-guid"
def test_run_without_thread(self) -> None:
"""Test that run() works without explicit thread (creates unique session key)."""