Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions apps/opik-documentation/documentation/fern/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,9 @@ navigation:
- section: Other
skip-slug: true
contents:
- page: Qdrant
path: docs/tracing/integrations/qdrant.mdx
slug: qdrant
- page: Guardrails AI
path: docs/tracing/integrations/guardrails-ai.mdx
slug: guardrails-ai
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: Qdrant
description: Track Qdrant retrieval operations in Opik.
headline: Qdrant | Opik Documentation
---

Opik can instrument Qdrant retrieval calls and log them as tool spans.

## Install

```bash
pip install opik qdrant-client
```

## Usage

```python
from qdrant_client import QdrantClient
from opik.integrations.qdrant import track_qdrant

client = QdrantClient(url="http://localhost:6333")
client = track_qdrant(client, project_name="retrieval-demo")

result = client.search(
collection_name="docs",
query_vector=[0.1, 0.2, 0.3],
limit=5,
)
```
Comment on lines +17 to +29
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new Qdrant example is a hand‑pasted snippet without the required one‑line intent/trigger note, required-vs-optional field guidance, or maintenance note pointing to whichever autogenerated canonical example this complements; can we either link to the generated example or add those required doc attributes (intent, applicability, optional/required fields, and pointer to the canonical source and update cadence) when embedding it?

Finding type: Keep docs accurate


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

In apps/opik-documentation/documentation/fern/docs/tracing/integrations/qdrant.mdx
around lines 17-29, the embedded Python usage snippet (the example/usage block) is
missing the required documentation attributes: a one-line intent/trigger note, guidance
which fields are required vs optional, and a maintenance note linking to the canonical
autogenerated example plus its update cadence. Edit the chunk immediately before or
above the code block to add (1) a single-sentence intent/trigger describing when to use
this snippet, (2) a short bullet or parenthetical that lists which parameters are
required vs optional in the example (e.g., required: client and collection_name;
optional: query_vector, limit), and (3) a maintenance note with a pointer (URL or repo
path) to the autogenerated canonical example and a recommended update cadence. Keep the
changes concise and place them so they are clearly associated with the usage example.


## What gets logged

- span `type`: `tool`
- span name format: `qdrant.<operation>`
- metadata keys:
- `opik.kind=retrieval`
- `opik.provider=qdrant`
- `opik.operation=<operation>`

Tracked operations include `query`, `query_points`, `search`, `search_batch`, `recommend`, `discover`, and `scroll` when available.
91 changes: 91 additions & 0 deletions sdks/python/src/opik/integrations/_retrieval_tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, Iterable, Optional, Tuple

import opik

Metadata = Dict[str, Any]


@dataclass(frozen=True)
class RetrievalTrackingConfig:
provider: str
operation_paths: Tuple[str, ...]
project_name: Optional[str] = None
Comment on lines +11 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

RetrievalTrackingConfig and the two public helpers in this module (patch_retrieval_client, as_tuple) currently have no docstrings, so callers can't learn what fields/arguments mean, what is returned, or what happens when no operations are patched; can we add concise docstrings (including the dataclass fields, the target mutation/opik_tracked side effect, and the tuple conversion) per the documentation guidance?

Finding type: Document behavior and context


  • Apply fix with Baz
Other fix methods

Fix in Cursor

Prompt for AI Agents:

In sdks/python/src/opik/integrations/_retrieval_tracker.py around lines 11 to 15, add
concise docstrings to the RetrievalTrackingConfig dataclass and to the two public helper
functions patch_retrieval_client and as_tuple. For RetrievalTrackingConfig, document
each field (provider, operation_paths, project_name) and what they represent. For
patch_retrieval_client, document the target parameter, the fact that the function may
mutate target by setting target.opik_tracked when any operations are patched, the return
value (the original or mutated target), and behavior when no operations are found. For
as_tuple, document that it converts an iterable of paths to an immutable tuple of
strings. Keep each docstring short (one to three sentences) and follow existing
project's docstring style.



def _resolve_target_and_method(root: Any, path: str) -> Tuple[Optional[Any], Optional[str]]:
parts = path.split(".")
target = root
for part in parts[:-1]:
target = getattr(target, part, None)
if target is None:
return None, None
return target, parts[-1]


def _build_metadata(provider: str, operation: str) -> Metadata:
return {
"created_from": provider,
"opik.kind": "retrieval",
"opik.provider": provider,
"opik.operation": operation,
}


def _patch_operation(
target: Any,
method_name: str,
provider: str,
operation_name: str,
project_name: Optional[str],
) -> bool:
method = getattr(target, method_name, None)
if method is None or not callable(method):
return False

if hasattr(method, "opik_tracked"):
return False

decorator = opik.track(
name=f"{provider}.{operation_name}",
type="tool",
tags=[provider, "retrieval"],
metadata=_build_metadata(provider, operation_name),
project_name=project_name,
)
setattr(target, method_name, decorator(method))
return True


def patch_retrieval_client(target: Any, config: RetrievalTrackingConfig) -> Any:
if hasattr(target, "opik_tracked"):
return target

patched = False
for operation_path in config.operation_paths:
operation_target, method_name = _resolve_target_and_method(target, operation_path)
if operation_target is None or method_name is None:
continue

operation_name = operation_path.split(".")[-1]
patched = (
_patch_operation(
operation_target,
method_name,
provider=config.provider,
operation_name=operation_name,
project_name=config.project_name,
)
or patched
)

if patched:
target.opik_tracked = True

return target


def as_tuple(paths: Iterable[str]) -> Tuple[str, ...]:
return tuple(paths)
3 changes: 3 additions & 0 deletions sdks/python/src/opik/integrations/qdrant/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .opik_tracker import track_qdrant

__all__ = ["track_qdrant"]
32 changes: 32 additions & 0 deletions sdks/python/src/opik/integrations/qdrant/opik_tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

from typing import Any, Optional

from opik.integrations._retrieval_tracker import (
RetrievalTrackingConfig,
as_tuple,
patch_retrieval_client,
)


def track_qdrant(qdrant_client: Any, project_name: Optional[str] = None) -> Any:
"""Adds Opik tracking wrappers to a Qdrant client.

The integration is dependency-light and patches known retrieval methods if present.
"""
config = RetrievalTrackingConfig(
provider="qdrant",
operation_paths=as_tuple(
[
"query",
"query_points",
"search",
"search_batch",
"recommend",
"discover",
"scroll",
]
),
project_name=project_name,
)
return patch_retrieval_client(qdrant_client, config)
Comment on lines +12 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

New track_qdrant patching logic adds retrieval tracking per sdks/python/AGENTS.md, but no unit test under tests/unit/ exercises it (no existing coverage for _retrieval_tracker or Qdrant patching paths); can we add a test that verifies the expected methods are wrapped/tagged or document why it's intentionally skipped? (Guideline: sdks/python/AGENTS.md)

Finding type: AI Coding Guidelines


  • Apply fix with Baz
Other fix methods

Fix in Cursor

Prompt for AI Agents:

In sdks/python/src/opik/integrations/qdrant/opik_tracker.py around lines 12-32, the new
track_qdrant function adds retrieval-patching logic but has no unit test coverage. Add a
new unit test file tests/unit/test_qdrant_tracker.py that constructs a minimal fake
Qdrant client object with callable attributes named: query, query_points, search,
search_batch, recommend, discover, and scroll; call track_qdrant(fake_client) and then
assert each of those attributes is still callable but has been replaced/wrapped (for
example, by having the fake methods set a flag or increment a counter when invoked and
the wrapper should still call the original and allow inspection). Make the test verify
at least that (1) each method exists and is callable after wrapping, (2) invoking a
wrapped method calls through to the original fake implementation, and (3) the
RetrievalTrackingConfig.provider equals 'qdrant' via any accessible side-effect or by
patching/inspecting the patch_retrieval_client call. If adding a test is not feasible,
add a short explanatory note in tests/unit/README.md describing why Qdrant patching is
intentionally untested and referencing sdks/python/AGENTS.md.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Commit 9b00b9c addressed this comment by adding a new unit test file sdks/python/tests/unit/integrations/test_qdrant_tracker.py that exercises the Qdrant retrieval patching path via track_qdrant(), spies on opik.integrations._retrieval_tracker.opik.track, asserts retrieval metadata (e.g., opik.provider == "qdrant" and operations like qdrant.search/qdrant.query_points), and verifies patching idempotency (not re-applied).

76 changes: 76 additions & 0 deletions sdks/python/tests/unit/integrations/test_qdrant_tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, List

from opik.integrations.qdrant.opik_tracker import track_qdrant


@dataclass
class _Calls:
values: List[Dict[str, Any]]


class _TrackSpy:
def __init__(self) -> None:
self.calls = _Calls(values=[])

def __call__(self, **track_kwargs: Any): # type: ignore[override]
self.calls.values.append(track_kwargs)

def decorator(func: Any) -> Any:
def wrapped(*args: Any, **kwargs: Any) -> Any:
return func(*args, **kwargs)

wrapped.opik_tracked = True
return wrapped

return decorator


def _build_qdrant_client() -> Any:
class Client:
def search(self) -> Dict[str, Any]:
return {"ok": True}

def query_points(self) -> Dict[str, Any]:
return {"ok": True}

return Client()


def test_track_qdrant__patches_methods_with_retrieval_metadata(monkeypatch: Any) -> None:
import opik.integrations._retrieval_tracker as retrieval_tracker

track_spy = _TrackSpy()
monkeypatch.setattr(retrieval_tracker.opik, "track", track_spy)

client = _build_qdrant_client()
tracked_client = track_qdrant(client, project_name="proj")

assert tracked_client is client
assert tracked_client.opik_tracked is True
assert len(track_spy.calls.values) == 2

operation_names = {call["name"] for call in track_spy.calls.values}
assert operation_names == {"qdrant.search", "qdrant.query_points"}

for call in track_spy.calls.values:
assert call["type"] == "tool"
assert call["metadata"]["opik.kind"] == "retrieval"
assert call["metadata"]["opik.provider"] == "qdrant"
assert "opik.operation" in call["metadata"]


def test_track_qdrant__idempotent(monkeypatch: Any) -> None:
import opik.integrations._retrieval_tracker as retrieval_tracker

track_spy = _TrackSpy()
monkeypatch.setattr(retrieval_tracker.opik, "track", track_spy)

client = _build_qdrant_client()
track_qdrant(client)
calls_after_first = len(track_spy.calls.values)
track_qdrant(client)

assert len(track_spy.calls.values) == calls_after_first
Loading