# AGENTS.md

This document is the primary engineering guide for autonomous coding agents working in the `UltraRAG` repository.

Use this file as the source of truth for architecture, conventions, workflows, and safe change patterns.
If `CLAUDE.md` exists, it should only point to this file.

---

## 1) Project Identity

`UltraRAG` is a lightweight RAG framework built around the Model Context Protocol (MCP).
The key design choice is strict modularization: retrieval, prompting, generation, routing, memory, and evaluation are implemented as independent MCP servers orchestrated by YAML pipelines.

Current core metadata:

- Package: `ultrarag`
- Version: `0.3.0`
- Python: `>=3.11, <3.13`
- CLI entrypoint: `ultrarag = ultrarag.client:main`
- Package manager: `uv` (`[tool.uv] package = true`)

---

## 2) Repository Map (What Matters Most)

```text
UltraRAG/
├── src/ultrarag/                    # Installable core package
│   ├── client.py                    # CLI + pipeline engine + run/build orchestration
│   ├── server.py                    # UltraRAG_MCP_Server (FastMCP extension)
│   ├── api.py                       # Python API wrappers (ToolCall, PipelineCall)
│   ├── cli.py                       # Rich banner and CLI visuals
│   ├── mcp_logging.py               # Central logging setup
│   ├── mcp_exceptions.py            # Node.js checks for remote MCP
│   └── utils.py                     # Subprocess lifecycle helpers
│
├── servers/                         # MCP microservices (each server is independent)
│   ├── retriever/
│   ├── generation/
│   ├── prompt/
│   ├── reranker/
│   ├── benchmark/
│   ├── evaluation/
│   ├── corpus/
│   ├── memory/
│   ├── router/
│   ├── custom/
│   ├── pageindex/
│   └── sayhello/
│
├── examples/
│   ├── demos/                       # UI-ready demo pipelines
│   └── experiments/                 # Experiment/research pipelines
│
├── ui/
│   ├── backend/                     # Flask backend + pipeline manager
│   └── frontend/                    # Vite + React + TypeScript
│
├── docs/                            # Docs and assets
├── script/                          # Utility scripts (deploy, case study, etc.)
├── pyproject.toml                   # Dependencies + package metadata
├── uv.lock                          # Locked dependency graph
├── Dockerfile*                      # Container variants
└── .gitignore
```

Important generated/derived files:

- `servers/*/server.yaml` (generated by each server `build` tool)
- `examples/**/parameter/*_parameter.yaml` (pipeline-merged parameters)
- `examples/**/server/*_server.yaml` (pipeline-merged server config)
- `output/memory_*.json` (per-run memory snapshots)

---

## 3) Mental Model of the System

Think of UltraRAG as a three-layer system:

1. **Interface layer**: CLI (`ultrarag ...`), UI (`ultrarag show ui`), and Python API (`ToolCall`, `PipelineCall`)
2. **Orchestration layer**: `src/ultrarag/client.py` (`build`, `load_pipeline_context`, `execute_pipeline`)
3. **Execution layer**: MCP servers in `servers/*`, each exposing tools/prompts over stdio (or remote MCP proxy)

The runtime contract is:

- A pipeline YAML declares **which servers** to use and **which steps** to execute.
- The client resolves I/O dependencies between steps.
- Each step calls exactly one MCP tool or prompt.
- Outputs are saved to a shared variable pool and can feed downstream steps.

---

## 4) Two-Phase Execution Lifecycle

### Phase A: Build

Command:

```bash
ultrarag build <pipeline.yaml>
```

What happens:

- Reads `servers:` from the pipeline YAML.
- For each referenced server, calls the server's `build` tool.
- Produces:
  - `<pipeline_dir>/parameter/<pipeline_name>_parameter.yaml`
  - `<pipeline_dir>/server/<pipeline_name>_server.yaml`

Why this matters:

- `build` materializes exact tool/prompt I/O metadata before runtime.
- UI and runner rely on these generated artifacts for deterministic execution.

### Phase B: Run

Command:

```bash
ultrarag run <pipeline.yaml> [--param path] [--is_demo]
```

What happens:

- Loads generated server config + parameter config.
- Creates `fastmcp.Client` transport config for each server.
- Executes pipeline steps in order, including `loop` and `branch`.
- Saves intermediate memory snapshots and writes `output/memory_*.json`.
- Invokes cleanup tools (e.g., tools ending with `vllm_shutdown`) if present.

---

## 5) Pipeline DSL Reference

UltraRAG accepts mixed step forms:

### 5.1 Plain step

```yaml
- retriever.retriever_search
```

### 5.2 Step with input/output remapping

```yaml
- generation.generate:
    input:
      prompt_ls: custom_prompt_ls
    output:
      ans_ls: final_answer_ls
```

### 5.3 Loop block

```yaml
- loop:
    times: 3
    steps:
    - retriever.retriever_search
    - generation.generate
```

### 5.4 Branch block

```yaml
- branch:
    router:
    - router.route_query
    branches:
      need_retrieval:
      - retriever.retriever_search
      direct_answer:
      - generation.generate
```

### 5.5 Prompt vs Tool step semantics

- Steps under the `prompt` server call `client.get_prompt(...)`.
- Non-prompt steps call `client.call_tool(...)`.

---

## 6) Variable Resolution and Data Flow Rules

In `UltraData`, each tool input value is interpreted by convention:

- `"$foo"` -> load from server-local params (`parameter.yaml`)
- `"bar"`  -> read from global variable pool (`global_vars["bar"]`)
- `"memory_xxx"` -> read/write memory lists

Output handling:

- Tool returns JSON payload -> keys mapped to declared outputs
- Output remapping (`output:` in pipeline step) is applied at save time
- Prompt outputs usually produce `prompt_ls`

Branch handling:

- Branch-aware values use wrapped list records with branch-state keys.
- Internal sentinel (`UNSET`) is used to distinguish "not filled yet" from `None`.

Memory handling:

- The engine tracks `memory_*` histories automatically.
- Final snapshots are serialized to `output/memory_<...>.json`.
- If a memory server is detected, turn-level memory auto-save can be triggered.

---

## 7) Core Python Modules (Authoritative Guide)

### `src/ultrarag/client.py`

This is the orchestration heart of the project.

Key responsibilities:

- CLI entrypoint (`main`)
- UI launch (`launch_ui`) and case-study launch (`launch_case_study`)
- Config loading (`Configuration`)
- Pipeline data graph and state (`UltraData`)
- Build pipeline configs (`build`)
- Load runtime context (`load_pipeline_context`)
- Execute step engine (`execute_pipeline`)
- Run full pipeline (`run`)

Important runtime behaviors:

- Supports both local python MCP servers (`path.endswith(".py")`) and remote MCP endpoints (`http(s)`).
- For remote MCP, requires Node.js >= 20 and uses `npx -y mcp-remote <url>`.
- Keeps loop-termination state in `ContextVar` for coroutine safety.
- Emits structured stream events in demo/UI flows (`step_start`, `step_end`, `token`, `sources`).

### `src/ultrarag/server.py`

Defines `UltraRAG_MCP_Server`, a compatibility wrapper over FastMCP.

Key responsibilities:

- Enhanced `tool()` and `prompt()` registration with `output` metadata support
- Metadata capture for automatic config generation
- `build(parameter_file)` to generate per-server `server.yaml`
- Compatibility filtering for FastMCP signature differences

### `src/ultrarag/api.py`

Provides ergonomic Python-side wrappers:

- `initialize(servers, server_root, log_level)`
- `ToolCall.server_name.tool_name(...)`
- `PipelineCall(pipeline_file, parameter_file, log_level)`

### `src/ultrarag/mcp_logging.py`

- Initializes root logger `UltraRAG`
- Rich console logging + file logging (`logs/<timestamp>.log`)
- Log level controlled by `log_level` argument and environment

### `src/ultrarag/mcp_exceptions.py`

- Validates local Node.js availability/version
- Raises `NodeNotInstalledError` / `NodeVersionTooLowError`

### `src/ultrarag/utils.py`

- Subprocess lifecycle helpers
- POSIX parent-death signal support
- Windows job object support for child-process cleanup

---

## 8) MCP Server Authoring Contract

Each server follows this shape:

```text
servers/<name>/
├── parameter.yaml
├── server.yaml            # generated
└── src/<name>.py
```

### 8.1 Registration styles

Use either:

1. Decorator style
2. Class-bound method registration style

Both are valid in this codebase.

### 8.2 `output=` grammar

Canonical form:

```text
input1,input2,$param_a -> output1,output2
```

Rules:

- Left side maps function args to pipeline inputs.
- Right side defines expected output keys from returned dict/JSON.
- `-> None` means no output variables.
- `$param` means value comes from server parameter file.

### 8.3 Return payload expectations

- For tools: return JSON-serializable dict payloads matching declared output keys.
- For prompts: return prompt messages (typically list-like prompt payloads consumed by `get_prompt`).

### 8.4 Entrypoint requirement

Server modules should end with:

```python
if __name__ == "__main__":
    app.run(transport="stdio")
```

---

## 9) Retriever/Generation/Prompt Specific Notes

### Retriever (`servers/retriever`)

- Supports multiple retrieval modes:
  - Dense retrieval
  - BM25
  - Web search
  - Project-memory retrieval
- Index backends are pluggable via factory:
  - `faiss`
  - `milvus`
- Web search backends are pluggable via factory:
  - `exa`
  - `tavily`
  - `zhipuai`

### Generation (`servers/generation`)

- Supports generation backends including `openai`, `vllm`, and hf workflows.
- Provides explicit cleanup tool `vllm_shutdown`.
- Demo mode can use local streaming generation service.

### Prompt (`servers/prompt`)

- Uses `SandboxedEnvironment` from Jinja2 for safer rendering.
- Validates template paths to reduce traversal risk.
- Escapes string inputs before template rendering.

---

## 10) UI Backend Architecture (`ui/backend`)

### `app.py`

- Flask app factory (`create_app`)
- Serves frontend static assets
- Exposes chat/pipeline/KB/auth endpoints
- Reads optional frontend override via `ULTRARAG_FRONTEND_DIR`

### `pipeline_manager.py`

This module is large and central to UI behavior.

Major responsibilities:

- Session lifecycle and streaming chat management
- Background chat task management
- Pipeline CRUD (list/load/save/rename/delete)
- Parameter load/save/build wrappers
- Knowledge base file ingest and pipeline triggering
- Memory synchronization to per-user KB collections
- Optional server introspection via AST stub generation if `server.yaml` is missing

Notable behavior:

- Applies defensive patches to suppress noisy closed-event-loop teardown logs.
- Uses a queue bridge for async-to-sync SSE event streaming.
- Supports automatic memory-to-KB sync for pipelines that include memory components.

---

## 11) Storage Model and Paths

Default UI storage root:

- `ui/storage`

Can be overridden by:

- `ULTRARAG_UI_STORAGE_ROOT`

Key subpaths:

- `db/users.sqlite3`
- `chat_sessions/`
- `knowledge_base/raw|corpus|chunks|index`
- `memory/`
- `knowledge_base/_memory_sync`

Runtime outputs:

- `output/memory_*.json`
- `logs/*.log`

---

## 12) Environment Variables You Should Know

- `ULTRARAG_UI_STORAGE_ROOT`: override UI storage root
- `ULTRARAG_FRONTEND_DIR`: override frontend static directory
- `ULTRARAG_SESSION_TIMEOUT`: foreground chat session timeout
- `ULTRARAG_BG_SESSION_TIMEOUT`: background session timeout
- `ULTRARAG_LOG_TS`: custom timestamp seed for log file naming
- `log_level`: consumed by core logger initialization

---

## 13) Dependency Model

Install tiers from `pyproject.toml`:

- Core install: no extras
- `retriever` extra
- `generation` extra
- `evaluation` extra
- `corpus` extra
- `all` extra (union)

Typical commands:

```bash
uv sync
uv sync --extra retriever
uv sync --extra generation
uv sync --all-extras
```

Development dependencies include:

- `ruff`
- `ipython`
- `jupyter`
- `pytest`

---

## 14) CLI Commands (Canonical)

```bash
ultrarag build <pipeline.yaml>
ultrarag run <pipeline.yaml> [--param <parameter.yaml>] [--log_level info|debug|warn|error] [--is_demo]
ultrarag show ui [--host 127.0.0.1] [--port 5050]
ultrarag show case [--config_path <memory.json>] [--host 127.0.0.1] [--port 8080]
```

Minimal smoke check:

```bash
ultrarag run examples/experiments/sayhello.yaml
```

---

## 15) Docker Variants

- `Dockerfile`: full image (builds frontend, installs all extras)
- `Dockerfile.base-cpu`: CPU base image
- `Dockerfile.base-gpu`: GPU base image

All variants start UI with:

```bash
ultrarag show ui --port 5050 --host 0.0.0.0
```

---

## 16) Development Playbooks

### 16.1 Add a new MCP server

1. Create `servers/<name>/parameter.yaml`
2. Implement `servers/<name>/src/<name>.py`
3. Register tools/prompts via `app.tool` / `app.prompt` (or class-bound registration)
4. Ensure `app.run(transport="stdio")` exists
5. Add server to `servers:` in a pipeline YAML
6. Run `ultrarag build <pipeline.yaml>`

### 16.2 Add a new tool to an existing server

1. Implement function/method
2. Register with explicit `output=...` contract
3. Ensure return payload keys match outputs
4. Update relevant parameter keys in `parameter.yaml` if using `$...`
5. Add the step in pipeline YAML and rebuild

### 16.3 Add a retriever index backend

1. Implement backend in `servers/retriever/src/index_backends/`
2. Follow `BaseIndexBackend` contract
3. Register in `_INDEX_BACKENDS` map in `index_backends/__init__.py`

### 16.4 Add a web-search backend

1. Implement backend in `servers/retriever/src/websearch_backends/`
2. Follow `BaseWebSearchBackend` contract
3. Register in `_WEBSEARCH_BACKENDS` map

### 16.5 Modify UI pipeline behavior

Primary files:

- `ui/backend/app.py`
- `ui/backend/pipeline_manager.py`

If touching build/runtime semantics, cross-check against:

- `src/ultrarag/client.py`

---

## 17) Coding Standards (Repository-Conformant)

- Use type hints for function signatures.
- Prefer `pathlib.Path` for filesystem paths.
- Use `yaml.safe_load` / `yaml.safe_dump`.
- Use project logger (`get_logger` or `app.logger`) instead of `print`.
- Keep tool outputs deterministic and JSON-serializable.
- Keep imports grouped: stdlib -> third-party -> local.
- Keep async boundaries explicit (`async`/`await`).

---

## 18) What Not To Edit Blindly

Treat these as generated or runtime artifacts unless intentionally regenerating:

- `servers/*/server.yaml`
- `examples/**/parameter/*_parameter.yaml`
- `examples/**/server/*_server.yaml`
- `output/*`
- `logs/*`
- `ui/storage/*` runtime data

Also avoid committing secrets:

- `.env`
- any credential-bearing local config

---

## 19) Common Failure Modes and Fixes

### Error: server file not found

- Check `servers.<name>` path in pipeline YAML.
- Ensure server entry script exists at `servers/<name>/src/<name>.py` (or update path in config).

### Error: missing variable in pipeline execution

- Verify output key names from upstream tool match downstream input names.
- Verify remapping under step-level `output:` is correct.
- Verify `$param` keys exist in that server's parameter config.

### Remote MCP server fails to start

- Ensure Node.js >= 20.
- Confirm `npx` is available.
- Confirm remote URL in server path is reachable.

### Build succeeds but UI cannot list tools

- Ensure `server.yaml` exists or can be inferred by AST parsing.
- Check for unusual dynamic registration patterns that static analysis cannot infer.

### No final answer in chat

- Inspect `output/memory_*.json`.
- Check whether generation step ran and produced `ans_ls`.
- Check stream events in UI path (`step_start`, `step_end`, `sources`, `token`, `final`).

---

## 20) Validation Checklist for Agents

Before finalizing any non-trivial change:

1. Build the affected pipeline:
   - `ultrarag build <pipeline.yaml>`
2. Run a relevant smoke case:
   - `ultrarag run <pipeline.yaml>`
3. If UI behavior changed:
   - run `ultrarag show ui` and verify route behavior
4. If retriever/generation backends changed:
   - validate parameter schema keys and output names
5. Keep generated artifacts intentional:
   - do not accidentally commit transient runtime outputs

---

## 21) Minimal Quickstart for New Agents

```bash
# 1) Install dependencies
uv sync --all-extras

# 2) Smoke test
ultrarag run examples/experiments/sayhello.yaml

# 3) Build and run a demo pipeline
ultrarag build examples/demos/LLM.yaml
ultrarag run examples/demos/LLM.yaml

# 4) Launch UI
ultrarag show ui --host 127.0.0.1 --port 5050
```

---

## 22) Final Notes

- This repository is orchestration-first: correctness depends heavily on I/O naming consistency across tools and pipeline steps.
- Most regressions come from mismatched variable names, stale generated configs, or incomplete parameter updates.
- When in doubt, inspect `src/ultrarag/client.py` first: it is the execution truth.
