|
8 | 8 | from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter |
9 | 9 |
|
10 | 10 | from last9_genai import ( |
| 11 | + agent_context, |
11 | 12 | conversation_context, |
12 | 13 | workflow_context, |
13 | 14 | propagate_attributes, |
@@ -249,6 +250,176 @@ def test_deeply_nested_contexts(self, tracer_setup): |
249 | 250 | assert spans[0].attributes["workflow.id"] == "wf_level_1" |
250 | 251 |
|
251 | 252 |
|
| 253 | +class TestAgentContext: |
| 254 | + """Test agent_context() context manager""" |
| 255 | + |
| 256 | + def test_agent_context_basic(self, tracer_setup): |
| 257 | + """Test basic agent_context with just agent_id""" |
| 258 | + tracer, memory_exporter = tracer_setup |
| 259 | + |
| 260 | + with agent_context(agent_id="agent_123"): |
| 261 | + with tracer.start_as_current_span("test_span"): |
| 262 | + pass |
| 263 | + |
| 264 | + spans = memory_exporter.get_finished_spans() |
| 265 | + assert len(spans) == 1 |
| 266 | + assert spans[0].attributes[GenAIAttributes.AGENT_ID] == "agent_123" |
| 267 | + |
| 268 | + def test_agent_context_with_all_fields(self, tracer_setup): |
| 269 | + """Test agent_context with id, name, and version""" |
| 270 | + tracer, memory_exporter = tracer_setup |
| 271 | + |
| 272 | + with agent_context(agent_id="bot_v2", agent_name="Support Bot", agent_version="2.0"): |
| 273 | + with tracer.start_as_current_span("test_span"): |
| 274 | + pass |
| 275 | + |
| 276 | + spans = memory_exporter.get_finished_spans() |
| 277 | + assert len(spans) == 1 |
| 278 | + assert spans[0].attributes[GenAIAttributes.AGENT_ID] == "bot_v2" |
| 279 | + assert spans[0].attributes[GenAIAttributes.AGENT_NAME] == "Support Bot" |
| 280 | + assert spans[0].attributes[GenAIAttributes.AGENT_VERSION] == "2.0" |
| 281 | + |
| 282 | + def test_agent_context_propagates_to_nested_spans(self, tracer_setup): |
| 283 | + """Test that agent context propagates to all nested spans""" |
| 284 | + tracer, memory_exporter = tracer_setup |
| 285 | + |
| 286 | + with agent_context(agent_id="nested_agent", agent_name="Nested"): |
| 287 | + with tracer.start_as_current_span("parent"): |
| 288 | + with tracer.start_as_current_span("child"): |
| 289 | + pass |
| 290 | + |
| 291 | + spans = memory_exporter.get_finished_spans() |
| 292 | + assert len(spans) == 2 |
| 293 | + assert spans[0].attributes[GenAIAttributes.AGENT_ID] == "nested_agent" |
| 294 | + assert spans[1].attributes[GenAIAttributes.AGENT_ID] == "nested_agent" |
| 295 | + |
| 296 | + def test_agent_context_cleanup(self, tracer_setup): |
| 297 | + """Test that agent context is cleaned up after exit""" |
| 298 | + tracer, memory_exporter = tracer_setup |
| 299 | + |
| 300 | + with agent_context(agent_id="temp_agent"): |
| 301 | + context = get_current_context() |
| 302 | + assert context["agent_id"] == "temp_agent" |
| 303 | + |
| 304 | + context = get_current_context() |
| 305 | + assert "agent_id" not in context or context.get("agent_id") != "temp_agent" |
| 306 | + |
| 307 | + def test_agent_context_override(self, tracer_setup): |
| 308 | + """Test that inner agent context overrides outer""" |
| 309 | + tracer, memory_exporter = tracer_setup |
| 310 | + |
| 311 | + with agent_context(agent_id="outer_agent", agent_name="Outer"): |
| 312 | + with agent_context(agent_id="inner_agent", agent_name="Inner"): |
| 313 | + with tracer.start_as_current_span("test_span"): |
| 314 | + pass |
| 315 | + |
| 316 | + spans = memory_exporter.get_finished_spans() |
| 317 | + assert len(spans) == 1 |
| 318 | + assert spans[0].attributes[GenAIAttributes.AGENT_ID] == "inner_agent" |
| 319 | + assert spans[0].attributes[GenAIAttributes.AGENT_NAME] == "Inner" |
| 320 | + |
| 321 | + def test_multi_agent_sequential(self, tracer_setup): |
| 322 | + """Test sequential agent contexts (multi-agent routing)""" |
| 323 | + tracer, memory_exporter = tracer_setup |
| 324 | + |
| 325 | + with agent_context(agent_id="router", agent_name="Router"): |
| 326 | + with tracer.start_as_current_span("route"): |
| 327 | + pass |
| 328 | + |
| 329 | + with agent_context(agent_id="handler", agent_name="Handler"): |
| 330 | + with tracer.start_as_current_span("handle"): |
| 331 | + pass |
| 332 | + |
| 333 | + spans = memory_exporter.get_finished_spans() |
| 334 | + assert len(spans) == 2 |
| 335 | + assert spans[0].attributes[GenAIAttributes.AGENT_ID] == "router" |
| 336 | + assert spans[1].attributes[GenAIAttributes.AGENT_ID] == "handler" |
| 337 | + |
| 338 | + def test_agent_with_conversation_context(self, tracer_setup): |
| 339 | + """Test agent nested inside conversation""" |
| 340 | + tracer, memory_exporter = tracer_setup |
| 341 | + |
| 342 | + with conversation_context(conversation_id="conv_abc", user_id="user_1"): |
| 343 | + with agent_context(agent_id="agent_xyz", agent_name="Bot"): |
| 344 | + with tracer.start_as_current_span("test_span"): |
| 345 | + pass |
| 346 | + |
| 347 | + spans = memory_exporter.get_finished_spans() |
| 348 | + assert len(spans) == 1 |
| 349 | + assert spans[0].attributes[GenAIAttributes.CONVERSATION_ID] == "conv_abc" |
| 350 | + assert spans[0].attributes["user.id"] == "user_1" |
| 351 | + assert spans[0].attributes[GenAIAttributes.AGENT_ID] == "agent_xyz" |
| 352 | + assert spans[0].attributes[GenAIAttributes.AGENT_NAME] == "Bot" |
| 353 | + |
| 354 | + def test_agent_with_workflow_context(self, tracer_setup): |
| 355 | + """Test agent nested with workflow""" |
| 356 | + tracer, memory_exporter = tracer_setup |
| 357 | + |
| 358 | + with agent_context(agent_id="rag_agent", agent_name="RAG"): |
| 359 | + with workflow_context(workflow_id="retrieval", workflow_type="rag"): |
| 360 | + with tracer.start_as_current_span("test_span"): |
| 361 | + pass |
| 362 | + |
| 363 | + spans = memory_exporter.get_finished_spans() |
| 364 | + assert len(spans) == 1 |
| 365 | + assert spans[0].attributes[GenAIAttributes.AGENT_ID] == "rag_agent" |
| 366 | + assert spans[0].attributes["workflow.id"] == "retrieval" |
| 367 | + assert spans[0].attributes["workflow.type"] == "rag" |
| 368 | + |
| 369 | + def test_agent_conversation_workflow_triple_nesting(self, tracer_setup): |
| 370 | + """Test all three contexts nested together""" |
| 371 | + tracer, memory_exporter = tracer_setup |
| 372 | + |
| 373 | + with conversation_context(conversation_id="session_1"): |
| 374 | + with agent_context(agent_id="agent_1", agent_name="Agent", agent_version="1.0"): |
| 375 | + with workflow_context(workflow_id="wf_1"): |
| 376 | + with tracer.start_as_current_span("test_span"): |
| 377 | + pass |
| 378 | + |
| 379 | + spans = memory_exporter.get_finished_spans() |
| 380 | + assert len(spans) == 1 |
| 381 | + assert spans[0].attributes[GenAIAttributes.CONVERSATION_ID] == "session_1" |
| 382 | + assert spans[0].attributes[GenAIAttributes.AGENT_ID] == "agent_1" |
| 383 | + assert spans[0].attributes[GenAIAttributes.AGENT_NAME] == "Agent" |
| 384 | + assert spans[0].attributes[GenAIAttributes.AGENT_VERSION] == "1.0" |
| 385 | + assert spans[0].attributes["workflow.id"] == "wf_1" |
| 386 | + |
| 387 | + def test_multi_agent_in_conversation(self, tracer_setup): |
| 388 | + """Test multiple agents within same conversation (handoff pattern)""" |
| 389 | + tracer, memory_exporter = tracer_setup |
| 390 | + |
| 391 | + with conversation_context(conversation_id="session_handoff"): |
| 392 | + with agent_context(agent_id="router_v1", agent_name="Router"): |
| 393 | + with tracer.start_as_current_span("classify"): |
| 394 | + pass |
| 395 | + |
| 396 | + with agent_context(agent_id="support_v2", agent_name="Support"): |
| 397 | + with tracer.start_as_current_span("respond"): |
| 398 | + pass |
| 399 | + |
| 400 | + spans = memory_exporter.get_finished_spans() |
| 401 | + assert len(spans) == 2 |
| 402 | + |
| 403 | + # Both have same conversation, different agents |
| 404 | + assert spans[0].attributes[GenAIAttributes.CONVERSATION_ID] == "session_handoff" |
| 405 | + assert spans[0].attributes[GenAIAttributes.AGENT_ID] == "router_v1" |
| 406 | + assert spans[0].attributes[GenAIAttributes.AGENT_NAME] == "Router" |
| 407 | + |
| 408 | + assert spans[1].attributes[GenAIAttributes.CONVERSATION_ID] == "session_handoff" |
| 409 | + assert spans[1].attributes[GenAIAttributes.AGENT_ID] == "support_v2" |
| 410 | + assert spans[1].attributes[GenAIAttributes.AGENT_NAME] == "Support" |
| 411 | + |
| 412 | + def test_agent_context_no_span(self, tracer_setup): |
| 413 | + """Test agent_context works even without spans""" |
| 414 | + tracer, memory_exporter = tracer_setup |
| 415 | + |
| 416 | + with agent_context(agent_id="no_span_agent"): |
| 417 | + pass |
| 418 | + |
| 419 | + spans = memory_exporter.get_finished_spans() |
| 420 | + assert len(spans) == 0 |
| 421 | + |
| 422 | + |
252 | 423 | class TestPropagateAttributes: |
253 | 424 | """Test propagate_attributes() context manager""" |
254 | 425 |
|
|
0 commit comments