Skip to content

Commit a38e4b2

Browse files
committed
add integration test for agent framework
1 parent 42624c3 commit a38e4b2

3 files changed

Lines changed: 309 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dev-dependencies = [
3838
"ruff>=0.1.0",
3939
"python-dotenv>=1.0.0",
4040
"openai>=1.0.0",
41+
"agent-framework-azure-ai >= 0.1.0",
4142
"azure-identity>=1.12.0",
4243
"openai-agents >= 0.2.6",
4344
]
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
import time
5+
6+
import pytest
7+
from microsoft_agents_a365.observability.core import configure, get_tracer_provider
8+
from microsoft_agents_a365.observability.core.constants import (
9+
GEN_AI_AGENT_ID_KEY,
10+
GEN_AI_INPUT_MESSAGES_KEY,
11+
GEN_AI_OUTPUT_MESSAGES_KEY,
12+
GEN_AI_REQUEST_MODEL_KEY,
13+
GEN_AI_SYSTEM_KEY,
14+
TENANT_ID_KEY,
15+
)
16+
from microsoft_agents_a365.observability.extensions.agentframework.trace_instrumentor import (
17+
AgentFrameworkInstrumentor,
18+
)
19+
20+
# AgentFramework SDK
21+
try:
22+
from agent_framework.azure import AzureOpenAIChatClient
23+
from agent_framework import ChatAgent, ai_function
24+
from azure.identity import AzureCliCredential
25+
from agent_framework.observability import setup_observability
26+
except ImportError:
27+
pytest.skip(
28+
"AgentFramework library and dependencies required for integration tests",
29+
allow_module_level=True,
30+
)
31+
32+
33+
@ai_function
34+
def add_numbers(a: float, b: float) -> float:
35+
"""Add two numbers together.
36+
37+
Args:
38+
a: First number
39+
b: Second number
40+
41+
Returns:
42+
The sum of a and b
43+
"""
44+
return a + b
45+
46+
47+
@pytest.mark.integration
48+
class TestAgentFrameworkTraceProcessorIntegration:
49+
"""Integration tests for AgentFramework trace processor with real Azure OpenAI."""
50+
51+
def setup_method(self):
52+
"""Set up test method with mock exporter."""
53+
self.captured_spans = []
54+
self.mock_exporter = MockAgent365Exporter(self.captured_spans)
55+
56+
def test_agentframework_trace_processor_integration(self, azure_openai_config, agent365_config):
57+
"""Test AgentFramework trace processor with real Azure OpenAI call."""
58+
59+
60+
# Configure observability
61+
configure(
62+
service_name="integration-test-service",
63+
service_namespace="agent365-tests",
64+
logger_name="test-logger",
65+
)
66+
67+
# Get the tracer provider and add our mock exporter
68+
provider = get_tracer_provider()
69+
provider.add_span_processor(self.mock_exporter)
70+
71+
setup_observability()
72+
73+
# Initialize the instrumentor
74+
instrumentor = AgentFrameworkInstrumentor()
75+
instrumentor.instrument()
76+
77+
try:
78+
# Create Azure OpenAI ChatClient
79+
chat_client = AzureOpenAIChatClient(
80+
endpoint=azure_openai_config["endpoint"],
81+
credential=AzureCliCredential(),
82+
deployment_name=azure_openai_config["deployment"],
83+
api_version=azure_openai_config["api_version"],
84+
)
85+
86+
# Create agent framework agent
87+
agent = ChatAgent(
88+
chat_client=chat_client,
89+
instructions="You are a helpful assistant.",
90+
tools=[],
91+
)
92+
93+
# Execute a simple prompt using async runner
94+
import asyncio
95+
96+
async def run_agent():
97+
result = await agent.run("What can you do with agent framework?")
98+
return result
99+
100+
asyncio.run(setup_observability())
101+
response = asyncio.run(run_agent())
102+
103+
# Give some time for spans to be processed
104+
time.sleep(1)
105+
106+
# Verify that spans were captured
107+
assert len(self.captured_spans) > 0, "No spans were captured"
108+
109+
# Verify we have the expected span types
110+
span_names = [span.name for span in self.captured_spans]
111+
print(f"Captured spans: {span_names}")
112+
113+
# Validate attributes on spans
114+
self._validate_span_attributes(agent365_config)
115+
116+
# Verify the response content
117+
assert response is not None
118+
assert len(response) > 0
119+
print(f"Agent response: {response}")
120+
121+
finally:
122+
# Clean up
123+
instrumentor.uninstrument()
124+
125+
def test_agentframework_trace_processor_with_tool_calls(self, azure_openai_config, agent365_config):
126+
"""Test AgentFramework trace processor with tool calls."""
127+
128+
# Configure observability
129+
configure(
130+
service_name="integration-test-service-tools",
131+
service_namespace="agent365-tests",
132+
logger_name="test-logger",
133+
)
134+
135+
# Get the tracer provider and add our mock exporter
136+
provider = get_tracer_provider()
137+
provider.add_span_processor(self.mock_exporter)
138+
139+
setup_observability()
140+
141+
# Initialize the instrumentor
142+
instrumentor = AgentFrameworkInstrumentor()
143+
instrumentor.instrument()
144+
145+
try:
146+
# Create Azure OpenAI ChatClient
147+
chat_client = AzureOpenAIChatClient(
148+
endpoint=azure_openai_config["endpoint"],
149+
credential=AzureCliCredential(),
150+
deployment_name=azure_openai_config["deployment"],
151+
api_version=azure_openai_config["api_version"],
152+
)
153+
154+
# Create agent framework agent
155+
agent = ChatAgent(
156+
chat_client=chat_client,
157+
instructions="You are a helpful agent framework assistant.",
158+
tools=[add_numbers],
159+
)
160+
161+
# Execute a prompt that requires tool usage
162+
import asyncio
163+
164+
165+
async def run_agent_with_tool():
166+
result = await agent.run("What is 15 + 27?")
167+
return result
168+
169+
response = asyncio.run(run_agent_with_tool())
170+
171+
# Give some time for spans to be processed
172+
time.sleep(1)
173+
174+
# Verify that spans were captured
175+
assert len(self.captured_spans) > 0, "No spans were captured"
176+
177+
# Verify we have the expected span types
178+
span_names = [span.name for span in self.captured_spans]
179+
print(f"Captured spans with tools: {span_names}")
180+
181+
# Validate attributes on spans including tool calls
182+
self._validate_tool_span_attributes(agent365_config)
183+
184+
# Verify the response content includes the calculation result
185+
assert response is not None
186+
assert len(response) > 0
187+
assert "42" in response # 15 + 27 = 42
188+
print(f"Agent response with tool: {response}")
189+
190+
finally:
191+
# Clean up
192+
instrumentor.uninstrument()
193+
194+
def _validate_span_attributes(self, agent365_config):
195+
"""Validate that spans have the expected attributes."""
196+
llm_spans_found = 0
197+
agent_spans_found = 0
198+
199+
for span in self.captured_spans:
200+
attributes = dict(span.attributes or {})
201+
print(f"Span '{span.name}' attributes: {list(attributes.keys())}")
202+
203+
# Check common attributes
204+
if TENANT_ID_KEY in attributes:
205+
assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"]
206+
207+
if GEN_AI_AGENT_ID_KEY in attributes:
208+
assert attributes[GEN_AI_AGENT_ID_KEY] == agent365_config["agent_id"]
209+
210+
# Check for LLM spans (generation spans)
211+
if GEN_AI_SYSTEM_KEY in attributes and attributes[GEN_AI_SYSTEM_KEY] == "openai":
212+
if GEN_AI_REQUEST_MODEL_KEY in attributes:
213+
llm_spans_found += 1
214+
# Validate LLM span attributes
215+
assert GEN_AI_REQUEST_MODEL_KEY in attributes
216+
assert attributes[GEN_AI_REQUEST_MODEL_KEY] is not None
217+
print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}")
218+
219+
# Check for input/output messages
220+
if GEN_AI_INPUT_MESSAGES_KEY in attributes:
221+
input_messages = attributes[GEN_AI_INPUT_MESSAGES_KEY]
222+
assert input_messages is not None
223+
print(f"✓ Input messages found: {input_messages[:100]}...")
224+
225+
if GEN_AI_OUTPUT_MESSAGES_KEY in attributes:
226+
output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY]
227+
assert output_messages is not None
228+
print(f"✓ Output messages found: {output_messages[:100]}...")
229+
230+
# Check for agent spans
231+
if "agent" in span.name.lower():
232+
agent_spans_found += 1
233+
print(f"✓ Found agent span: {span.name}")
234+
235+
# Ensure we found at least some spans with telemetry data
236+
assert len(self.captured_spans) > 0, "No spans were captured"
237+
print(f"✓ Captured {len(self.captured_spans)} spans total")
238+
print(f"✓ Found {llm_spans_found} LLM spans and {agent_spans_found} agent spans")
239+
240+
def _validate_tool_span_attributes(self, agent365_config):
241+
"""Validate that spans have the expected attributes including tool calls."""
242+
llm_spans_found = 0
243+
agent_spans_found = 0
244+
tool_spans_found = 0
245+
246+
for span in self.captured_spans:
247+
attributes = dict(span.attributes or {})
248+
print(f"Span '{span.name}' attributes: {list(attributes.keys())}")
249+
250+
# Check common attributes
251+
if TENANT_ID_KEY in attributes:
252+
assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"]
253+
254+
if GEN_AI_AGENT_ID_KEY in attributes:
255+
assert attributes[GEN_AI_AGENT_ID_KEY] == agent365_config["agent_id"]
256+
257+
# Check for LLM spans (generation spans)
258+
if GEN_AI_SYSTEM_KEY in attributes and attributes[GEN_AI_SYSTEM_KEY] == "openai":
259+
if GEN_AI_REQUEST_MODEL_KEY in attributes:
260+
llm_spans_found += 1
261+
print(f"✓ Found LLM span with model: {attributes[GEN_AI_REQUEST_MODEL_KEY]}")
262+
263+
# Check for tool calls in messages
264+
if GEN_AI_OUTPUT_MESSAGES_KEY in attributes:
265+
output_messages = attributes[GEN_AI_OUTPUT_MESSAGES_KEY]
266+
if "tool_calls" in output_messages:
267+
print("✓ Found tool calls in LLM output messages")
268+
269+
# Check for agent spans
270+
if "agent" in span.name.lower():
271+
agent_spans_found += 1
272+
print(f"✓ Found agent span: {span.name}")
273+
274+
# Check for tool execution spans
275+
if "execute_tool" in span.name.lower() or "calculator_tool" in span.name.lower():
276+
tool_spans_found += 1
277+
print(f"✓ Found tool execution span: {span.name}")
278+
279+
# Ensure we found the expected span types
280+
assert len(self.captured_spans) > 0, "No spans were captured"
281+
print(f"✓ Captured {len(self.captured_spans)} spans total")
282+
print(
283+
f"✓ Found {llm_spans_found} LLM spans, {agent_spans_found} agent spans, and {tool_spans_found} tool spans"
284+
)
285+
286+
287+
class MockAgent365Exporter:
288+
"""Mock span processor that captures spans instead of sending them."""
289+
290+
def __init__(self, captured_spans):
291+
self.captured_spans = captured_spans
292+
293+
def on_start(self, span, parent_context=None):
294+
"""Called when a span starts."""
295+
pass
296+
297+
def on_end(self, span):
298+
"""Called when a span ends."""
299+
self.captured_spans.append(span)
300+
301+
def shutdown(self):
302+
"""Mock shutdown."""
303+
pass
304+
305+
def force_flush(self, timeout_millis: int = 30000) -> bool:
306+
"""Mock force flush."""
307+
return True

uv.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)