{"id":"f37d859a-1c26-47b4-9146-3f8e2b3f02b1","slug":"ragweld","kind":"mcp","name":"ragweld","summary":"The retrieval stack matters — but ragweld’s differentiator is the engineering surface area around it: benchmarking, evaluation, diagnosis, and operations. It’s built for senior engineers who need answ","owner":"DMontgomery40","repoUrl":"https://github.com/DMontgomery40/ragweld","license":"GPL-3.0","licenseTier":"strong-copyleft","trust":"community","stars":5,"match":null,"fewStars":false,"format":"mcp","createdAt":"2026-01-28T11:08:09.000Z","addedAt":"2026-10-21T16:43:08.567Z","logoUrl":"https://avatars.githubusercontent.com/u/130489651?u=3f5971dbb75903af435b69dfad27de893a0f0fe8&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["databases","communication","data-analytics"],"tags":["benchmark-framework","discord-webhooks","evaluation-metrics","gradient-descent","grafana-dashboard","graphrag","hybrid-rag","llm-training-toolkit"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-26T01:26:12.801Z","sample":false,"url":"https://conduits.dev/tools/ragweld","language":"Python","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"> **This project has been renamed to ragweld.** The internal codebase still references \"tribrid\" in many places — config keys, interfaces, variable names — and that will stay as-is. The product name, repo, and website are now **[ragweld](https://ragweld.com)**.\n\n<p align=\"center\">\n  <img src=\"web/public/ragweld-wordmark.png\" alt=\"ragweld\" width=\"320\" />\n</p>\n\n<h1 align=\"center\">ragweld</h1>\n\n<p align=\"center\">\n  <strong>One complete, self-hosted AI platform for retrieval and agent systems</strong><br />\n  Model routing, multimodal indexing and retrieval, evals, training, tracing, and operations—assembled into one opinionated workbench instead of a dozen disconnected tools.\n</p>\n\n<p align=\"center\">\n  <a href=\"https://dmontgomery40.github.io/ragweld/latest/\"><img src=\"https://img.shields.io/badge/docs-mkdocs-blue\" alt=\"Documentation\"></a>\n  <a href=\"LICENSE\"><img src=\"https://img.shields.io/badge/license-GPL--3.0--or--later-blue\" alt=\"License\"></a>\n  <a href=\"https://python.org\"><img src=\"https://img.shields.io/badge/python-3.11+-blue\" alt=\"Python\"></a>\n  <a href=\"#api-first-integration\"><img src=\"https://img.shields.io/badge/API-first-1d4ed8\" alt=\"API First\"></a>\n  <a href=\"#mcp-integration\"><img src=\"https://img.shields.io/badge/MCP-enabled-purple\" alt=\"MCP Enabled\"></a>\n</p>\n\n---\n\n## Why ragweld\n\nBuilding a serious AI stack usually means researching overlapping model gateways,\ndocument pipelines, vector and graph stores, evaluation systems, training tools,\ntrace platforms, and observability products—and then owning every integration\nbetween them. Ragweld makes those choices, wires the components together, and\nexposes the result as one platform with shared configuration, provenance, health,\nand operator workflows.\n\nIt is built for two closely related users:\n\n- **AI consultancies and technical teams serving SMBs** that need a complete,\n  repeatable platform without rebuilding the stack for every deployment.\n- **RAG and agent power users** who want the full retrieval, evaluation,\n  training, tracing, cost, and infrastructure picture without connecting every\n  subsystem themselves.\n\nThe underlying open-source systems remain inspectable and replaceable, but the\nproduct value is the integration: sane defaults, one operational surface, and a\nquality loop that connects real queries and traces to evals, training, promotion,\nand rollback.\n\n---\n\n## Mainline Status: OSS-Composition Modernization\n\nLocal `main` is the canonical development line and `origin/main` is its publication target. The OSS-composition work is a formal replacement program for the broken bespoke platform layers, not a parallel experimental branch.\n\n### Replacement-Only Rule\n\n- No fallbacks\n- No legacy compatibility shims\n- No transition-period dual paths\n- No backend-only swaps that leave operator UI stale\n- If a subsystem is replaced, the touched backend/UI/docs/tests/instructions must move to the new path together\n\n### Locked Target Stack\n\n- Inference: `vLLM`\n- Gateway/routing: `LiteLLM`\n- Orchestration: `Flyte`\n- Retrieval/indexing: `Haystack + Docling + Qdrant`\n- Graph parity: `Neo4j`\n- Training execution: `Unsloth`\n- Runs/evals/regressions: `MLflow + Ragas + Promptfoo`\n- Eval drilldown substrate: `Langfuse`\n- Observability: `OpenTelemetry + Grafana Alloy + Tempo + Loki + Mimir + Pyroscope + Faro`\n- Frontend shell/workbench target: `Dockview + react-resizable-panels + TanStack Query + assistant-ui + shadcn/ui + Radix + xterm + Monaco`\n\n### Protected Product Surfaces\n\n- Workspace shell and dock/splits experience\n- Embedded Grafana and operator-console feel\n- Training Center as a first-class in-product surface\n- Eval analysis and drilldown as a first-class in-product surface\n- Graph parity surfaces during the migration\n\nChat is explicitly **not** protected as an implementation. It should be rebuilt inside the ragweld shell on stronger OSS foundations.\n\n### What This Branch Already Replaced\n\n- Generation now has one application boundary: `Ragweld API -> LiteLLM -> vLLM`. Upstream paid credentials and routes remain private to LiteLLM; the application sees authenticated aliases only.\n- Retrieval/indexing runs on the `Docling + Haystack + Qdrant` lane: Docling extracts rich documents, every corpus's dense and sparse vectors live in a promoted Qdrant generation, Postgres keeps chunk rows as control/state, and Neo4j keeps the graph leg.\n- Online observability now has a hard-cut `OTel + Langfuse + Tempo + Alloy` control surface, canonical trace metadata, cost attribution, and workbench visibility.\n\n### What Is Still Legacy And Still Needs Replacement\n\n- Training execution still runs on the MLX-native backend: Flyte orchestrates Learning Agent runs and MLflow tracks them, but `Unsloth` needs a CUDA host.\n- Eval analysis/drilldown still needs its full `Langfuse + MLflow + Ragas/Promptfoo` substrate.\n- Chat is still hand-rolled and has not yet been rebuilt on `assistant-ui`.\n- Large parts of the repo still describe the mainline implementation below; treat the sections in this README as current-state context unless the fork status above says otherwise.\n\n## Screenshots\n\n### 1) API routing + MCP channel overrides\n\n![ragweld API-first routing panel with HTTP and MCP model override controls](assets/routing-api-mcp.png)\n\n### 2) Guardrailed indexing\n\n![ragweld indexing UI with embedding mismatch detection and guided reindex controls](assets/indexing-guardrails.png)\n\n### 3) Learning Agent Studio run HUD\n\n![ragweld Learning Agent Studio with run HUD and training timeline panels](assets/learning-agent-run-hud.png)\n\n### 4) Graph Explorer workbench\n\n![ragweld Graph Explorer with corpus stats, entities list, and relationship visualization](assets/graph-explorer.png)\n\n### 5) Recall gates + memory policy\n\n![ragweld chat recall gating controls with intensity and recency settings](assets/recall-gating.png)\n\n### 6) Infrastructure services panel\n\n![ragweld infrastructure services panel with runtime and container controls](assets/infrastructure-services.png)\n\n### 7) Dock chooser for workspace composition\n\n![ragweld dock chooser dialog for selecting workspace tabs and diagnostics](assets/dock-chooser.png)\n\n## The Problem\n\nSingle-method RAG systems fail in predictable ways:\n\n- **Vector search** misses exact identifiers, function names, and error codes\n- **Keyword/BM25 search** fails on conceptual queries and paraphrases\n- **Neither** understands code relationships—what calls what, what imports what\n\n## The Solution\n\nragweld runs **three retrieval methods in parallel**, fuses their results, and optionally reranks:\n\n```\n                                    ┌─────────────────────┐\n                                    │   Vector Search     │\n                                    │   (Qdrant)          │\n                                    │   Semantic similarity│\n                              ┌────►│   \"auth flow\" →     │────┐\n                              │     │   \"token exchange\"  │    │\n                              │     └─────────────────────┘    │\n                              │                                │\n┌──────────────┐              │     ┌─────────────────────┐    │     ┌─────────────┐     ┌──────────────┐\n│              │              │     │   Sparse Search     │    │     │             │     │              │\n│    Query     │──────────────┼────►│   (PostgreSQL FTS)  │────┼────►│   Fusion    │────►│   Reranker   │────► Results\n│              │              │     │   BM25 ranking      │    │     │   (RRF /    │     │   (optional) │\n└──────────────┘              │     │   Exact matches     │    │     │   Weighted) │     │              │\n                              │     └─────────────────────┘    │     └─────────────┘     └──────────────┘\n                              │                                │\n                              │     ┌─────────────────────┐    │\n                              │     │   Graph Search      │    │\n                              └────►│   (Neo4j)           │────┘\n                                    │   Entity traversal  │\n                                    │   \"what calls this?\"│\n                                    └─────────────────────┘\n```\n\nEach search method compensates for the others' weaknesses. The result: **dramatically better recall** across query types.\n\n---\n\n## Features\n\n### Positioning, Honestly\n- **Versioned source-of-truth config**: Pydantic-backed config is the law, with generated types and UI/API surfaces derived from it\n- **Versioned prompt behavior**: system prompts and config-driven runtime behavior are explicit, reviewable repo artifacts\n- **Quality-loop lineage bundles**: benchmark, eval, synthetic, training, and promotion flows attach immutable versions into corpus-scoped bundles with operator aliases\n- **Executable specs**: machine-checkable intent lives in `spec/` and is meant to be updated with product behavior\n- **Manifest-backed training artifacts**: training and promotion flows are anchored in explicit artifacts and manifests, not hand-waved state\n- **Repo-local system of record**: agent/operator knowledge stays versioned in-repo under `docs/`\n- **Provenance-minded workflows**: eval, training, tracing, and synthetic flows are designed for traceability, without claiming full end-to-end DSV governance\n\n### MLOps Engineering Platform\n- **Synthetic Data Lab**: recipe-based generation for eval datasets, semantic cards, keywords, triplets, and autotune patches\n- **Dual Training Studios**: trainable learning reranker and trainable in-product agent model, with checkpoints, promote/rollback, and run telemetry\n- **Evaluation Workbench**: run management, drilldowns, and diffs across retrieval, indexing, model, and routing changes\n- **Tracing + Observability**: branch target is `OTel + Langfuse + Tempo + Alloy + Grafana`; the online request slice is already wired end-to-end with in-product status, trace deep links, and cost attribution\n- **Semantic Cache + Recall Gates**: token-cost control and smart memory retrieval policy in the chat path\n- **Gateway Aliases + Model Catalog**: authenticated LiteLLM runtime aliases alongside a separate pricing/candidate catalog\n\n### Tri-Brid Retrieval\n- **Vector Search**: dense chunk vectors in Qdrant (HNSW, cosine), one promoted generation per corpus\n- **Sparse Search**: IDF-modified BM25 sparse vectors (fastembed `Qdrant/bm25`) stored in the same Qdrant generation\n- **Graph Search**: Neo4j knowledge graph with entity extraction and relationship traversal\n- **Fusion**: Reciprocal Rank Fusion (RRF) or configurable weighted scoring\n- **Reranking**: Cloud APIs (Cohere/Voyage/Jina) or Qwen3 LoRA learning rerankers (MLX)\n\n### Full-Stack Application\n- **Backend**: FastAPI with async support, comprehensive API\n- **Frontend**: React + TypeScript + Zustand, fully typed from Pydantic\n- **Configuration**: 500+ tunable parameters, all via UI or API\n- **Observability**: canonical request tracing now centers on OpenTelemetry, Langfuse, Tempo, Alloy, Grafana, and the workbench trace cache bridge\n\n### API-first Integration\n- **Primary contract**: FastAPI endpoints under `/api/*` are the canonical production interface\n- **Channel aliases**: optional LiteLLM model aliases for API, chat, CLI, and MCP channels through one gateway\n- **Ops surface**: indexing, retrieval, evals, training, tracing, and observability are all API-addressable\n\n### MCP Integration (Model Context Protocol)\n- **Embedded MCP Server**: Streamable HTTP transport mounted at `/mcp`\n- **Three MCP Tools**: `search`, `answer`, `list_corpora`\n- **Claude Desktop / IDE Ready**: Connect any MCP-compatible client directly to ragweld\n- **Stateless HTTP Mode**: No session management required (recommended for most use cases)\n\n### Knowledge Graph\n- Automatic entity extraction (functions, classes, modules, variables)\n- Relationship mapping (calls, imports, inherits, contains, references)\n- Community detection (Louvain, Label Propagation)\n- Graph inspection via **RAG → Graph** (UI), Neo4j Browser, and `/api/graph/*` endpoints\n\n### Local Tracing & Debugging\n- **Per-Request Traces**: Full request metadata for chat/search/answer, including canonical trace ids, correlation ids, route summaries, external links, and cost summaries\n- **Debug Footer**: Inline debug metadata showing confidence, fusion method, retrieval leg counts\n- **Langfuse + Tempo Deep Links**: Workbench traces can link directly into external drilldown tools when configured\n- **Loki Integration**: Stream logs directly in the Chat UI via Loki proxy endpoints\n- **Trace Cache Bridge**: Configurable local retention while the workbench migrates toward fully canonical observability APIs\n\n### Evaluation & Cost Tracking\n- Built-in evaluation framework with eval datasets\n- **Detailed Cost Breakdowns**: Per-request costs for generation, embeddings, and reranking\n- Model comparison tools with accurate pricing from `data/models.json`\n- Retrieval quality metrics (MRR, Recall@K, NDCG)\n\n---\n\n## Performance\n\nragweld measures pipeline latency and throughput via **Prometheus + Grafana** (see dashboards `tribrid-overview` and `tribrid-rag-metrics`). The benchmark runner measures complete HTTP search requests against a running Ragweld API, including application processing and network time. Warmup requests are excluded from measured latency and throughput.\n\n### Reproducible benchmark (index + search)\n\nPrereqs:\n\n```bash\n# Run on LXC100 against the already running API and a registered corpus.\nuv run python -m scripts.benchmark_perf \\\n  --api-base-url http://127.0.0.1:58012 \\\n  --corpus-id ragweld --skip-index --iterations 5 --warmup 1 \\\n  --query 'How does the indexing run fence prevent concurrent replacement?' \\\n  --out-json /var/tmp/ragweld-benchmark.json\n```\n\nTo include indexing, omit `--skip-index`. The server uses the registered corpus path unless `--corpus-path` supplies another path on that server. Indexing uses the normal run fence, validation and promotion lifecycle. `--force-reindex` permits changed embedding settings; semantic graph indexing also requires `--approved-graph-schema-hash` with the exact reviewed schema hash.\n\nThe runner prints Markdown and writes JSON when `--out-json` is supplied. `--request-timeout` bounds each HTTP request (default 60 seconds); `--index-timeout` bounds waiting for the accepted run (default 3,600 seconds). A failed, cancelled or unfinished index produces no success report. A wait timeout leaves the server-owned job running, so inspect that corpus's run before starting another benchmark.\n\n`--out-json` replaces the selected report: after input validation, the runner removes the previous report before making API requests and publishes a complete JSON file only on success. A failed rerun therefore leaves no stale metrics at that path. Concurrent invocations using the same output path are rejected without changing the active writer's report.\n\n### Latest benchmark (local dev run)\n\nHistorical internal-pipeline measurement from `2026-02-01` (vector+sparse+graph enabled, `final_k=10`); these values are not directly comparable to the current HTTP benchmark.\n\n| Operation | Performance | Notes |\n|---|---:|---|\n| Indexing | 419.0s | 497 files, 8,753 chunks, 702,647 tokens |\n| Search (tri-brid) | p50 459ms, p95 487ms | 25 calls (5 queries × 5 iters), ~1.62 QPS |\n\nPer-query (ms):\n\n| Query | p50 | p95 | mean |\n|---|---:|---:|---:|\n| authentication flow | 373.5 | 758.5 | 449.6 |\n| prometheus metrics endpoint /metrics | 462.5 | 484.9 | 466.5 |\n| neo4j graph retrieval mode | 461.6 | 487.1 | 467.5 |\n| where is /api/search implemented | 449.4 | 458.7 | 450.0 |\n| fusion rrf_k parameter | 471.0 | 483.0 | 465.9 |\n\n---\n\n## Quick Start\n\n### Branch-Specific Note\n\nTreat the mainline status section above as the architecture truth for modernization decisions. Implementation details below may still describe older subsystems that have not been fully replaced yet.\n\n### Prerequisites\n\n- Docker & Docker Compose\n- Python 3.11+ with [uv](https://github.com/astral-sh/uv)\n- Node.js 18+\n- Provider credentials for the configured cloud workflows; OpenAI embedding credentials belong to LiteLLM\n\n### 1. Clone and Configure\n\n```bash\ngit clone https://github.com/DMontgomery40/ragweld.git\ncd ragweld\ncp .env.example .env\n```\n\nThe app's `.env` configures its gateway connection with `LITELLM_BASE_URL` and\n`LITELLM_API_KEY`. OpenAI embeddings and generation call LiteLLM; their upstream\nprovider credentials belong in the gateway's private `infra/litellm.env`.\n\nFor a new install without an existing gateway environment file:\n\n```bash\ncp infra/litellm.env.example infra/litellm.env\nchmod 600 infra/litellm.env\n```\n\nEdit that private file to replace `OPENAI_API_KEY=disabled` when enabling OpenAI\nembeddings. Preserve existing gateway settings and Langfuse credentials when\nupdating an installation. Do not put `OPENAI_API_KEY` in the app's `.env` or\nexport it into the app process. Compose passes the app's `LITELLM_API_KEY` to\nthe gateway as `LITELLM_MASTER_KEY`; this gateway client credential is separate\nfrom the upstream OpenAI key.\n\nOn the Proxmox deployment, `.env` points to `/etc/ragweld/runtime.env` and\n`infra/litellm.env` points to `/etc/ragweld/litellm.env`. Update the existing\nowner-only files on LXC100 instead of replacing those symlinks. After changing\nthe gateway environment, reconcile/recreate the `litellm` service through the\ndeployment's Compose configuration during an idle interval. Restarting only the\nAPI does not reload the gateway environment. See\n[gateway credentials](docs/references/generation-gateway-catalog.md#gateway-credentials)\nand [Proxmox activation](docs/references/native-run-accounting.md#proxmox-activation).\n\n### 2. Start Infrastructure\n\n```bash\ndocker compose --project-name ragweld up -d postgres neo4j qdrant litellm\n```\n\nBy default, Docker volumes are stored alongside the repo. To store DB data **outside** the repo (recommended for real corpora),\nset `TRIBRID_DB_DIR` (wired in `docker-compose.yml`):\n\n```bash\n# Example (macOS)\nexport TRIBRID_DB_DIR=\"/Users/davidmontgomery/ragweld-db\"\ndocker compose up -d postgres neo4j\n```\n\nThis starts:\n- **PostgreSQL** for corpus control/state rows and caches (port 5432)\n- **Qdrant** vector store for dense + sparse chunk vectors (port 56333)\n- **Neo4j** graph database (ports 7474, 7687)\n- **LiteLLM** gateway for generation and OpenAI embeddings (loopback port 54000)\n\n### 2a. Start The Observability Overlay For This Branch\n\n```bash\ndocker compose -f docker-compose.yml -f infra/docker-compose.observability.yml up -d grafana tempo alloy\n```\n\nRecommended matching config values on this branch:\n\n```text\ntracing.tracing_mode=otel or otel_langfuse\ntracing.otlp_endpoint=http://localhost:54320/v1/traces\ntracing.alloy_base_url=http://localhost:52345\ntracing.tempo_base_url=http://localhost:53200\nui.grafana_base_url=http://localhost:3301\n```\n\nReference:\n- `infra/docker-compose.observability.yml`\n- `infra/alloy/config.alloy`\n- `infra/tempo.yaml`\n- `docs/references/observability-online-slice.md`\n\n### 3. Start Backend\n\n```bash\nuv sync\nuv run uvicorn server.main:app --reload --port 58012\n```\n\nNotes:\n- The backend will **auto-load** repo-root `.env` on startup (dev convenience).\n- If you change `.env` while the backend is running, you must **restart** the backend for changes to take effect.\n- `GET /api/secrets/check?keys=LITELLM_API_KEY` checks only whether the app's\n  gateway credential is present. It does not validate the key or inspect\n  `OPENAI_API_KEY` in LiteLLM; upstream provider keys are outside this endpoint.\n\nAPI available at http://localhost:58012\nOpenAPI docs at http://localhost:58012/docs\n\n### 4. Start Frontend\n\n```bash\ncd web\nnpm install\nnpm run dev\n```\n\n### One-command dev start (recommended)\n\nFrom the repo root:\n\n```bash\n./start.sh\n```\n\nWith full observability stack (Prometheus + Grafana + Loki + Promtail):\n\n```bash\n./start.sh --with-observability\n./start.sh --with-flyte            # adds the Flyte control plane for Learning Agent orchestration\n```\n\nStop only Ragweld-owned host processes and containers while preserving all data\nvolumes and the host-owned Colima VM:\n\n```bash\n./stop.sh\n```\n\nReset only the fixed `ragweld` Compose project's volumes after stopping it:\n\n```bash\n./stop.sh\n./reset-data.sh --confirm=DELETE-RAGWELD-DATA\n```\n\nThe reset command never deletes repo-local files, models, or Colima itself.\n\nObservability URLs:\n- Grafana: http://localhost:3301 (admin/admin)\n- Prometheus: http://localhost:59090\n- Loki: http://localhost:53100\n\nUI available at http://localhost:55173\n\n### 5. Index Your First Corpus\n\nVia UI: Use the corpus switcher in the top navigation.\n\nVia API:\n```bash\n# Create a corpus\ncurl -X POST \"http://localhost:58012/api/repos\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"my-project\",\n    \"path\": \"/absolute/path/to/your/code\"\n  }'\n\n# Start indexing\ncurl -X POST \"http://localhost:58012/api/index\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"repo_id\": \"my-project\", \"repo_path\": \"/path/to/my-project\", \"force_reindex\": false}'\n\n# Check progress\ncurl \"http://localhost:58012/api/index/my-project/status\"\n```\n\n### 6. Search\n\n```bash\ncurl -X POST \"http://localhost:58012/api/search\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"query\": \"How does the authentication system work?\",\n    \"corpus_id\": \"my-project\",\n    \"top_k\": 10\n  }'\n```\n\n---\n\n## MCP Integration\n\nragweld includes a built-in **Model Context Protocol (MCP)** server, allowing any MCP-compatible client (Claude Desktop, Cursor, VS Code extensions, custom agents) to use tri-brid retrieval directly.\n\n### MCP Tools Available\n\n| Tool | Description |\n|------|-------------|\n| `search` | Tri-brid search (vector + sparse + graph) returning ranked chunks |\n| `answer` | RAG-powered answer generation with citations |\n| `list_corpora` | List all available corpora for searching |\n\n### Connecting Claude Desktop\n\nAdd to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"tribrid-rag\": {\n      \"url\": \"http://localhost:58012/mcp/\"\n    }\n  }\n}\n```\n\nRestart Claude Desktop. You can now ask Claude to search your indexed codebases.\n\n### MCP Configuration\n\nMCP settings are in `tribrid_config.json` under the `mcp` section:\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `enabled` | `true` | Enable/disable the MCP server |\n| `mount_path` | `/mcp` | HTTP endpoint path |\n| `stateless_http` | `true` | Stateless mode (recommended) |\n| `json_response` | `true` | JSON responses (recommended) |\n| `require_api_key` | `false` | Require Bearer token auth |\n| `default_top_k` | `20` | Default result count |\n| `default_mode` | `tribrid` | Default retrieval mode |\n\n### MCP Retrieval Modes\n\n| Mode | Vector | Sparse | Graph | Use Case |\n|------|--------|--------|-------|----------|\n| `tribrid` | ✓ | ✓ | ✓ | Best recall (default) |\n| `dense_only` | ✓ | - | - | Semantic queries only |\n| `sparse_only` | - | ✓ | - | Exact keyword matching |\n| `graph_only` | - | - | ✓ | Relationship traversal |\n\n### Check MCP Status\n\n```bash\ncurl http://localhost:58012/api/mcp/status\n```\n\n---\n\n## Local Tracing\n\nragweld captures detailed per-request traces for debugging and development. Traces are stored in an in-memory ring buffer (no external dependencies).\n\n### Trace Features\n\n- **Automatic Capture**: Every chat/search request generates a trace\n- **Event Timeline**: Request → Retrieval → Fusion → Response events with timestamps\n- **Debug Footer**: Inline metadata on every chat response showing:\n  - Confidence score (heuristic based on fusion method)\n  - Active retrieval legs (vector/sparse/graph)\n  - Fusion method and parameters (RRF k or weighted weights)\n  - Result counts per leg\n  - Run ID for correlation\n\n### API Endpoints\n\n| Endpoint | Description |\n|----------|-------------|\n| `GET /api/traces/latest` | Get the most recent trace |\n| `GET /api/traces/latest?run_id=...` | Get a specific trace by run ID |\n| `GET /api/traces/latest?repo=...` | Get latest trace for a corpus |\n\n### Configuration\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `tracing.tracing_enabled` | `1` | Enable trace capture |\n| `tracing.tracing_mode` | `local` | Trace mode (`local`, `otel`, `otel_langfuse`, `off`) |\n| `tracing.otel_export_enabled` | `1` | Enable canonical OTLP trace export |\n| `tracing.otlp_endpoint` | `\"\"` | OTLP HTTP endpoint for OTel export |\n| `tracing.otlp_headers` | `\"\"` | Comma-separated OTLP headers (`k=v`) |\n| `tracing.otel_service_name` | `ragweld-api` | Service identity attached to emitted spans |\n| `tracing.langfuse_enabled` | `0` | Enable Langfuse generation observations |\n| `tracing.langfuse_base_url` | `\"\"` | Langfuse deployment base URL |\n| `tracing.langfuse_project` | `ragweld` | Langfuse project label |\n| `tracing.tempo_base_url` | `\"\"` | Tempo or Grafana Explore base URL for trace deep links |\n| `tracing.alloy_base_url` | `\"\"` | Grafana Alloy collector base URL for readiness/status checks |\n| `tracing.cost_tracking_enabled` | `1` | Enable online request cost attribution in traces |\n| `tracing.trace_retention` | `50` | Max traces per corpus (ring buffer) |\n| `tracing.trace_sampling_rate` | `1.0` | Sampling rate (1.0 = capture all) |\n| `ui.chat_show_debug_footer` | `1` | Show debug footer under chat answers |\n\n---\n\n## Loki Log Integration\n\nWhen running with the observability stack, ragweld proxies Loki queries for unified log viewing in the Chat UI.\n\n### Loki Proxy Endpoints\n\n| Endpoint | Description |\n|----------|-------------|\n| `GET /api/loki/status` | Check Loki reachability |\n| `GET /api/stream/loki/tail` | SSE stream of live logs |\n\n### Chat UI Integration\n\nThe Chat tab includes a **Trace & Logs** panel that:\n- Shows the trace timeline for the current/selected run\n- Streams live logs from Loki filtered by service\n- Correlates logs with trace events by timestamp\n\n---\n\n## Operator Runbook (Real Corpus GraphRAG)\n\nThis section is optimized for running a **real on-disk corpus** end-to-end (Postgres + Neo4j), indexing it safely (gitignore-aware + size skips), and verifying graph retrieval is actually contributing results.\n\n### TL;DR (10–15 line runbook)\n\n```bash\n# Start the fixed Ragweld Compose project plus the host API/UI.\n./start.sh\n\n# Create a corpus from any real document directory, then exclude heavy build outputs.\ncurl -sS -X POST http://127.0.0.1:58012/api/repos -H 'Content-Type: application/json' -d '{\"corpus_id\":\"example-corpus\",\"name\":\"Example corpus\",\"path\":\"/path/to/documents\"}'\ncurl -sS -X PATCH http://127.0.0.1:58012/api/repos/example-corpus -H 'Content-Type: application/json' -d '{\"exclude_paths\":[\".worktrees/\",\"out.noindex/\",\"data/qdrant/\",\"node_modules/\",\"dist/\"]}'\n\n# Index + poll\ncurl -sS -X POST http://127.0.0.1:58012/api/index -H 'Content-Type: application/json' -d '{\"repo_id\":\"example-corpus\",\"repo_path\":\"/path/to/documents\",\"force_reindex\":true}'\nwhile true; do curl -sS http://127.0.0.1:58012/api/index/example-corpus/status | python -m json.tool; sleep 2; done\n```\n\n### Verify GraphRAG is actually contributing results\n\n```bash\ncurl -sS -X POST http://127.0.0.1:58012/api/search \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"corpus_id\":\"example-corpus\",\"query\":\"How does the retrieval pipeline work?\",\"top_k\":8,\"include_vector\":true,\"include_sparse\":true,\"include_graph\":true}' \\\n  | python -m json.tool\n```\n\nSuccess criteria:\n- `debug.fusion_graph_attempted` is `true`\n- `debug.fusion_graph_error` is `null`\n- `debug.fusion_graph_hydrated_chunks > 0`\n- At least one match has `\"source\": \"graph\"`\n\n### How file inclusion works (gitignore + safety/perf)\n\n- **Gitignore semantics**: indexing uses a gitignore-aware loader that supports **nested `.gitignore` files**.\n- **Always-ignored** (even if corpus has no `.gitignore`): `.git/`, `.venv/`, `node_modules/`, `__pycache__/`, `*.pyc`, `.DS_Store`, `.env`.\n- **Size skip (LAW)**: files larger than `min(chunking.max_indexable_file_size, indexing.index_max_file_size_mb*1024*1024)` are skipped *before* reading/chunking.\n- **Corpus-level excludes**: set `exclude_paths` on the corpus (stored in Postgres `corpora.meta`) to skip large build outputs that are not gitignored. You can set this via **UI** (`RAG → Indexing → Corpus settings`) or **API** (`PATCH /api/corpora/{corpus_id}`).\n\n### Troubleshooting\n\n- **Neo4j health in `docker compose ps` may say \"starting\" forever**: the Neo4j container healthcheck uses `curl`, but the Neo4j image doesn't ship curl. Use cypher-shell instead:\n\n```bash\ndocker compose --project-name ragweld exec neo4j cypher-shell -u neo4j -p password 'RETURN 1 AS ok;'\n```\n\n- **Neo4j \"critical error needs restart\"**:\n  - Inspect the scoped service logs with `docker compose --project-name ragweld logs neo4j`.\n  - Use `./stop.sh` for a non-destructive stop. Only use the separately confirmed `reset-data.sh` workflow when the named project volumes should be discarded.\n\n- **Postgres startup failure**:\n  - Inspect `docker compose --project-name ragweld logs postgres`; do not change permissions or remove volumes without identifying the failing path first.\n\n### Graph inspection (UI + Neo4j Browser)\n\n- **UI (recommended)**: `RAG → Graph` shows graph stats, communities, entity search, and entity neighborhood subgraphs.\n  - Endpoints: `/api/graph/{corpus_id}/stats`, `/api/graph/{corpus_id}/entities`, `/api/graph/{corpus_id}/entity/neighbors?entity_id=`, `/api/graph/{corpus_id}/communities`, `/api/graph/{corpus_id}/community/{community_id}/members`\n- **Neo4j Browser**: http://localhost:7474 (neo4j/password)\n- Useful Cypher examples:\n\n```cypher\nMATCH (c:Chunk {repo_id:\"example-corpus\"}) RETURN count(c);\nMATCH (d:Document {repo_id:\"example-corpus\"}) RETURN count(d);\nMATCH (e:Entity {repo_id:\"example-corpus\"}) RETURN e.entity_type, count(*) ORDER BY count(*) DESC;\nMATCH (e:Entity {repo_id:\"example-corpus\"})-[:IN_CHUNK]->(c:Chunk {repo_id:\"example-corpus\"}) RETURN e.name, c.file_path, c.chunk_id LIMIT 25;\nSHOW INDEXES YIELD name, type, state WHERE type=\"VECTOR\" RETURN name, state;\n```\n\n### UI notes (current state)\n\n- **Indexing UI**: `RAG → Indexing` is fully functional and streams progress via SSE (`/api/stream/operations/index?corpus_id=...`). Long indexing runs won't hang if the UI isn't connected.\n- **Corpus settings**: `RAG → Indexing → Corpus settings` lets you edit `exclude_paths` (and other corpus metadata) used by indexing.\n- **Retrieval UI**: `RAG → Retrieval` exposes graph retrieval toggles (e.g., `graph_search.enabled`, `graph_search.mode`, `graph_search.max_hops`, `graph_search.chunk_neighbor_window`).\n- **Graph UI**: `RAG → Graph` lets you browse communities, search entities, and load neighbor subgraphs (powered by `/api/graph/*`).\n- **MCP UI**: `Infrastructure → MCP` shows transport status and connection info.\n\n---\n\n## Architecture\n\n### Schema and Type Boundaries\n\nPydantic validates public API payloads, persisted operator configuration, and\nuntrusted external or cross-process data. Registered public wire schemas feed\nTypeScript generation; internal domain types and local UI state/view models stay\nwith their owning modules.\n\nPublic frontend wire types are **generated** and never hand-copied. Explicit,\ntyped, tested transformations are allowed at semantic boundaries. Compatibility\nfallbacks, shape guessing, dual contracts, and competing transport truths are not.\n\n```\nregistered Pydantic schemas ──► generate_types.py ──► web/src/types/generated.ts\n      (API + config)                 (script)               (wire types)\n         │                                                    │\n         ▼                                                    ▼\n    FastAPI uses                                      React components use\n    these types                                       these types\n```\n\n### Directory Structure\n\n```\nragweld/\n├── server/                     # Python FastAPI backend\n│   ├── api/                    # REST endpoints\n│   │   ├── search.py           # /api/search - tri-brid retrieval\n│   │   ├── chat.py             # /api/chat - conversational RAG + tracing\n│   │   ├── index.py            # /api/index - corpus indexing\n│   │   ├── repos.py            # /api/repos - corpus management\n│   │   ├── config.py           # /api/config - configuration + MCP status\n│   │   ├── graph.py            # /api/graph - knowledge graph queries\n│   │   ├── docker.py           # Docker, local runtime status, and Loki diagnostics\n│   │   ├── cost.py             # /api/cost - detailed cost estimation\n│   │   ├── eval.py             # /api/eval - evaluation framework\n│   │   └── health.py           # /api/health - service health\n│   ├── mcp/                    # MCP server implementation\n│   │   ├── server.py           # FastMCP server singleton\n│   │   └── tools.py            # MCP tool implementations\n│   ├── db/\n│   │   ├── postgres.py         # corpus registry, chunk rows, summaries, caches\n│   │   └── neo4j.py            # Graph database operations\n│   ├── indexing/\n│   │   ├── chunker.py          # Code-aware chunking\n│   │   ├── embedder.py         # Embedding generation\n│   │   ├── graph_builder.py    # Entity/relationship extraction\n│   │   └── loader.py           # File loading with gitignore support\n│   ├── models/\n│   │   └── tribrid_config_model.py  # THE source of truth (~500+ fields)\n│   ├── retrieval/\n│   │   ├── vector.py           # Dense retrieval\n│   │   ├── sparse.py           # BM25/FTS retrieval\n│   │   ├── graph.py            # Graph traversal\n│   │   ├── fusion.py           # RRF and weighted fusion\n│   │   └── rerank.py           # Reranking (local, cloud, learning)\n│   └── services/\n│       ├── rag.py              # RAG orchestration + ChatDebugInfo\n│       ├── traces.py           # Local trace store (ring buffer)\n│       └── config_store.py     # Configuration persistence\n│\n├── web/                        # React TypeScript frontend\n│   └── src/\n│       ├── components/         # UI components\n│       │   ├── Dashboard/      # System status, metrics\n│       │   ├── RAG/            # Search, config panels\n│       │   ├── Chat/           # Conversational interface + debug footer\n│       │   └── Infrastructure/ # Service management, MCP status\n│       ├── stores/             # Zustand state management\n│       ├── hooks/              # React hooks\n│       ├── types/\n│       │   └── generated.ts    # Auto-generated from Pydantic\n│       └── api/                # API client\n│\n├── data/\n│   ├── models.json             # LLM/embedding model definitions + pricing\n│   └── glossary.json           # UI tooltip definitions (~250 terms)\n│\n├── infra/                      # Docker and deployment configs\n├── scripts/                    # Development and maintenance scripts\n├── tests/                      # Test suite\n└── mkdocs/                     # Documentation site\n```\n\n---\n\n## Configuration\n\nragweld is highly configurable. Every parameter is defined in Pydantic with validation, defaults, and descriptions.\n\n### Key Configuration Sections\n\n| Section | What it controls |\n|---------|------------------|\n| `retrieval` | Top-K per leg, BM25 parameters, query expansion, confidence thresholds |\n| `fusion` | Method (RRF/weighted), per-leg weights, normalization |\n| `graph_storage` | Max hops, entity types, relationship types, community detection |\n| `reranking` | Mode (none/local/cloud/trained), model selection, top-N |\n| `embedding` | Provider, model, dimensions, batch size |\n| `chunking` | Strategy, max tokens, overlap |\n| `indexing` | Postgres config, concurrent workers |\n| `tracing` | Local trace capture, sampling rate, retention |\n| `mcp` | MCP server enable, mount path, defaults |\n| `ui` | Debug footer visibility, streaming, history limits |\n\n### Configuration Methods\n\n1. **UI**: RAG tab in the web interface\n2. **API**: `GET/PUT /api/config`\n3. **File**: `tribrid_config.json`\n4. **Per-corpus**: `?repo_id=...` parameter for corpus-specific settings\n\n### Example: Adjust Fusion Weights\n\n```bash\ncurl -X PUT \"http://localhost:58012/api/config\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"fusion\": {\n      \"vector_weight\": 0.5,\n      \"sparse_weight\": 0.3,\n      \"graph_weight\": 0.2\n    }\n  }'\n```\n\n---\n\n## API Reference\n\n### Search & Retrieval\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/search` | POST | Tri-brid search with fusion and optional reranking |\n| `/api/answer` | POST | RAG-powered answer generation with citations |\n| `/api/chat` | POST | Conversational RAG with history and tracing |\n| `/api/chat/stream` | POST | Streaming chat with SSE |\n\n### Corpus Management\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/repos` | GET | List all corpora |\n| `/api/repos` | POST | Create a new corpus |\n| `/api/repos/{id}` | GET | Get corpus details |\n| `/api/repos/{id}` | PATCH | Update corpus settings |\n| `/api/repos/{id}` | DELETE | Delete a corpus |\n\n### Indexing\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/index/start` | POST | Start indexing a corpus |\n| `/api/index/{id}/status` | GET | Get indexing progress |\n| `/api/index/{id}/stats` | GET | Get index statistics |\n\n### Knowledge Graph\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/graph/{id}/entities` | GET | List entities in the graph |\n| `/api/graph/{id}/communities` | GET | List detected communities |\n| `/api/graph/{id}/stats` | GET | Graph stats (entity/rel/community counts) |\n| `/api/graph/{id}/entity?entity_id=` | GET | Fetch a single entity |\n| `/api/graph/{id}/entity/relationships?entity_id=` | GET | Outgoing relationships of one entity (404 when the corpus has no promoted graph) |\n| `/api/graph/{id}/entity/neighbors?entity_id=` | GET | Entity neighborhood subgraph |\n| `/api/graph/{id}/query` | POST | Read-only Cypher query (debug) |\n\n### Tracing & Debugging\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/traces/latest` | GET | Get latest trace (optional `run_id` or `repo` filter) |\n| `/api/loki/status` | GET | Check Loki reachability |\n| `/api/stream/loki/tail` | GET | SSE stream of live logs |\n\n### MCP\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/mcp/status` | GET | MCP transport status |\n| `/api/mcp/rag_search` | GET | Debug endpoint for quick tri-brid search |\n| `/mcp/` | POST | MCP Streamable HTTP endpoint (for MCP clients) |\n\n### Configuration\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/config` | GET | Get current configuration |\n| `/api/config` | PUT | Update configuration |\n| `/api/models` | GET | List available models |\n\n### Cost Estimation\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/cost/estimate` | POST | Detailed cost breakdown (gen + embed + rerank) |\n| `/api/cost/history` | GET | Historical cost records |\n| `/api/cost/summary` | GET | Cost summary by period |\n\n### Evaluation\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/eval/run` | POST | Run evaluation suite |\n| `/api/eval/results` | GET | Get evaluation results |\n\nFull OpenAPI documentation: http://localhost:58012/docs\n\n---\n\n## Development\n\n### Commands\n\n```bash\n# Install dependencies\nuv sync\n\n# Run tests\nuv run pytest\n\n# Type checking\nuv run mypy server\n\n# Linting\npython3 scripts/jev_lint.py\n\n# Inspect semantic lint scope\npython3 scripts/jev_lint.py --dry-run # inspect semantic lint scope\n\n# Regenerate TypeScript types (after changing Pydantic models)\nuv run python scripts/generate_types.py\n\n# Validate type sync\nuv run python scripts/validate_types.py\n```\n\n### Adding a New Feature\n\n1. **Add to Pydantic first**: Define the field in `tribrid_config_model.py`\n2. **Regenerate types**: Run `scripts/generate_types.py`\n3. **Implement backend**: Use the new field in your Python code\n4. **Implement frontend**: Use the generated TypeScript type\n5. **Add tests**: Both Python and Playwright\n\n### Testing\n\n```bash\n# Unit tests\nuv run pytest tests/unit\n\n# API tests\nuv run pytest tests/api\n\n# Integration tests (requires running services)\nuv run pytest tests/integration\n\n# Frontend type and production-build checks\nnpm --prefix web run lint\nnpm --prefix web run build\n\n# E2E tests\nweb/node_modules/.bin/playwright test --config playwright.config.ts\n```\n\n---\n\n## Observability\n\n### Endpoints\n\n| Service | URL | Credentials |\n|---------|-----|-------------|\n| API Docs | http://localhost:58012/docs | - |\n| Grafana | http://localhost:3301 | admin/admin |\n| Prometheus | http://localhost:59090 | - |\n| Loki | http://localhost:53100 | - |\n| Neo4j Browser | http://localhost:7474 | neo4j/password |\n\n### Health Check\n\n```bash\ncurl http://localhost:58012/api/health\n```\n\nReturns dependency-free process liveness. Use readiness for live store status:\n\n```bash\ncurl http://localhost:58012/api/ready\n```\n\n### Metrics\n\n```bash\ncurl http://localhost:58012/metrics\n```\n\nPrometheus-format metrics for retrieval latency, throughput, error rates.\n\n---\n\n## Embedding Providers\n\n| Provider | Models | Notes |\n|----------|--------|-------|\n| OpenAI | `text-embedding-3-small`, `text-embedding-3-large` | Best general-purpose |\n| Voyage | `voyage-code-2`, `voyage-large-2` | Optimized for code |\n| Local | Any sentence-transformers model | No API costs |\n\nConfigure in `.env` or via the UI.\n\n---\n\n## Reranking Options\n\n| Mode | Model | Notes |\n|------|-------|-------|\n| `none` | - | Fastest, use fusion scores only |\n| `local` | `ms-marco-MiniLM-L-6-v2` | Good balance of speed/quality |\n| `cloud` | Cohere, Voyage, Jina | Best quality, API costs |\n| `trained` | Your fine-tuned model | Custom domain adaptation |\n\n---\n\n## Cost Estimation\n\nThe `/api/cost/estimate` endpoint provides detailed breakdowns:\n\n```bash\ncurl -X POST \"http://localhost:58012/api/cost/estimate\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"gen_provider\": \"openai\",\n    \"gen_model\": \"openai.gpt-6-luna\",\n    \"tokens_in\": 2000,\n    \"tokens_out\": 500,\n    \"embed_provider\": \"openai\",\n    \"embed_model\": \"text-embedding-3-small\",\n    \"embeds\": 10,\n    \"requests_per_day\": 100\n  }'\n```\n\nResponse includes:\n- **Per-request cost**: Generation + embedding + reranking\n- **Daily/monthly projections**: Based on requests_per_day\n- **Detailed breakdown**: Costs per component with model pricing info\n- **Validation errors**: Missing model definitions or pricing data\n\n---\n\n## Documentation\n\nFull documentation: https://dmontgomery40.github.io/ragweld/\n\nBuild locally:\n```bash\nmkdocs serve\n```\n\n---\n\n## Contributing\n\n1. Fork the repository\n2. Create a feature branch\n3. Make your changes (Pydantic first!)\n4. Run tests and type checking\n5. Submit a pull request\n\n---\n\n## License\n\nGPL-3.0-or-later. See [LICENSE](LICENSE) for details.\n\n---\n\n<p align=\"center\">\n  <strong>Three search engines. One answer.</strong>\n</p>\n"}