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
20 changes: 20 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.venv
__pycache__
*.py[oc]
*.egg-info
build
dist

.git
.github

data
.env
.DS_Store

.pytest_cache
.mypy_cache
.ruff_cache

docs
tests
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copy to .env and fill in. Exactly one LLM key is required — the retrieval
# stack (embeddings + FAISS) needs no key at all (see docs/adr/0003).

RAGMESH_LLM_PROVIDER=anthropic
RAGMESH_LLM_MODEL=claude-sonnet-4-5-20250929

ANTHROPIC_API_KEY=
# OPENAI_API_KEY=
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"

- name: Set up Python
run: uv python install 3.12

- name: Install dependencies
run: uv sync --all-extras

- name: Ruff
run: uv run ruff check .

- name: Mypy
run: uv run mypy src

- name: Unit tests
run: uv run pytest tests/unit -v

# Integration tier (real docker compose + a live LLM key) is intentionally
# not run here — see project-docs/architecture-rationale.md #11. Run it manually
# before tagging a release: `uv run pytest tests/integration -m slow`.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,14 @@ wheels/

# Virtual environments
.venv

# Personal planning docs, not for the public repo
/docs/

# Secrets and generated artifacts
.env
data/
.DS_Store
.pytest_cache/
.mypy_cache/
.ruff_cache/
74 changes: 73 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,75 @@
# ragmesh

Retrieval as a mesh of MCP services — an agentic RAG platform built with LangGraph. Provider-agnostic (OpenAI / Anthropic / Bedrock), eval-gated, production-shaped.
[![CI](https://github.com/BaliDataMan/ragmesh/actions/workflows/ci.yml/badge.svg)](https://github.com/BaliDataMan/ragmesh/actions/workflows/ci.yml)
![Python 3.12](https://img.shields.io/badge/python-3.12-blue)
![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-informational)

Retrieval as a mesh of MCP services — an agentic RAG platform built on
LangGraph. A single ReAct-style agent talks to a real MCP retrieval server
over the network (not an in-process function call), backed by a local FAISS
vector store. Provider-agnostic (Anthropic / OpenAI / Bedrock).

The sample corpus is this repo's own documentation (`README.md` + `project-docs/adr/`)
— ask it how its own retrieval pipeline works.

## Architecture

```mermaid
flowchart LR
User -->|POST /chat| API[FastAPI agent-api]
API --> Agent[LangGraph agent using create_agent]
Agent -->|MCP over streamable-http| MCP[FastMCP retrieval server]
MCP --> FAISS[FAISS index of local embeddings]
Docs[README and ADR docs] -->|embedded at image build time| FAISS
```

`agent-api` and `mcp-server` are separate containers on a Docker network — the
retrieval tool is a real network service, not a decorated Python function.
See `project-docs/architecture-rationale.md` for the full reasoning behind every
structural choice, and `project-docs/adr/` for the three closest judgment calls
(MCP transport, MCP server library, embeddings/vector store).

## Quickstart

```bash
cp .env.example .env # fill in ANTHROPIC_API_KEY (or OPENAI_API_KEY)
docker compose up --build
curl -X POST localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{"question": "What MCP transport does ragmesh use, and why?"}'
```

That's the only secret required — retrieval (embeddings + FAISS) needs no
API key at all (see `project-docs/adr/0003`).

## Local development

```bash
uv sync --all-extras
uv run pytest tests/unit -v # hermetic: no network, no model downloads
uv run ruff check .
uv run mypy src
```

Building the FAISS index locally (outside Docker):

```bash
uv run python -m ragmesh.ingest
uv run ragmesh "What transport does ragmesh's MCP server use?"
```

## Project layout

```
src/ragmesh/
config.py, llm.py # env-driven settings, provider-agnostic chat model
ingest.py, retrieval.py # build + query the FAISS index
mcp_server/server.py # FastMCP retrieval server (streamable-http)
agent.py # MCP client + LangGraph agent, build-once/cache
api.py, cli.py # FastAPI POST /chat, thin CLI
project-docs/
adr/ # architecture decision records
architecture-rationale.md, developer-guide.md
tests/unit/ # hermetic, fake models/embeddings
tests/integration/ # opt-in: real docker compose + live LLM key
```
26 changes: 26 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
services:
mcp-server:
build:
context: .
dockerfile: docker/mcp-server.Dockerfile
healthcheck:
test: ["CMD", "python", "-c", "import socket; socket.create_connection(('localhost', 8000), timeout=2)"]
interval: 5s
timeout: 3s
retries: 10

agent-api:
build:
context: .
dockerfile: docker/agent.Dockerfile
ports:
- "8080:8080"
environment:
RAGMESH_LLM_PROVIDER: ${RAGMESH_LLM_PROVIDER:-anthropic}
RAGMESH_LLM_MODEL: ${RAGMESH_LLM_MODEL:-claude-sonnet-4-5-20250929}
RAGMESH_MCP_SERVER_URL: http://mcp-server:8000/mcp
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
depends_on:
mcp-server:
condition: service_healthy
13 changes: 13 additions & 0 deletions docker/agent.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM python:3.12-slim

WORKDIR /app

RUN pip install --no-cache-dir uv

COPY pyproject.toml uv.lock README.md ./
COPY src/ src/
RUN uv sync --frozen --no-dev --extra anthropic --extra openai

EXPOSE 8080

CMD ["uv", "run", "--no-dev", "uvicorn", "ragmesh.api:app", "--host", "0.0.0.0", "--port", "8080"]
17 changes: 17 additions & 0 deletions docker/mcp-server.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM python:3.12-slim

WORKDIR /app

RUN pip install --no-cache-dir uv

COPY pyproject.toml uv.lock README.md ./
COPY src/ src/
RUN uv sync --frozen --no-dev

COPY project-docs/ project-docs/

RUN uv run --no-dev python -m ragmesh.ingest

EXPOSE 8000

CMD ["uv", "run", "--no-dev", "python", "-m", "ragmesh.mcp_server.server"]
32 changes: 32 additions & 0 deletions project-docs/adr/0001-mcp-transport-streamable-http.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ADR-0001: MCP transport is streamable-http, not stdio or SSE

## Status
Accepted

## Context
The retrieval tool is served by a real MCP server running in its own container
(see the mesh architecture this repo is named for), separate from the agent
process. MCP supports three transports: stdio, SSE, and streamable-http.

## Decision
Use streamable-http.

## Rationale
- **stdio** requires client and server to share a process tree (pipes over
stdin/stdout). It cannot cross the Docker network boundary between the
`agent-api` and `mcp-server` containers, so it's not an option here — not a
judgment call, a hard constraint.
- **SSE** is the older two-endpoint HTTP transport from MCP's original spec.
The 2025-03-26 spec revision superseded it with streamable-http, a single
endpoint that supports both request/response and streaming.
- **streamable-http** is the current spec-recommended network transport.
Shipping SSE in new code reads as not having kept up with the protocol.

## Consequences
The MCP server needs an actual HTTP listener, a port, and a health check —
more moving parts than stdio, but this is the same cost SSE would have
incurred anyway, so it isn't a trade-off against SSE, only against the
infeasible stdio option.

## Revisit when
The MCP spec changes transports again — check before assuming this still holds.
30 changes: 30 additions & 0 deletions project-docs/adr/0002-fastmcp-official-sdk-vs-jlowin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# ADR-0002: MCP server built on the official `mcp` SDK's FastMCP, not `jlowin/fastmcp`

## Status
Accepted

## Context
Two `FastMCP` implementations exist: `mcp.server.fastmcp.FastMCP`, shipped in
the official Anthropic-maintained `mcp` Python SDK, and the standalone
third-party `fastmcp` package (PrefectHQ/jlowin), which has broader ergonomics
and a convenient in-memory test `Client`.

## Decision
Use the official SDK's `FastMCP`.

## Rationale
This repo exists to demonstrate correct use of the MCP ecosystem, and "used
the standard, spec-reference implementation correctly" is a better signal for
that purpose than "found a nicer third-party wrapper." It also keeps this
codebase's testing approach aligned with `langchain-mcp-adapters`, whose own
reference examples are built against the official SDK — so the two libraries'
idioms match instead of fighting each other.

## Consequences
Possibly more boilerplate than `jlowin/fastmcp` would require. Accepted: the
signal matters more than the convenience for a portfolio repo.

## Revisit when
If the official SDK's public API churns badly across a major version — verify
the `FastMCP` class name and constructor shape against whatever version is
pinned before writing code against a new release.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# ADR-0003: Local embeddings + FAISS, not a managed vector DB or paid embeddings API

## Status
Accepted

## Context
Retrieval needs an embedding model and a vector index. Options range from a
fully managed vector database (Pinecone, Weaviate) with a paid embeddings API,
to a fully local stack running on-disk.

## Decision
Use local, no-API-key embeddings (`sentence-transformers/all-MiniLM-L6-v2` via
`langchain-huggingface`) indexed into a local FAISS store
(`langchain_community.vectorstores.FAISS`).

## Rationale
`docker compose up` should require exactly one secret — the chat LLM's API
key — not two. A second required key (for a paid embeddings API) or a managed
vector DB account raises the barrier for anyone, including an interviewer,
trying to actually run the repo. FAISS is also the right scale for a small,
static sample corpus (this repo's own docs); a managed vector DB would be
over-engineering here.

## Consequences
- FAISS-on-local-disk is not how retrieval would be done at production scale.
This is a deliberate, documented simplification, not an oversight.
- Retrieval quality from a small local embedding model is weaker than
commercial embeddings would give.
- The index is built at Docker image build time (`RUN python -m ragmesh.ingest`
after copying the source docs), not at container startup — deterministic,
fast cold starts, no race between the agent querying the MCP server and the
index finishing its build. The cost: editing `project-docs/adr/` or `README.md`
requires `docker compose up --build`, not just a restart, to take effect.

## Revisit when
A later milestone wants to demonstrate a "swap the vector store via config"
story analogous to the LLM provider swap — reasonable v0.2+ scope, not v0.1.
Loading
Loading