Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
779bf1f
feat: add Arize Phoenix tracing integration
MichaelMcCulloch-deepsense-ai Jan 2, 2026
e9c7e83
feat: enhance Phoenix handler with OpenInference semantic conventions
MichaelMcCulloch-deepsense-ai Jan 6, 2026
ffb3704
licenses and libraries
MichaelMcCulloch-deepsense-ai Jan 9, 2026
7f80362
missed one
MichaelMcCulloch-deepsense-ai Jan 12, 2026
a5f8294
refactor: number the header instructions.
MichaelMcCulloch-deepsense-ai Jan 12, 2026
9a22e30
feat: update changelog
MichaelMcCulloch-deepsense-ai Jan 14, 2026
94fced1
feat: less restrictive versioning; unwhitelist aioitertools
MichaelMcCulloch-deepsense-ai Jan 20, 2026
8bd7d0a
feat: add openinference-semantic-conventions>=0.1.25
MichaelMcCulloch-deepsense-ai Jan 20, 2026
ef3db13
feat: Upgrade arize-phoenix to version 12.30.0, update the uv.lock fi…
MichaelMcCulloch-deepsense-ai Jan 21, 2026
3758794
chore: update package versions for nightly build 1.4.0.dev202601130240
ds-ragbits-robot Jan 13, 2026
ff01cdd
chore: Package configuration adjustment (#896)
mkoruszowic Jan 16, 2026
958da8f
chore: update package versions for nightly build 1.4.0.dev202601170236
ds-ragbits-robot Jan 17, 2026
8e1a354
feat: add Arize Phoenix tracing integration
MichaelMcCulloch-deepsense-ai Jan 2, 2026
9b1b0f6
feat: Upgrade arize-phoenix to version 12.30.0, update the uv.lock fi…
MichaelMcCulloch-deepsense-ai Jan 21, 2026
1588bc2
feat: Upgrade arize-phoenix to version 12.30.0, update the uv.lock fi…
MichaelMcCulloch-deepsense-ai Jan 21, 2026
54ec1b7
style: Standardize string formatting by adjusting line breaks and quo…
MichaelMcCulloch-deepsense-ai Jan 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .libraries-whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ types-requests
sse-starlette
google-genai
ollama
pytest-xdist
pytest-xdist
aioitertools
llvmlite
3 changes: 3 additions & 0 deletions .license-whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ Unlicense
Proprietary License
Historical Permission Notice and Disclaimer (HPND)
ISC
Elastic-2.0
zlib/libpng
aioitertools
98 changes: 98 additions & 0 deletions examples/core/audit/phoenix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
Ragbits Core Example: Arize Phoenix Audit

This example demonstrates how to collect traces using Ragbits audit module with Arize Phoenix.
We run a simple LLM generation to collect telemetry data, which is then sent to the Phoenix server.

1. The script exports traces to the local Phoenix server running on http://localhost:6006.
You need to have Phoenix running locally:

```bash
pip install arize-phoenix
python -m phoenix.server.main serve
```

2. To run the script, execute the following command:

```bash
uv run examples/core/audit/phoenix.py
```

3. To visualize the traces:
1. Open your browser and navigate to http://localhost:6006.
2. Check the Projects tab (default project).
"""

# /// script
# requires-python = ">=3.10"
# dependencies = [
# "ragbits-core[phoenix]",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it will be good to have constraint for the version where this extras will be available

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And which version would that be?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be included in the next release which is 1.4.0 . Am I right @mhordynski ?

# "tqdm",
# ]
# ///

import asyncio
from collections.abc import AsyncGenerator

from pydantic import BaseModel
from tqdm.asyncio import tqdm

from ragbits.core.audit import set_trace_handlers, traceable
from ragbits.core.llms import LiteLLM
from ragbits.core.prompt import Prompt


class QuestionPromptInput(BaseModel):
"""
Input schema for the question prompt.
"""

question: str


class QuestionPrompt(Prompt[QuestionPromptInput, str]):
"""
A simple prompt for answering questions.
"""

system_prompt = "You are a helpful assistant."
user_prompt = "Question: {{ question }}"


@traceable
async def process_request() -> None:
"""
Process an example request.
"""
llm = LiteLLM(model_name="gpt-3.5-turbo")

# Simple generation
prompt = QuestionPrompt(QuestionPromptInput(question="What is the capital of France?"))
await llm.generate(prompt)

# You can also use explicit tracing context
# with trace("my_custom_span") as span:
# ...


async def main() -> None:
"""
Run the example.
"""
# Ragbits observability setup
# This automatically sets up the OpenTelemetry TracerProvider pointing to Phoenix default (localhost:6006)
set_trace_handlers("phoenix")

print("Running requests... Make sure Phoenix is running at http://localhost:6006")

async def run() -> AsyncGenerator:
for _ in range(3):
await process_request()
yield

async for _ in tqdm(run()):
pass


if __name__ == "__main__":
asyncio.run(main())
2 changes: 2 additions & 0 deletions packages/ragbits-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Add Arize Phoenix tracing integration
- Enhance Phoenix handler with OpenInference semantic conventions
- Add Support for Thinking in agents (#837)
- Add support for confirmation requests in chat (#853)
- Add name parameter and slightly refactor HuggingFace dataloder (#829)
Expand Down
4 changes: 4 additions & 0 deletions packages/ragbits-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ logfire = [
"logfire[system-metrics]>=3.19.0,<4.0.0",
"opentelemetry-api>=1.27.0,<2.0.0",
]
phoenix = [
"arize-phoenix>=4.0.0,<5.0.0",
"openinference-instrumentation>=0.1.0,<1.0.0",
]
Comment on lines 76 to 80
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the range constraints so restrictive? uv uses >=4.36.0 by default

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12.0.0; 0.1.41

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update uv.lock accordingly

qdrant = [
"qdrant-client>=1.12.1,<2.0.0",
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
R = TypeVar("R")


def set_trace_handlers(handlers: Handler | list[Handler]) -> None:
def set_trace_handlers(handlers: Handler | list[Handler]) -> None: # noqa: PLR0912
"""
Set the global trace handlers.

Expand Down Expand Up @@ -63,6 +63,12 @@ def set_trace_handlers(handlers: Handler | list[Handler]) -> None:
if not any(isinstance(item, CLITraceHandler) for item in _trace_handlers):
_trace_handlers.append(CLITraceHandler())

case "phoenix":
from ragbits.core.audit.traces.phoenix import PhoenixTraceHandler

if not any(isinstance(item, PhoenixTraceHandler) for item in _trace_handlers):
_trace_handlers.append(PhoenixTraceHandler())

case _:
raise ValueError(f"Handler {handler} not found.")
else:
Expand Down
86 changes: 86 additions & 0 deletions packages/ragbits-core/src/ragbits/core/audit/traces/phoenix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import json

from openinference.semconv.trace import OpenInferenceSpanKindValues, SpanAttributes
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.trace import Span

from ragbits.core.audit.traces.otel import OtelTraceHandler


class PhoenixTraceHandler(OtelTraceHandler):
"""
Phoenix trace handler.
"""

def __init__(
self,
endpoint: str = "http://localhost:6006/v1/traces",
service_name: str = "ragbits-phoenix",
) -> None:
"""
Initialize the PhoenixTraceHandler instance.

Args:
endpoint: The Phoenix endpoint.
service_name: The service name.
"""
resource = Resource(attributes={SERVICE_NAME: service_name})
span_exporter = OTLPSpanExporter(endpoint=endpoint)
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter))
trace.set_tracer_provider(tracer_provider)
super().__init__(provider=tracer_provider)

def start(self, name: str, inputs: dict, current_span: Span | None = None) -> Span:
"""
Log input data at the beginning of the trace.

Args:
name: The name of the trace.
inputs: The input data.
current_span: The current trace span.

Returns:
The updated current trace span.
"""
span = super().start(name, inputs, current_span)

# Check if it's an LLM generation
if "generate" in name.lower():
span.set_attribute(SpanAttributes.OPENINFERENCE_SPAN_KIND, OpenInferenceSpanKindValues.LLM.value)

if "model_name" in inputs:
span.set_attribute(SpanAttributes.LLM_MODEL_NAME, inputs["model_name"])

if "prompt" in inputs:
span.set_attribute(SpanAttributes.INPUT_VALUE, str(inputs["prompt"]))

if "options" in inputs:
span.set_attribute(SpanAttributes.LLM_INVOCATION_PARAMETERS, str(inputs["options"]))

return span

def stop(self, outputs: dict, current_span: Span) -> None:
"""
Log output data at the end of the trace.

Args:
outputs: The output data.
current_span: The current trace span.
"""
if "response" in outputs:
response = outputs["response"]
# If response is a list of objects, serialize it
if isinstance(response, list | dict):
try:
current_span.set_attribute(SpanAttributes.OUTPUT_VALUE, json.dumps(str(response)))
except (TypeError, ValueError):
current_span.set_attribute(SpanAttributes.OUTPUT_VALUE, str(response))
else:
current_span.set_attribute(SpanAttributes.OUTPUT_VALUE, str(response))

super().stop(outputs, current_span)
97 changes: 97 additions & 0 deletions packages/ragbits-core/tests/unit/audit/test_phoenix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from unittest.mock import MagicMock, patch

import pytest
from openinference.semconv.trace import OpenInferenceSpanKindValues, SpanAttributes

from ragbits.core.audit.traces.phoenix import PhoenixTraceHandler


@pytest.fixture
def mock_otel_exporter():
with patch("ragbits.core.audit.traces.phoenix.OTLPSpanExporter") as mock:
yield mock


@pytest.fixture
def mock_tracer_provider():
with patch("ragbits.core.audit.traces.phoenix.TracerProvider") as mock:
yield mock


@pytest.fixture
def mock_batch_span_processor():
with patch("ragbits.core.audit.traces.phoenix.BatchSpanProcessor") as mock:
yield mock


@pytest.fixture
def mock_set_tracer_provider():
with patch("ragbits.core.audit.traces.phoenix.trace.set_tracer_provider") as mock:
yield mock


def test_phoenix_trace_handler_init(
mock_otel_exporter: MagicMock,
mock_tracer_provider: MagicMock,
mock_batch_span_processor: MagicMock,
mock_set_tracer_provider: MagicMock,
) -> None:
handler = PhoenixTraceHandler()

assert isinstance(handler, PhoenixTraceHandler)
mock_otel_exporter.assert_called_once_with(endpoint="http://localhost:6006/v1/traces")
mock_tracer_provider.assert_called_once()
mock_batch_span_processor.assert_called_once()
mock_set_tracer_provider.assert_called_once()


def test_phoenix_trace_handler_init_custom_endpoint(
mock_otel_exporter: MagicMock,
mock_tracer_provider: MagicMock,
mock_batch_span_processor: MagicMock,
mock_set_tracer_provider: MagicMock,
) -> None:
custom_endpoint = "http://custom-phoenix:6006/v1/traces"
handler = PhoenixTraceHandler(endpoint=custom_endpoint)

assert isinstance(handler, PhoenixTraceHandler)
mock_otel_exporter.assert_called_once_with(endpoint=custom_endpoint)


def test_start_llm_span(
mock_otel_exporter: MagicMock,
mock_tracer_provider: MagicMock,
mock_batch_span_processor: MagicMock,
mock_set_tracer_provider: MagicMock,
) -> None:
handler = PhoenixTraceHandler()
span_mock = MagicMock()

with patch("ragbits.core.audit.traces.otel.OtelTraceHandler.start", return_value=span_mock) as mock_super_start:
inputs = {"model_name": "gpt-4", "prompt": "hello", "options": {"temperature": 0.5}}
handler.start("generate", inputs)

mock_super_start.assert_called_once()
span_mock.set_attribute.assert_any_call(
SpanAttributes.OPENINFERENCE_SPAN_KIND, OpenInferenceSpanKindValues.LLM.value
)
span_mock.set_attribute.assert_any_call(SpanAttributes.LLM_MODEL_NAME, "gpt-4")
span_mock.set_attribute.assert_any_call(SpanAttributes.INPUT_VALUE, "hello")
span_mock.set_attribute.assert_any_call(SpanAttributes.LLM_INVOCATION_PARAMETERS, "{'temperature': 0.5}")


def test_stop_llm_span(
mock_otel_exporter: MagicMock,
mock_tracer_provider: MagicMock,
mock_batch_span_processor: MagicMock,
mock_set_tracer_provider: MagicMock,
) -> None:
handler = PhoenixTraceHandler()
span_mock = MagicMock()

with patch("ragbits.core.audit.traces.otel.OtelTraceHandler.stop") as mock_super_stop:
outputs = {"response": "world"}
handler.stop(outputs, span_mock)

mock_super_stop.assert_called_once()
span_mock.set_attribute.assert_any_call(SpanAttributes.OUTPUT_VALUE, "world")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"ragbits-cli",
"ragbits-core[chroma,fastembed,local,otel,logfire,qdrant,pgvector,weaviate,azure,gcs,hf,s3,google_drive]",
"ragbits-core[chroma,fastembed,local,otel,logfire,qdrant,pgvector,weaviate,azure,gcs,hf,s3,google_drive,phoenix]",
"ragbits-document-search[unstructured,ray]",
"ragbits-evaluate[relari]",
"ragbits-guardrails[openai]",
Expand Down
Loading
Loading