Skip to content

LlamaIndex Integration

LlamaIndex-based RAG pipelines introduce an extra drift surface beyond the raw model: retrieval quality and context assembly can shift silently even when the underlying LLM hasn't changed. Two patterns cover most needs.

Pattern 1: Test the Underlying LLM Directly

If you only care about model-level drift (not retrieval), test the same model your LlamaIndex Settings.llm points to, using LiteLLMProvider directly -- no LlamaIndex dependency needed for this path.

from promptcanary import CanarySuite, LiteLLMProvider

# If your LlamaIndex app uses:
#   Settings.llm = OpenAI(model="gpt-5.4")
# test the same model directly:
suite = CanarySuite.from_yaml("canary.yaml")
provider = LiteLLMProvider("openai/gpt-5.4")
result = suite.run(provider)

Pattern 2: Wrap Your Query Engine as a Custom Provider

To canary-test your full RAG pipeline -- retrieval, context assembly, synthesis -- wrap your LlamaIndex query engine in a BaseLLMProvider:

from promptcanary.providers.base import BaseLLMProvider
from promptcanary.core.models import CanaryPrompt, LLMResponse, ProviderConfig

class LlamaIndexProvider(BaseLLMProvider):
    """Wraps a LlamaIndex query engine for end-to-end canary testing."""

    def __init__(self, query_engine, model_id: str = "llamaindex/custom-rag"):
        super().__init__(ProviderConfig(model_id=model_id))
        self.query_engine = query_engine

    def complete(self, prompt: CanaryPrompt, *, system_prompt: str | None = None) -> LLMResponse:
        response = self.query_engine.query(prompt.text)
        return LLMResponse(
            prompt_id=prompt.id,
            provider_model_id=self.config.model_id,
            content=str(response),
            finish_reason="stop",
            raw_response={"source_node_count": len(getattr(response, "source_nodes", []))},
        )


# Usage:
# from your_app import my_query_engine
# provider = LlamaIndexProvider(my_query_engine)
# result = suite.run(provider)

This catches drift from anywhere in the pipeline: embedding model changes, index rebuilds that shift retrieval results, chunking strategy edits, or synthesis prompt changes -- not just the underlying LLM.

Detecting Retrieval Drift Specifically

If you want to isolate retrieval drift from generation drift, add a custom probe that inspects the retrieved-node count (captured above in raw_response) rather than only the final answer text:

from promptcanary.core.probes.base import BaseProbe
from promptcanary.core.models import CanaryPrompt, LLMResponse, ProbeCategory

class RetrievalCountProbe(BaseProbe):
    """Flags when the number of retrieved source nodes changes unexpectedly."""

    probe_id = "retrieval_count"
    name = "Retrieval Node Count"
    category = ProbeCategory.CUSTOM

    def __init__(self, expected_min: int = 1) -> None:
        self.expected_min = expected_min

    def evaluate(self, prompt: CanaryPrompt, response: LLMResponse):
        count = response.raw_response.get("source_node_count", 0)
        passed = count >= self.expected_min
        return self._make_result(
            prompt.id,
            passed=passed,
            score=1.0 if passed else 0.0,
            details=f"Retrieved {count} source node(s) (min expected: {self.expected_min}).",
        )

LLMResponse.raw_response is a dict[str, Any] field designed exactly for this: stashing pipeline-specific metadata that a custom probe can read back, without needing to extend the core model.

Which Pattern to Choose

Goal Pattern
Detect provider-side model drift only Pattern 1 -- direct LiteLLMProvider
Detect drift anywhere in your RAG pipeline Pattern 2 -- wrap the query engine
Isolate retrieval drift from generation drift Pattern 2 + a custom retrieval-aware probe
Both model and pipeline coverage Run both suites; a regression in Pattern 1 only means the model changed; a regression in Pattern 2 only means your pipeline changed