Skip to content

Commit da6cfc2

Browse files
Add first-class agent identity tracking (OTel GenAI semantic conventions) (#3)
* Add first-class agent identity tracking using OTel GenAI semantic conventions Add gen_ai.agent.id, gen_ai.agent.name, gen_ai.agent.description, and gen_ai.agent.version attributes following the OpenTelemetry GenAI semantic conventions for agent spans. This enables tracking agent identity across multi-agent systems with automatic context propagation. - Add agent_context() context manager (same pattern as conversation_context) - Auto-propagate agent attributes via Last9SpanProcessor - Add create_agent/invoke_agent operation names - Add agent_tracking.py example - Update README and context_tracking example Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix black formatting in agent_tracking example Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add tests for agent_context: propagation, nesting, cleanup, multi-agent 12 new tests covering: - Basic agent_id propagation to spans - All fields (id, name, version) - Nested span propagation - Context cleanup after exit - Inner context overrides outer - Sequential multi-agent (routing pattern) - Nesting with conversation_context - Nesting with workflow_context - Triple nesting (conversation + agent + workflow) - Multi-agent handoff within same conversation - No-span edge case Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review: agent_description support, require agent_name, drop unused Operations - agent_context() now accepts agent_description (maps to gen_ai.agent.description) and wires it through the span processor, matching OpenAI Agents SDK and autogen-core emission. - agent_name is now required (first positional) and agent_id is optional. Per OTel GenAI semconv, gen_ai.agent.name is required and gen_ai.agent.id is recommended. - Drop unused Operations.CREATE_AGENT / INVOKE_AGENT. These were added but had no callers and will be re-added as part of the full OTel-semconv cleanup in ENG-896 (which also fixes CHAT_COMPLETIONS / TEXT_COMPLETION / TOOL_CALL). - Extract the processor's reserved-key list into a module-level frozenset so adding a new typed context var updates both branches in one place. - Restore _custom_attributes on agent_context exit (was dropped in the previous revision if **custom_attrs were passed). - Document coexistence with native-instrumented agent frameworks (AutoGen, OpenAI Agents SDK): those frameworks set gen_ai.agent.name directly on their own invoke_agent / create_agent spans inside the span body, which overrides our on_start injection. agent_context still tags sibling / child spans correctly. - Tests updated for new signature + agent_description propagation + custom_attrs restoration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address copilot feedback: drop unused fixture unpacks in agent tests github-code-quality bot flagged three agent tests that unpacked (tracer, memory_exporter) but only asserted on contextvars state. Replace with either `_` for the unused element or drop the unpack entirely (keeping the fixture parameter so Last9SpanProcessor setup still runs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9e8c0cc commit da6cfc2

8 files changed

Lines changed: 540 additions & 9 deletions

File tree

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
**Key Features:**
1717
- 🎯 **Conversation Tracking**: Automatic multi-turn conversation tracking with `conversation_context`
18+
- 🤖 **Agent Tracking**: First-class agent identity with `agent_context` (OTel `gen_ai.agent.*` semantic conventions)
1819
- 🔄 **Workflow Management**: Track complex multi-step AI workflows with `workflow_context`
1920
- 🎨 **Zero-Touch Instrumentation**: `@observe()` decorator for automatic tracking
2021
- 📊 **Context Propagation**: Thread-safe attribute tracking across nested operations
@@ -25,6 +26,7 @@
2526

2627
### Core Tracking
2728
- 🎯 **Conversation Tracking**: Multi-turn conversations with `gen_ai.conversation.id` and turn numbers
29+
- 🤖 **Agent Identity**: Track agents with `gen_ai.agent.id`, `gen_ai.agent.name`, `gen_ai.agent.version` (OTel semantic conventions)
2830
- 🔄 **Workflow Management**: Track multi-step AI operations across LLM calls, tools, and retrievals
2931
- 📊 **Auto-Context Propagation**: Thread-safe context managers that automatically tag all nested operations
3032
- 🎨 **Decorator Pattern**: `@observe()` for zero-touch instrumentation with full input/output/latency tracking
@@ -143,6 +145,31 @@ with conversation_context(conversation_id="support_123"):
143145
result = lookup_and_respond()
144146
```
145147

148+
### Track Agents
149+
150+
Track agent identity using [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/) (`gen_ai.agent.*`):
151+
152+
```python
153+
from last9_genai import agent_context
154+
155+
# Track agent identity — all child spans get gen_ai.agent.* attributes
156+
with agent_context(agent_id="support_bot_v2", agent_name="Support Bot", agent_version="2.0"):
157+
response = client.chat.completions.create(
158+
model="gpt-4o",
159+
messages=[{"role": "user", "content": "Help me with my order"}]
160+
)
161+
# Span automatically has gen_ai.agent.id, gen_ai.agent.name, gen_ai.agent.version
162+
163+
# Nest with conversations for full context
164+
with conversation_context(conversation_id="session_123", user_id="user_456"):
165+
with agent_context(agent_id="router_agent", agent_name="Router"):
166+
route = classify_intent(query)
167+
168+
with agent_context(agent_id="support_agent", agent_name="Support"):
169+
response = handle_support(query)
170+
# Each agent's spans are tagged separately, both share the conversation
171+
```
172+
146173
### Decorator Pattern (Zero-Touch)
147174

148175
Use `@observe()` for automatic tracking of everything:
@@ -527,6 +554,11 @@ workflow.llm_calls = 3
527554
# Conversation
528555
gen_ai.conversation.id = "session_123"
529556
gen_ai.conversation.turn_number = 2
557+
558+
# Agent (OTel GenAI semantic conventions)
559+
gen_ai.agent.id = "support_bot_v2"
560+
gen_ai.agent.name = "Support Bot"
561+
gen_ai.agent.version = "2.0"
530562
```
531563

532564
## Model Pricing
@@ -617,6 +649,7 @@ See [`examples/`](./examples/) directory:
617649

618650
**Advanced:**
619651
- [`conversation_tracking.py`](./examples/conversation_tracking.py) - Multi-turn conversations
652+
- [`agent_tracking.py`](./examples/agent_tracking.py) - Agent identity tracking with OTel semantic conventions
620653

621654
## Contributing
622655

examples/agent_tracking.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Agent identity tracking example
4+
5+
Demonstrates tracking agent identity using OTel GenAI semantic conventions
6+
(gen_ai.agent.id, gen_ai.agent.name, gen_ai.agent.version).
7+
8+
This is useful for multi-agent systems where you need to attribute spans
9+
to specific agents and correlate their interactions.
10+
"""
11+
12+
import sys
13+
import os
14+
15+
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
16+
17+
import time
18+
from opentelemetry import trace
19+
from opentelemetry.sdk.trace import TracerProvider
20+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
21+
from last9_genai import (
22+
Last9SpanProcessor,
23+
ModelPricing,
24+
agent_context,
25+
conversation_context,
26+
workflow_context,
27+
)
28+
29+
30+
def setup_tracing():
31+
"""Set up OpenTelemetry tracing with Last9 auto-enrichment"""
32+
provider = TracerProvider()
33+
trace.set_tracer_provider(provider)
34+
35+
console_exporter = ConsoleSpanExporter()
36+
provider.add_span_processor(BatchSpanProcessor(console_exporter))
37+
38+
custom_pricing = {
39+
"gpt-4o": ModelPricing(input=2.50, output=10.0),
40+
"gpt-4o-mini": ModelPricing(input=0.15, output=0.60),
41+
}
42+
l9_processor = Last9SpanProcessor(custom_pricing=custom_pricing)
43+
provider.add_span_processor(l9_processor)
44+
45+
return trace.get_tracer(__name__)
46+
47+
48+
def simulate_llm_call(tracer, model: str, prompt: str) -> dict:
49+
"""Simulate an LLM API call"""
50+
with tracer.start_span("gen_ai.chat.completions") as span:
51+
time.sleep(0.05)
52+
span.set_attribute("gen_ai.request.model", model)
53+
span.set_attribute("gen_ai.operation.name", "chat")
54+
span.set_attribute("gen_ai.usage.input_tokens", len(prompt.split()) * 2)
55+
span.set_attribute("gen_ai.usage.output_tokens", 50)
56+
return {"response": f"Response to: {prompt[:40]}..."}
57+
58+
59+
def single_agent_example():
60+
"""Basic agent context example"""
61+
tracer = setup_tracing()
62+
63+
print("\n--- Example 1: Single agent tracking ---\n")
64+
65+
with agent_context(agent_id="support_bot_v2", agent_name="Support Bot", agent_version="2.0"):
66+
result = simulate_llm_call(tracer, "gpt-4o", "Help me with my order")
67+
print(f" Response: {result['response']}")
68+
69+
print("\n Span attributes:")
70+
print(" gen_ai.agent.id = 'support_bot_v2'")
71+
print(" gen_ai.agent.name = 'Support Bot'")
72+
print(" gen_ai.agent.version = '2.0'")
73+
74+
75+
def multi_agent_routing_example():
76+
"""Multi-agent system with routing"""
77+
tracer = setup_tracing()
78+
79+
print("\n--- Example 2: Multi-agent routing ---\n")
80+
81+
with conversation_context(conversation_id="session_abc", user_id="user_42"):
82+
# Router agent classifies intent
83+
with agent_context(agent_id="router_v1", agent_name="Router Agent"):
84+
intent = simulate_llm_call(tracer, "gpt-4o-mini", "Classify: refund my order")
85+
print(f" Router: {intent['response']}")
86+
87+
# Specialist agent handles the request
88+
with agent_context(
89+
agent_id="refund_agent_v3", agent_name="Refund Agent", agent_version="3.1"
90+
):
91+
response = simulate_llm_call(tracer, "gpt-4o", "Process refund for order #12345")
92+
print(f" Refund Agent: {response['response']}")
93+
94+
print("\n Router spans: gen_ai.agent.id='router_v1', conversation_id='session_abc'")
95+
print(" Refund spans: gen_ai.agent.id='refund_agent_v3', conversation_id='session_abc'")
96+
97+
98+
def agent_with_workflow_example():
99+
"""Agent context nested with workflow context"""
100+
tracer = setup_tracing()
101+
102+
print("\n--- Example 3: Agent + workflow nesting ---\n")
103+
104+
with conversation_context(conversation_id="session_xyz"):
105+
with agent_context(agent_id="rag_agent", agent_name="RAG Agent", agent_version="1.0"):
106+
with workflow_context(workflow_id="retrieval_pipeline", workflow_type="rag"):
107+
simulate_llm_call(tracer, "gpt-4o-mini", "Expand query: best restaurants")
108+
simulate_llm_call(tracer, "gpt-4o", "Synthesize answer from documents")
109+
print(" RAG pipeline completed")
110+
111+
print("\n All spans have:")
112+
print(" gen_ai.conversation.id = 'session_xyz'")
113+
print(" gen_ai.agent.id = 'rag_agent'")
114+
print(" workflow.id = 'retrieval_pipeline'")
115+
116+
117+
if __name__ == "__main__":
118+
print("Last9 GenAI - Agent Identity Tracking (OTel Semantic Conventions)")
119+
print("=" * 70)
120+
121+
try:
122+
single_agent_example()
123+
multi_agent_routing_example()
124+
agent_with_workflow_example()
125+
126+
trace.get_tracer_provider().force_flush(timeout_millis=5000)
127+
128+
print("\n" + "=" * 70)
129+
print("All agent tracking examples completed!")
130+
print("\nAttributes follow OTel GenAI semantic conventions:")
131+
print(" gen_ai.agent.id - Unique agent identifier")
132+
print(" gen_ai.agent.name - Human-readable name")
133+
print(" gen_ai.agent.version - Agent version")
134+
print("\nSee: https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/")
135+
136+
except Exception as e:
137+
print(f"Error: {e}")
138+
import traceback
139+
140+
traceback.print_exc()

examples/context_tracking.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ModelPricing,
2424
conversation_context,
2525
workflow_context,
26+
agent_context,
2627
propagate_attributes,
2728
)
2829

@@ -202,11 +203,35 @@ def simulated_chat_endpoint(session_id: str, user_id: str, message: str):
202203
print(" - Zero manual attribute setting!")
203204

204205

206+
def agent_context_example():
207+
"""Agent identity tracking with OTel semantic conventions"""
208+
tracer = setup_tracing()
209+
210+
print("\n🔄 Example 5: Agent identity tracking\n")
211+
212+
with conversation_context(conversation_id="multi_agent_session", user_id="user_agent"):
213+
# Router agent
214+
with agent_context(agent_id="router_v1", agent_name="Router Agent"):
215+
simulate_llm_call(tracer, "gpt-3.5-turbo", "Classify user intent")
216+
print(" ✅ Router agent classified intent")
217+
218+
# Specialist agent
219+
with agent_context(agent_id="support_v2", agent_name="Support Agent", agent_version="2.0"):
220+
simulate_llm_call(tracer, "gpt-4o", "Handle support request")
221+
print(" ✅ Support agent handled request")
222+
223+
print("\n Agent spans automatically have:")
224+
print(" - gen_ai.agent.id (unique per agent)")
225+
print(" - gen_ai.agent.name (human-readable)")
226+
print(" - gen_ai.agent.version (when provided)")
227+
print(" - gen_ai.conversation.id (from parent context)")
228+
229+
205230
def multi_turn_conversation_example():
206231
"""Example with turn numbers"""
207232
tracer = setup_tracing()
208233

209-
print("\n🔄 Example 5: Multi-turn conversation with turn tracking\n")
234+
print("\n🔄 Example 6: Multi-turn conversation with turn tracking\n")
210235

211236
conversation_id = "multi_turn_session"
212237
messages = ["Hello!", "What's the weather?", "Thank you!"]
@@ -236,6 +261,7 @@ def multi_turn_conversation_example():
236261
nested_workflow_example()
237262
propagate_attributes_example()
238263
fastapi_pattern_example()
264+
agent_context_example()
239265
multi_turn_conversation_example()
240266

241267
# Force export of spans

last9_genai/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
propagate_attributes,
6060
conversation_context,
6161
workflow_context,
62+
agent_context,
6263
get_current_context,
6364
clear_context,
6465
)
@@ -96,6 +97,7 @@
9697
"propagate_attributes",
9798
"conversation_context",
9899
"workflow_context",
100+
"agent_context",
99101
"get_current_context",
100102
"clear_context",
101103
# Span processor

0 commit comments

Comments
 (0)