Skip to content

Commit f456c3a

Browse files
committed
feat(examples): add LlamaIndex zero-code OTLP example
Uses official LlamaIndexOpenTelemetry integration. Adds e2e tests.
1 parent 43bc581 commit f456c3a

4 files changed

Lines changed: 174 additions & 0 deletions

File tree

examples/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ agentevals accepts OTLP/HTTP on port 4318 (`http/protobuf` and `http/json`) and
3030
| [zero-code-examples/strands/](./zero-code-examples/strands/) | Strands | OpenAI |
3131
| [zero-code-examples/adk/](./zero-code-examples/adk/) | Google ADK | Gemini |
3232
| [zero-code-examples/pydantic-ai/](./zero-code-examples/pydantic-ai/) | Pydantic AI | OpenAI |
33+
| [zero-code-examples/llama-index/](./zero-code-examples/llama-index/) | LlamaIndex | OpenAI |
3334

3435
This approach works with any framework that has OTel instrumentation: LangChain, Strands, Google ADK, etc. If your framework already emits OTel spans, you only need to add `OTLPSpanExporter` (and `OTLPLogExporter` if it uses GenAI log-based content delivery).
3536

@@ -105,6 +106,7 @@ Detection checks for `gen_ai.request.model` / `gen_ai.input.messages` (GenAI sem
105106
| [zero-code-examples/strands/](./zero-code-examples/strands/) | Strands | OpenAI | GenAI semconv (events*) | Standard OTLP export |
106107
| [zero-code-examples/adk/](./zero-code-examples/adk/) | Google ADK | Gemini | ADK built-in | Standard OTLP export |
107108
| [zero-code-examples/pydantic-ai/](./zero-code-examples/pydantic-ai/) | Pydantic AI | OpenAI | GenAI semconv (span attrs) | Standard OTLP export |
109+
| [zero-code-examples/llama-index/](./zero-code-examples/llama-index/) | LlamaIndex | OpenAI | GenAI semconv (logs) | Standard OTLP export |
108110
| [langchain_agent](./langchain_agent/) | LangChain | OpenAI | GenAI semconv (logs) | SDK WebSocket |
109111
| [strands_agent](./strands_agent/) | Strands | OpenAI | GenAI semconv (events*) | SDK WebSocket |
110112
| [dice_agent](./dice_agent/) | Google ADK | Gemini | ADK built-in | SDK WebSocket |
@@ -226,6 +228,7 @@ python examples/zero-code-examples/ollama/run.py
226228
python examples/zero-code-examples/strands/run.py
227229
python examples/zero-code-examples/adk/run.py
228230
python examples/zero-code-examples/pydantic-ai/run.py
231+
python examples/zero-code-examples/llama-index/run.py
229232

230233
# SDK examples:
231234
python examples/sdk_example/context_manager_example.py
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
llama-index-core>=0.14.0
2+
llama-index-llms-openai-like>=0.3.0
3+
llama-index-observability-otel>=0.1.0
4+
5+
opentelemetry-sdk>=1.36.0
6+
opentelemetry-exporter-otlp-proto-http>=1.36.0
7+
python-dotenv>=1.0.0
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Run a LlamaIndex dice agent with standard OTLP export.
2+
3+
Uses the official LlamaIndexOpenTelemetry integration to set up OTel tracing.
4+
It handles the tracer provider setup and span export internally.
5+
Traces stream to agentevals via OTLPSpanExporter with no agentevals SDK needed.
6+
7+
Note: LlamaIndexOpenTelemetry exports spans only. Log-based content delivery
8+
is not part of this integration. Message content is captured via span attributes.
9+
10+
Prerequisites:
11+
1. pip install -r examples/zero-code-examples/llama-index/requirements.txt
12+
2. agentevals serve --dev
13+
3. export OPENAI_API_KEY="your-key-here"
14+
15+
Usage:
16+
python examples/zero-code-examples/llama-index/run.py
17+
"""
18+
19+
import asyncio
20+
import os
21+
import random
22+
23+
from dotenv import load_dotenv
24+
from llama_index.core.agent.workflow import FunctionAgent
25+
from llama_index.core.tools import FunctionTool
26+
from llama_index.llms.openai_like import OpenAILike
27+
from llama_index.observability.otel import LlamaIndexOpenTelemetry
28+
from opentelemetry import trace
29+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
30+
from opentelemetry.sdk.resources import Resource
31+
32+
load_dotenv(override=True)
33+
34+
35+
def roll_die(sides: int) -> int:
36+
"""Roll a die with the given number of sides and return the result."""
37+
return random.randint(1, sides)
38+
39+
40+
def check_prime(number: int) -> bool:
41+
"""Return True if the number is prime, False otherwise."""
42+
if number < 2:
43+
return False
44+
return all(number % i for i in range(2, int(number**0.5) + 1))
45+
46+
47+
async def main():
48+
if not os.getenv("OPENAI_API_KEY"):
49+
print("OPENAI_API_KEY not set.")
50+
return
51+
52+
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")
53+
print(f"OTLP endpoint: {endpoint}")
54+
55+
os.environ.setdefault(
56+
"OTEL_RESOURCE_ATTRIBUTES",
57+
"agentevals.eval_set_id=llama_index_eval,agentevals.session_name=llama-index-zero-code",
58+
)
59+
60+
resource = Resource.create()
61+
62+
LlamaIndexOpenTelemetry(
63+
span_exporter=OTLPSpanExporter(),
64+
span_processor="batch",
65+
service_name_or_resource=resource,
66+
).start_registering()
67+
68+
llm = OpenAILike(
69+
model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
70+
api_base=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
71+
is_chat_model=True,
72+
is_function_calling_model=True,
73+
)
74+
agent = FunctionAgent(
75+
tools=[FunctionTool.from_defaults(fn=roll_die), FunctionTool.from_defaults(fn=check_prime)],
76+
llm=llm,
77+
system_prompt="You are a helpful assistant. You can roll dice and check if numbers are prime.",
78+
)
79+
80+
test_queries = [
81+
"Hi! Can you help me?",
82+
"Roll a 20-sided die for me",
83+
"Is the number you rolled prime?",
84+
]
85+
86+
try:
87+
for i, query in enumerate(test_queries, 1):
88+
print(f"\n[{i}/{len(test_queries)}] User: {query}")
89+
result = await agent.run(query)
90+
print(f" Agent: {result.response.content}")
91+
finally:
92+
print()
93+
trace.get_tracer_provider().force_flush()
94+
print("All traces flushed to OTLP receiver.")
95+
96+
97+
if __name__ == "__main__":
98+
asyncio.run(main())

tests/integration/test_live_agents.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,72 @@ def test_session_visible_via_api(self, live_servers):
365365
assert session_name in session_ids
366366

367367

368+
@_skip_no_openai
369+
class TestLlamaIndexZeroCode:
370+
"""Run the LlamaIndex zero-code OTLP example and verify session grouping."""
371+
372+
def test_session_created_spans_only(self, live_servers):
373+
main_port, otlp_http_port, mgr = live_servers
374+
session_name = "e2e-llama-index"
375+
376+
result = _run_agent(
377+
"examples/zero-code-examples/llama-index/run.py",
378+
otlp_http_port,
379+
session_name,
380+
extra_env={
381+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "true",
382+
},
383+
)
384+
assert result.returncode == 0, f"Agent failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
385+
386+
wait_for_session_complete_sync(mgr, session_name, timeout=30)
387+
session = mgr.sessions[session_name]
388+
389+
assert session.is_complete
390+
assert session.source == "otlp"
391+
assert len(session.spans) > 0, "Expected spans from LlamaIndex agent"
392+
393+
def test_invocations_extracted(self, live_servers):
394+
main_port, otlp_http_port, mgr = live_servers
395+
session_name = "e2e-llama-index-inv"
396+
397+
result = _run_agent(
398+
"examples/zero-code-examples/llama-index/run.py",
399+
otlp_http_port,
400+
session_name,
401+
extra_env={
402+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "true",
403+
},
404+
)
405+
assert result.returncode == 0, f"Agent failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
406+
407+
wait_for_session_complete_sync(mgr, session_name, timeout=30)
408+
session = mgr.sessions[session_name]
409+
410+
assert len(session.invocations) > 0, "Expected extracted invocations"
411+
412+
def test_session_visible_via_api(self, live_servers):
413+
main_port, otlp_http_port, mgr = live_servers
414+
session_name = "e2e-llama-index-api"
415+
416+
result = _run_agent(
417+
"examples/zero-code-examples/llama-index/run.py",
418+
otlp_http_port,
419+
session_name,
420+
extra_env={
421+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "true",
422+
},
423+
)
424+
assert result.returncode == 0
425+
426+
wait_for_session_complete_sync(mgr, session_name, timeout=30)
427+
428+
resp = httpx.get(f"http://127.0.0.1:{main_port}/api/streaming/sessions")
429+
assert resp.status_code == 200
430+
session_ids = [s["sessionId"] for s in resp.json()["data"]]
431+
assert session_name in session_ids
432+
433+
368434
@_skip_no_openai
369435
class TestAgentRerun:
370436
"""Verify that re-running an agent with the same session_name creates

0 commit comments

Comments
 (0)