Skip to content

Streaming

ragwise supports token streaming out of the box for OpenAI, Anthropic, and Ollama. Tokens are yielded as they arrive — no waiting for the full response.

Basic usage

import asyncio
from ragwise import RAG

async def main():
    async with RAG(llm="openai/gpt-4o-mini") as rag:
        await rag.ingest("./docs/")

        async for token in rag.stream_query("What changed in v2?"):
            print(token, end="", flush=True)

        print()  # newline after stream ends

asyncio.run(main())

stream_query() uses the same retrieval path as query() — hybrid BM25+dense search, sufficiency check, context assembly — and then streams the LLM response token by token.

LLM support

Provider Streaming support
OpenAI (openai/gpt-4o-mini, etc.)
Anthropic (anthropic/claude-*)
Ollama (ollama/llama3, etc.)
Custom LLM (implements .complete() only) Fallback — yields full response as one token

If your custom LLM doesn't implement StreamingLLMProtocol, stream_query() falls back gracefully: it calls complete() and yields the full string as a single token. No errors, no exceptions.

Streaming in a FastAPI endpoint

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from ragwise import RAG

app = FastAPI()
rag: RAG | None = None

@app.on_event("startup")
async def startup():
    global rag
    rag = RAG(llm="openai/gpt-4o-mini")
    await rag.__aenter__()
    await rag.ingest("./docs/")

@app.get("/stream")
async def stream(q: str):
    async def token_generator():
        async for token in rag.stream_query(q):
            yield f"data: {token}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(token_generator(), media_type="text/event-stream")

This produces a standard Server-Sent Events (SSE) stream that any frontend can consume.

Streaming with config

All QueryConfig options apply to stream_query() exactly as they do to query():

from ragwise import RAG, QueryConfig

async with RAG(llm="openai/gpt-4o-mini", store="lance://./ragwise-index") as rag:
    config = QueryConfig(
        top_k=8,
        tenant_id="org_a",               # multi-tenant isolation
        include_citations=True,
    )

    async for token in rag.stream_query("What is the cancellation policy?", config=config):
        print(token, end="", flush=True)

Difference from query()

query() stream_query()
Return type Answer (text + citations) AsyncGenerator[str, None] (tokens)
Waits for full response Yes No — yields immediately
Citations answer.citations Not included — use rag.search() for sources
Caching Hits LLM cache Bypasses cache

If you need citations alongside streaming, call rag.search() first for the sources, then stream the generation:

results = await rag.search("What is the cancellation policy?", top_k=5)
sources = [r.source for r in results]

async for token in rag.stream_query("What is the cancellation policy?"):
    print(token, end="", flush=True)

print("\nSources:", sources)

Implementing StreamingLLMProtocol

To add streaming to a custom LLM:

from collections.abc import AsyncGenerator
from ragwise.generation.llm import StreamingLLMProtocol

class MyStreamingLLM:
    async def complete(self, prompt: str) -> str:
        # non-streaming fallback
        return "..."

    async def stream_complete(self, prompt: str) -> AsyncGenerator[str, None]:
        # yield tokens as they arrive from your LLM backend
        async for token in my_backend.stream(prompt):
            yield token

# ragwise detects StreamingLLMProtocol at runtime
async with RAG(llm=MyStreamingLLM()) as rag:
    async for token in rag.stream_query("..."):
        print(token, end="", flush=True)

ragwise uses isinstance(llm, StreamingLLMProtocol) at runtime to decide whether to stream or fall back. The protocol check is structural — no inheritance required.