Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ INDEXERUI_PORT=8060 # Port to expose the Indexer UI
INDEXERUI_URL='http://X.X.X.X:INDEXERUI_PORT'
API_BASE_URL='http://X.X.X.X:APP_PORT' # Base URL of your FastAPI backend.

# Web Search
# WEBSEARCH_API_TOKEN= # Web search provider API token. If unset, web search is silently disabled.
# WEBSEARCH_BASE_URL=https://api.staan.ai/search/web # Web search provider endpoint
# WEBSEARCH_TOP_K=5 # Number of web results to include (default: 5)
# WEBSEARCH_LANG=fr-FR # Search language/market (default: fr-FR)

# LOGGING
LOG_LEVEL=DEBUG # See possible values https://loguru.readthedocs.io/en/stable/api/logger.html

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ services/*
#helm
charts/openrag-stack/charts/*.tgz

# Planning
.planning/

# Astro / Starlight
.astro/
dist/
Expand Down
2 changes: 2 additions & 0 deletions .hydra_config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defaults:
- chunker: ${oc.env:CHUNKER, recursive_splitter} # recursive_splitter
- retriever: ${oc.env:RETRIEVER_TYPE, single} # single # multiQuery # hyde
- rag: ChatBotRag
- websearch: ${oc.env:WEBSEARCH_PROVIDER, staan}

llm_params: &llm_params
temperature: 0.1
Expand Down Expand Up @@ -67,6 +68,7 @@ map_reduce:
# Enable debug logging for map & reduce
debug: ${oc.decode:${oc.env:MAP_REDUCE_DEBUG, false}}


verbose:
level: ${oc.env:LOG_LEVEL, DEBUG}

Expand Down
11 changes: 11 additions & 0 deletions .hydra_config/websearch/base.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
provider: ''
api_token: ${oc.env:WEBSEARCH_API_TOKEN, ""}
base_url: ''
top_k: ${oc.decode:${oc.env:WEBSEARCH_TOP_K, 5}}
lang: ${oc.env:WEBSEARCH_LANG, fr-FR}
max_tokens: ${oc.decode:${oc.env:WEBSEARCH_MAX_TOKENS, 2000}}
fetch_content: ${oc.decode:${oc.env:WEBSEARCH_FETCH_CONTENT, true}}
fetch_max_results: ${oc.decode:${oc.env:WEBSEARCH_FETCH_MAX_RESULTS, 3}}
fetch_timeout: ${oc.decode:${oc.env:WEBSEARCH_FETCH_TIMEOUT, 1.0}}
fetch_max_tokens: ${oc.decode:${oc.env:WEBSEARCH_FETCH_MAX_TOKENS, 500}}
fetch_verify_ssl: ${oc.decode:${oc.env:WEBSEARCH_FETCH_VERIFY_SSL, false}}
5 changes: 5 additions & 0 deletions .hydra_config/websearch/staan.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defaults:
- base

provider: staan
base_url: ${oc.env:WEBSEARCH_BASE_URL, "https://api.staan.ai/search/web"}
24 changes: 23 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,28 @@ await vectordb.list_partition_members.remote(partition)
- For admins with `SUPER_ADMIN_MODE=true`, `all` resolves to all system partitions
- Model prefix is `openrag-` (legacy: `ragondin-`)

### Web Search Integration

Optional web search augmentation via the Staan API, allowing the LLM to combine RAG document context with live web results.

**Configuration** (`.hydra_config/config.yaml` → `websearch:` block, env vars):
- `WEBSEARCH_API_TOKEN` — provider API token; if unset, web search is silently disabled
- `WEBSEARCH_BASE_URL` — provider endpoint (default: Staan API)
- `WEBSEARCH_TOP_K` — number of web results (default: 5)
- `WEBSEARCH_LANG` — search language/market (default: `fr-FR`)

**How it works:**
- Client sends `metadata: {"websearch": true}` in the chat completion request
- **Combined mode** (partition + websearch): RAG retrieval and web search run concurrently via `asyncio.gather()`; web results are appended after document sources with continuous `[Source N]` numbering
- **Web-only mode** (no partition + websearch): skips RAG retrieval entirely, uses web results as sole context; if no results (token unset / search fails), falls back to plain direct LLM mode
- Source entries include `source_type: "document"` or `source_type: "web"` in the `extra.sources` response

**Key files:**
- `openrag/components/websearch/` — `WebSearchService`, `BaseWebSearchProvider`, `StaanProvider`
- `openrag/components/utils.py` — `format_web_context()` formats web results as numbered source blocks
- `openrag/components/pipeline.py` — `_prepare_for_web_only()`, web search logic in `_prepare_for_chat_completion()`
- `openrag/routers/openai.py` — `__prepare_sources()` merges document and web sources

### File Quota System

Per-user file quota enforcement tracked via the `file_count` and `file_quota` columns on `users`, and `created_by` on `files`.
Expand Down Expand Up @@ -236,7 +258,7 @@ Environment variables override config values (see `.env.example`).
act -j api-tests -W .github/workflows/api_tests.yml --bind
```

**Mock VLLM for CI:** `.github/workflows/api_tests/mock_vllm.py` provides fake embeddings and completions endpoints (streaming and non-streaming) for testing without a real LLM. Pydantic request models use `ConfigDict(extra="allow")` to accept vendor-specific fields like `extra_body`.
**Mock VLLM for CI:** `tests/api_tests/api_run/mock_vllm.py` provides fake embeddings and completions endpoints (streaming and non-streaming) for testing without a real LLM. Pydantic request models use `ConfigDict(extra="allow")` to accept vendor-specific fields like `extra_body`.

## Key Patterns

Expand Down
57 changes: 48 additions & 9 deletions docs/content/docs/documentation/API.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -400,17 +400,16 @@ OpenAI-compatible text completion endpoint.

#### Extra arguments

* When using the openai endpoint /v1/chat/completions, one can provide extra arguments in the request body to customize the RAG behavior:
- `spoken_style_answer`: boolean (default: false) - If true, the model will generate a succint spoken style conversational answer based on the retrieved documents.
- `use_map_reduce`: boolean (default: false) - If true, the model will use a map-reduce strategy to aggregate information from multiple documents. For more information see the [map-reduce documentation](/openrag/documentation/env_vars/#map--reduce-configuration).
- `llm_override`: object (optional) - Route the request to a different LLM endpoint while still using OpenRAG's RAG pipeline (retrieval, reranking, prompt construction). Accepts the following fields:
- `base_url`: string - Base URL of the target LLM API (e.g. `https://api.openai.com/v1`)
- `api_key`: string - API key for the target LLM
- `model`: string - Model name to use on the target endpoint
* When using the openai endpoint /v1/chat/completions, you can pass extra arguments **via the `metadata` field** of the request body to customize the RAG behavior:

Any field not provided falls back to the default OpenRAG LLM configuration.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `websearch` | `bool` | `false` | Augments the RAG context with live web search results. When used with a partition (`openrag-{partition}`), document and web results are combined. When used without a partition (direct LLM mode), web results are the sole context. Requires `WEBSEARCH_API_TOKEN` to be configured. See [web search configuration](/openrag/documentation/env_vars/#web-search-configuration). |

@Ahmath-Gadji Ahmath-Gadji Mar 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It would nice to add the websearch metadata here like use_map_reduce

class OpenAIChatCompletionRequest(BaseModel):
"""Modèle représentant une requête de complétion chat pour l'API OpenAI."""
model: str | None = Field(None, description="model name")
messages: list[OpenAIMessage]
temperature: float | None = Field(0.3)
top_p: float | None = Field(1.0)
stream: bool | None = Field(False)
max_tokens: int | None = Field(default_max_tokens)
logprobs: int | None = Field(None)
metadata: dict[str, Any] | None = Field(
{
"use_map_reduce": False,
"spoken_style_answer": False,
},
description="Extra custom parameters. Supports 'llm_override' object with optional 'base_url', 'api_key', and 'model' to override the downstream LLM endpoint.",
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

And in the chainlit frontend we can also provide the websearch functionality following what's been done here:

commands = [
{
"id": "DeepSearch",
"icon": "brain-cog",
"description": "This uses a custom DeepSearch RAG mechanism (Map & Reduce) to handle complex queries.\nSlower but gives accurate answers.\nUse in an empty context as it consumes more tokens.",
},
{
"id": "SpokenStyleAnswer",
"icon": "audio-lines",
"description": "Get a conversational text answer suitable for voice assistants.\nThe answer is concise, clear, and factual.",
"persistent": True,
},
]

"metadata": {
"use_map_reduce": message.command == "DeepSearch",
"spoken_style_answer": message.command == "SpokenStyleAnswer",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in ad422b5

| `spoken_style_answer` | `bool` | `false` | Generates a succinct spoken-style conversational answer based on the retrieved documents. |
| `use_map_reduce` | `bool` | `false` | Uses a map-reduce strategy to aggregate information from multiple documents. See [map-reduce configuration](/openrag/documentation/env_vars/#map--reduce-configuration). |
| `llm_override` | `object` | `null` | Routes the request to a different LLM endpoint while still using OpenRAG's RAG pipeline (retrieval, reranking, prompt construction). Accepts: `base_url` (string), `api_key` (string), `model` (string). Any field not provided falls back to the default OpenRAG LLM configuration. |

These arguments are supplied via the metadata field of the OpenAI request body. Example:
Examples:

```bash title="Enabling conversational answer with openai chat completions endpoint"
curl -X 'POST' 'http://localhost:8080/v1/chat/completions' \
Expand All @@ -433,6 +432,46 @@ curl -X 'POST' 'http://localhost:8080/v1/chat/completions' \
}'
```

```bash title="Enabling web search with RAG documents"
curl -X 'POST' 'http://localhost:8080/v1/chat/completions' \
-H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_AUTH_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"model": "openrag-{partition_name}",
"messages": [
{
"role": "user",
"content": "your_query"
}
],
"stream": false,
"metadata": {
"websearch": true
}
}'
```

```bash title="Web search only (no RAG partition)"
curl -X 'POST' 'http://localhost:8080/v1/chat/completions' \
-H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_AUTH_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"model": "",
"messages": [
{
"role": "user",
"content": "your_query"
}
],
"stream": false,
"metadata": {
"websearch": true
}
}'
```

```bash title="Using a custom LLM endpoint with OpenRAG's RAG pipeline"
curl -X 'POST' 'http://localhost:8080/v1/chat/completions' \
-H 'accept: application/json' \
Expand Down
22 changes: 22 additions & 0 deletions docs/content/docs/documentation/env_vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,28 @@ Ray Serve enables deployment of the FastAPI as a scalable service. For simple de
| `CHAINLIT_PORT` | int | 8090 | Port for the Chainlit UI interface if ray serve is enable `ENABLE_RAY_SERVE`. If not chainlit UI is simply a subroute (`/chainlit` [see this](/openrag/getting_started/usage/#default-ports)) of the FastAPI **`base_url`**|


### Web Search Configuration

Web search allows the LLM to augment RAG document context with live web results. It is disabled by default — set `WEBSEARCH_API_TOKEN` to enable it.

| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `WEBSEARCH_PROVIDER` | `str` | `staan` | Web search provider to use. Currently supported: `staan`. |
| `WEBSEARCH_API_TOKEN` | `str` | `""` | API token for the web search provider. If empty, web search is disabled. |
| `WEBSEARCH_BASE_URL` | `str` | (provider default) | Base URL of the web search provider API. |
| `WEBSEARCH_TOP_K` | `int` | `5` | Number of web search results to return. |
| `WEBSEARCH_LANG` | `str` | `fr-FR` | Language/market code for web search queries. |
| `WEBSEARCH_MAX_TOKENS` | `int` | `2000` | Maximum token budget for all web sources combined in the LLM context. This budget is reserved from the global context window when web results are present. |
| `WEBSEARCH_FETCH_CONTENT` | `bool` | `true` | When enabled, fetches actual page content from the top URLs instead of relying on short search snippets. |
| `WEBSEARCH_FETCH_MAX_RESULTS` | `int` | `3` | Number of top URLs to fetch content from (the remaining results use their search snippet). |
| `WEBSEARCH_FETCH_TIMEOUT` | `float` | `1.0` | Per-URL timeout in seconds for content fetching. URLs that don't respond within this time fall back to their snippet. |
| `WEBSEARCH_FETCH_MAX_TOKENS` | `int` | `500` | Maximum approximate tokens of content to extract per page. Content is truncated at word boundaries. |
| `WEBSEARCH_FETCH_VERIFY_SSL` | `bool` | `false` | Whether to verify SSL certificates when fetching page content. |

:::tip[How to Enable Web Search?]
When chatting, you can enable web search through the OpenAI-compatible API by setting `"websearch": true` in the `metadata` field of the request body. See the [API documentation](/openrag/documentation/api/#extra-arguments) for examples.
:::

### Map & Reduce Configuration
The map & reduce mechanism processes documents by fetching chunks (map phase), filtering out irrelevant ones and summarizing relevant content (reduce phase) with respect to the user's query. The algorithm works as follows:

Expand Down
16 changes: 16 additions & 0 deletions docs/content/docs/documentation/features_in_details.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ See the section on [distributed deployment in a ray cluster](#5-distributed-depl

</details>

### 🌐 Web Search Augmentation
Enhance RAG responses with live web search results. When enabled, the LLM can combine document context with up-to-date information from the web.

<details>

<summary>Web Search Features</summary>

* **Combined mode** — RAG retrieval and web search run concurrently; web results are appended as additional sources alongside document sources
* **Web-only mode** — Skip RAG entirely by omitting the partition; uses web results as the sole context
* **Content fetching** — The top 3 URLs are fetched in parallel (1s timeout) and their main content is extracted, providing richer context than search snippets alone
* **Boilerplate filtering** — Navigation, footers, headers, and other non-content HTML elements are stripped before extraction
* **Graceful fallback** — If web search fails or returns no results, the pipeline continues with document context only (or falls back to direct LLM mode)
* **Source attribution** — Web sources are tagged with `source_type: "web"` in the response, distinct from `source_type: "document"`

</details>

### 🔍 Advanced Retrieval & Reranking
[OpenRag](https://open-rag.ai/) Leverages state-of-the-art retrieval techniques for superior accuracy.

Expand Down
17 changes: 17 additions & 0 deletions openrag/app_front.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
"description": "Get a conversational text answer suitable for voice assistants.\nThe answer is concise, clear, and factual.",
"persistent": True,
},
{
"id": "WebSearch",
"icon": "globe",
"description": "Augment the RAG context with live web search results.\nCombines document and web sources for more comprehensive answers.",
},
]


Expand Down Expand Up @@ -162,6 +167,17 @@ async def _format_sources(metadata_sources, only_txt=False, api_key=None):
d = {}
headers = get_headers(api_key)
for i, s in enumerate(metadata_sources):
if s.get("source_type") == "web":
title = s.get("title") or s.get("url", f"Web source {i + 1}")
url = s.get("url", "")
snippet = s.get("snippet", "")
content = f"**[{title}]({url})**\n\n{snippet}"
source_name = title
if source_name in d:
source_name = f"{title} ({i})"
d[source_name] = cl.Text(content=content, name=source_name, display="side")
continue

filename = Path(s["filename"])
file_url = s["file_url"]
file_url = file_url.replace(INTERNAL_BASE_URL, external_url) # put the correct base url
Expand Down Expand Up @@ -222,6 +238,7 @@ async def on_message(message: cl.Message):
"metadata": {
"use_map_reduce": message.command == "DeepSearch",
"spoken_style_answer": message.command == "SpokenStyleAnswer",
"websearch": message.command == "WebSearch",
},
}

Expand Down
Loading