|
7 | 7 | from microsoft_agents_a365.observability.core import configure, get_tracer_provider |
8 | 8 | from microsoft_agents_a365.observability.core.constants import ( |
9 | 9 | GEN_AI_AGENT_ID_KEY, |
| 10 | + GEN_AI_AGENT_NAME_KEY, |
| 11 | + GEN_AI_EXECUTION_TYPE_KEY, |
10 | 12 | GEN_AI_INPUT_MESSAGES_KEY, |
| 13 | + GEN_AI_OPERATION_NAME_KEY, |
11 | 14 | GEN_AI_OUTPUT_MESSAGES_KEY, |
12 | 15 | GEN_AI_REQUEST_MODEL_KEY, |
13 | 16 | GEN_AI_SYSTEM_KEY, |
| 17 | + INVOKE_AGENT_OPERATION_NAME, |
14 | 18 | TENANT_ID_KEY, |
15 | 19 | ) |
| 20 | +from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder |
16 | 21 | from microsoft_agents_a365.observability.extensions.openai.trace_instrumentor import ( |
17 | 22 | OpenAIAgentsTraceInstrumentor, |
18 | 23 | ) |
@@ -184,6 +189,122 @@ async def run_agent_with_tool(): |
184 | 189 | # Clean up |
185 | 190 | instrumentor.uninstrument() |
186 | 191 |
|
| 192 | + def test_invoke_agent_span_required_attributes(self, azure_openai_config, agent365_config): |
| 193 | + """Test that invoke_agent span has all required attributes per schema.""" |
| 194 | + |
| 195 | + # Configure observability |
| 196 | + configure( |
| 197 | + service_name="integration-test-invoke-agent", |
| 198 | + service_namespace="agent365-tests", |
| 199 | + logger_name="test-logger", |
| 200 | + ) |
| 201 | + |
| 202 | + # Get the tracer provider and add our mock exporter |
| 203 | + provider = get_tracer_provider() |
| 204 | + provider.add_span_processor(self.mock_exporter) |
| 205 | + |
| 206 | + # Initialize the instrumentor |
| 207 | + instrumentor = OpenAIAgentsTraceInstrumentor() |
| 208 | + instrumentor.instrument() |
| 209 | + |
| 210 | + try: |
| 211 | + # Create Azure OpenAI client |
| 212 | + openai_client = AsyncAzureOpenAI( |
| 213 | + api_key=azure_openai_config["api_key"], |
| 214 | + api_version=azure_openai_config["api_version"], |
| 215 | + azure_endpoint=azure_openai_config["endpoint"], |
| 216 | + ) |
| 217 | + |
| 218 | + # Create agent |
| 219 | + agent = Agent( |
| 220 | + name="TestAgent", |
| 221 | + instructions="You are a helpful assistant. Answer briefly.", |
| 222 | + model=OpenAIChatCompletionsModel( |
| 223 | + model=azure_openai_config["deployment"], openai_client=openai_client |
| 224 | + ), |
| 225 | + ) |
| 226 | + |
| 227 | + # Execute agent wrapped with BaggageBuilder to provide required attributes |
| 228 | + import asyncio |
| 229 | + |
| 230 | + async def run_agent(): |
| 231 | + with ( |
| 232 | + BaggageBuilder() |
| 233 | + .agent_id("test-agent-id") |
| 234 | + .agent_name("TestAgent") |
| 235 | + .agent_auid("test-agent-auid") |
| 236 | + .agent_upn("test-agent@test.com") |
| 237 | + .agent_blueprint_id("test-blueprint-id") |
| 238 | + .tenant_id("test-tenant-id") |
| 239 | + .caller_id("test-caller-id") |
| 240 | + .caller_name("Test Caller") |
| 241 | + .caller_upn("test-caller@test.com") |
| 242 | + .caller_client_ip("127.0.0.1") |
| 243 | + .conversation_id("test-conversation-id") |
| 244 | + .channel_name("test-channel") |
| 245 | + .correlation_id("test-correlation-id") |
| 246 | + .build() |
| 247 | + ): |
| 248 | + result = await Runner.run(agent, "Say hello") |
| 249 | + return result.final_output |
| 250 | + |
| 251 | + response = asyncio.run(run_agent()) |
| 252 | + |
| 253 | + # Give time for spans to be processed |
| 254 | + time.sleep(1) |
| 255 | + |
| 256 | + # Find the invoke_agent span |
| 257 | + invoke_agent_span = None |
| 258 | + for span in self.captured_spans: |
| 259 | + if span.name.startswith(INVOKE_AGENT_OPERATION_NAME): |
| 260 | + invoke_agent_span = span |
| 261 | + break |
| 262 | + |
| 263 | + assert invoke_agent_span is not None, "invoke_agent span not found" |
| 264 | + attributes = dict(invoke_agent_span.attributes or {}) |
| 265 | + |
| 266 | + print(f"invoke_agent span attributes: {list(attributes.keys())}") |
| 267 | + |
| 268 | + # Validate REQUIRED attributes (must be present) |
| 269 | + required_attributes = [ |
| 270 | + GEN_AI_OPERATION_NAME_KEY, # "gen_ai.operation.name" - Set by SDK |
| 271 | + GEN_AI_AGENT_ID_KEY, # "gen_ai.agent.id" |
| 272 | + GEN_AI_AGENT_NAME_KEY, # "gen_ai.agent.name" |
| 273 | + GEN_AI_EXECUTION_TYPE_KEY, # "gen_ai.execution.type" |
| 274 | + GEN_AI_INPUT_MESSAGES_KEY, # "gen_ai.input.messages" |
| 275 | + GEN_AI_OUTPUT_MESSAGES_KEY, # "gen_ai.output.messages" |
| 276 | + ] |
| 277 | + |
| 278 | + missing_required = [] |
| 279 | + for attr in required_attributes: |
| 280 | + if attr not in attributes: |
| 281 | + missing_required.append(attr) |
| 282 | + else: |
| 283 | + print(f"✓ Required attribute present: {attr} = {str(attributes[attr])[:50]}...") |
| 284 | + |
| 285 | + assert len(missing_required) == 0, f"Missing required attributes: {missing_required}" |
| 286 | + |
| 287 | + # Validate operation name value |
| 288 | + assert attributes[GEN_AI_OPERATION_NAME_KEY] == INVOKE_AGENT_OPERATION_NAME, ( |
| 289 | + f"Expected operation name '{INVOKE_AGENT_OPERATION_NAME}', " |
| 290 | + f"got '{attributes[GEN_AI_OPERATION_NAME_KEY]}'" |
| 291 | + ) |
| 292 | + |
| 293 | + # Validate agent name matches |
| 294 | + assert attributes[GEN_AI_AGENT_NAME_KEY] == "TestAgent", ( |
| 295 | + f"Expected agent name 'TestAgent', got '{attributes[GEN_AI_AGENT_NAME_KEY]}'" |
| 296 | + ) |
| 297 | + |
| 298 | + # Validate input/output messages are non-empty |
| 299 | + assert attributes[GEN_AI_INPUT_MESSAGES_KEY], "Input messages should not be empty" |
| 300 | + assert attributes[GEN_AI_OUTPUT_MESSAGES_KEY], "Output messages should not be empty" |
| 301 | + |
| 302 | + print("✓ All required invoke_agent span attributes validated") |
| 303 | + print(f"Agent response: {response}") |
| 304 | + |
| 305 | + finally: |
| 306 | + instrumentor.uninstrument() |
| 307 | + |
187 | 308 | def _validate_span_attributes(self, agent365_config): |
188 | 309 | """Validate that spans have the expected attributes.""" |
189 | 310 | llm_spans_found = 0 |
|
0 commit comments