Skip to content
Merged
30 changes: 29 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
- uses: actions/download-artifact@v4
with:
path: artifacts
Expand All @@ -261,15 +265,37 @@ jobs:
mv artifacts/conproxy-aarch64-unknown-linux-gnu/conproxy \
artifacts/conproxy-aarch64-unknown-linux-gnu/conproxy-aarch64-unknown-linux-gnu
chmod +x artifacts/conproxy-*/conproxy-*
- name: Resolve previous tag for compare link
id: prev
run: |
# Find the most recent semver tag (excluding the current one).
PREV=$(git tag --list 'v*' --sort=-v:refname \
| grep -v "^${GITHUB_REF_NAME}$" \
| head -n 1 || true)
if [ -n "$PREV" ]; then
echo "PREV=$PREV" >> "$GITHUB_OUTPUT"
echo "COMPARE=https://github.com/${{ github.repository }}/compare/${PREV}...${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
else
echo "PREV=" >> "$GITHUB_OUTPUT"
echo "COMPARE=" >> "$GITHUB_OUTPUT"
fi
- name: Create release + attach assets
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
generate_release_notes: true
prerelease: ${{ contains(github.ref_name, '-') }}
make_latest: ${{ !contains(github.ref_name, '-') }}
body: |
## conproxy ${{ steps.v.outputs.VERSION }}

Install:
${{ steps.prev.outputs.COMPARE && format('**Compare:** [{0}…{1}]({2})', steps.prev.outputs.PREV, github.ref_name, steps.prev.outputs.COMPARE) || '**First release.**' }}

**Stability**
- **Shipped:** exact cache, MCP suite, all backends except Pinecone / Milvus
- **Experimental:** Pinecone, Milvus, peer mesh (no mTLS — see README)

**Install**

```bash
# cargo (binary)
Expand All @@ -283,6 +309,8 @@ jobs:
helm install conproxy ${{ env.HELM_OCI }}/conproxy \
--version ${{ steps.v.outputs.VERSION }}
```

**Docs:** [README](https://github.com/${{ github.repository }}#readme) · [Benchmarks](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/docs/benchmarks.md)
files: |
artifacts/conproxy-x86_64-unknown-linux-musl/conproxy-x86_64-unknown-linux-musl
artifacts/conproxy-aarch64-unknown-linux-gnu/conproxy-aarch64-unknown-linux-gnu
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

Cache proxy for heterogeneous RAG/vector search backends (Elasticsearch, OpenSearch, Qdrant, pgvector, Meilisearch, Pinecone, Milvus). Rust 2021, Axum + tonic, multi-process: lib + `conproxy` CLI + `test_runner` + `generate_embeddings` + `perf_summarize` + `hitrate_bench` + `console_snap` + `corpus_seed` + Python SDK.

## Product framing (for PR copy, README, and any external docs you write)

- **What:** retrieval-leg cache for **agentic RAG** (embed + upstream search). **Not** an LLM-answer cache.
- **Pitch:** cost + faster search on **hits**; agents re-query (retries, fanout, tool storms).
- **Not:** GPTCache / RedisVL SemanticCache territory; not "faster chat RAG" as the headline alone.
- **Proof:** `docs/benchmarks.md` + `make bench-hitrate*`; BYO with `make bench-hitrate-replay QUERIES=…`.
- **User-facing decision docs:** `README.md` (consider/skip + vs table), `docs/benchmarks.md`. This file is ops-focused.
- **Stability:** pinecone / milvus experimental; peer = trusted network, no mTLS.

## Fast Feedback Tiers

Three tiers — Tier 1 & 2 for the per-PR loop, Tier 3 for release publishing. Run the tier that matches your stage. Vertical-specific commands live in the `contributing` skill (Feature Test Matrix + 14 verticals).
Expand Down
68 changes: 65 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# conproxy

> Search retrieval cache for agentic RAG.
> Retrieval cache for agentic RAG — lower cost, faster search.

conproxy is a caching proxy for search backends. LLM caches skip re-generating answers. conproxy skips re-running the search — embed, rerank, backend — when agents ask the same thing twice.
conproxy sits in front of your search backends. LLM caches skip re-generating answers. conproxy skips re-running the search — embed, rerank, upstream — when agents hit the same (or near-same) query again.

**Why it pays**

- **Cost** — hits skip another embed call and managed-vector read
- **Speed** — cache hits ~**138×** faster than miss path on the agentic live bench (hit p50 ~0.1 ms vs miss ~13.8 ms; ~89.5% exact hit rate) — [benchmarks](docs/benchmarks.md)

**When to use**

Expand All @@ -16,7 +21,53 @@ conproxy is a caching proxy for search backends. LLM caches skip re-generating a
- LLM-response caching (that's GPTCache or RedisVL SemanticCache territory)
- Cross-org mTLS peer replication (not planned; use a mesh sidecar)

One MCP endpoint, any backend, semantic tier with false-hit gating. Benchmarks reproducible.
**The problem**

LLM caches (GPTCache, RedisVL SemanticCache) skip re-generating answers, but agents still re-rerank, re-embed, and re-query the same corpora on retries, multi-agent fanout, and tool-call storms. Every repeated retrieval costs an embed call and a managed-vector read. conproxy caches the retrieval leg itself.

**Consider conproxy if…**

- [ ] Multiple agents or tool loops hit the same corpus
- [ ] Embed or managed-vector $ is visible
- [ ] You want one MCP/HTTP search façade over ES / Qdrant / pgvector / Meilisearch / Pinecone / Milvus
- [ ] You need measured hit rate / false-hit gate, not vibes (`make bench-hitrate`)

**Skip conproxy if…**

- You only need an LLM-response cache → use GPTCache / RedisVL
- A single in-process memoize hash covers your duplicates
- You need write-path CDC / multi-region invalidation today (not shipped; track correctness doc)
- One tiny backend, no agent loops, no cost pressure

**vs alternatives**

| Need | Prefer |
|------|--------|
| Cache **LLM answers** | GPTCache / RedisVL SemanticCache |
| Cache **search/retrieval** under agents | **conproxy** |
| One process, no daemon, single backend | In-process memoize / app cache |
| Multi-backend cascade / MCP tune / dry-run scope | **conproxy** |
| LLM-side semantic cache for prompts | LangChain cache / provider-level caching |

**At a glance**

| | |
|--|--|
| **Category** | Retrieval-leg cache for agentic RAG |
| **Not** | LLM answer cache (GPTCache / RedisVL) |
| **Pays when** | Agents re-query — hits skip embed + upstream |
| **Proof** | ~89.5% exact hit rate; hit p50 ~0.1 ms vs miss ~13.8 ms (~**138×**) — [benchmarks](docs/benchmarks.md) |
| **Integrate** | MCP `conproxy mcp` · HTTP/gRPC · [Python SDK](docs/sdk-python.md) |

**FAQ**

- **What is conproxy?** A caching proxy in front of search backends. Caches retrieval results, not LLM tokens.
- **How is it different from GPTCache / RedisVL SemanticCache?** Those cache LLM answers. conproxy caches embed + search results for agents re-querying the same corpora.
- **When does it pay?** Retries, multi-agent fanout, tool-call storms. Cost + latency win on every hit.
- **How do I try it?** Install (binary / Docker / Helm) → see Quick Start below. One curl hits the proxy.
- **How do I prove it on my data?** `make bench-hitrate` for synthetic traces; `make bench-hitrate-replay QUERIES=path/to/trace.txt` for your real query log.

One MCP endpoint, any backend, cost + latency on hits, false-hit gated semantic tier. Benchmarks reproducible.

```
agent ──► MCP / HTTP / gRPC ──► conproxy ──► backends
Expand All @@ -36,6 +87,7 @@ Works with Elasticsearch, OpenSearch, Qdrant, pgvector, Meilisearch, Pinecone, M
**Agentic cache**

- In-memory cache with TTL, jitter, and background refresh; S3-FIFO eviction
- Hit path skips embed + upstream — **cost and latency** win on every hit; coalesce collapses concurrent duplicates
- Semantic tier with τ-frontier and measured false-hit rate (≤1% gate)
- Request coalescing (singleflight) to collapse concurrent duplicates
- Negative caching for errors; serve-stale-while-refresh
Expand Down Expand Up @@ -112,6 +164,15 @@ docker run -d --name conproxy -p 9999:9999 -p 10000:10000 \
ghcr.io/jmcgrath207/conproxy:0.1.0
```

**Docker Compose (proxy + Meilisearch):**
```bash
git clone https://github.com/jmcgrath207/conproxy
cd conproxy/examples/docker-compose
docker compose up -d
curl -s http://127.0.0.1:10000/health
```
See [`examples/docker-compose/`](examples/docker-compose/) and [`docs/docker-compose.md`](docs/docker-compose.md) for the full walkthrough.

**Helm (Kubernetes):**
```bash
helm install conproxy oci://ghcr.io/jmcgrath207/charts/conproxy \
Expand Down Expand Up @@ -259,6 +320,7 @@ Multi-leg cascade and federated variants: see [`examples/multi-upstream-cascade.
| [MCP Integration](docs/mcp-integration.md) | Setup for Claude Desktop, opencode, and other stdio clients; tune tools |
| [Distill](docs/distill.md) | Cache export for LLM ingestion |
| [Deployment](docs/deployment.md) | Production setup and monitoring |
| [Docker Compose](docs/docker-compose.md) | Side-by-side conproxy + backend stack ([example](examples/docker-compose/)) |
| [Feature Flags](docs/feature-flags.md) | Compile-time features |
| [Python SDK](docs/sdk-python.md) | Native client + LangChain/LlamaIndex adapters |

Expand Down
34 changes: 28 additions & 6 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,38 @@ services:
ports:
- "9999:9999"
- "10000:10000"
volumes:
- ./.conproxy:/var/lib/conproxy/.conproxy:ro
command: start --listen 0.0.0.0:9999
### Docker Compose

For a runnable, side-by-side conproxy + Meilisearch stack (pinned versions,
healthcheck, non-root) see
[`docs/docker-compose.md`](docker-compose.md) and
[`examples/docker-compose/`](../examples/docker-compose/).

```yaml
# Minimal shape (full file in the example):
services:
meilisearch:
image: getmeili/meilisearch:v1.8
environment:
MEILI_NO_ANALYTICS: "true"
ports: ["7700:7700"]

qdrant:
image: qdrant/qdrant:latest
conproxy:
image: ghcr.io/jmcgrath207/conproxy:0.1.0
depends_on:
meilisearch: { condition: service_healthy }
ports:
- "6333:6333"
- "9999:9999"
- "10000:10000"
volumes:
- ./conproxy.toml:/etc/conproxy/conproxy.toml:ro
command: ["start", "--config", "/etc/conproxy/conproxy.toml", "--listen", "0.0.0.0:9999"]
```

Note: the in-repo `tests/e2e/docker-compose.yml` boots the **full test
matrix** (qdrant + ES + OS + meilisearch×2 + pgvector) and is **not** a
user-facing starting point — use `examples/docker-compose/` instead.

## P2P Replication

Multiple conproxy instances can replicate cache state between each other using CDC events over gRPC.
Expand Down
136 changes: 136 additions & 0 deletions docs/docker-compose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Docker Compose

Quickest way to bring up conproxy + a backend on a single host. Run from
`examples/docker-compose/`:

```bash
docker compose up -d
curl -s http://127.0.0.1:10000/health
```

For the full step-by-step (customize, seed, troubleshoot), see
[`examples/docker-compose/README.md`](../examples/docker-compose/README.md).
This page covers the why behind the compose layout and the production gaps.

## Layout

```mermaid
flowchart LR
Host["host: docker compose"] -->|port 9999 gRPC| Conproxy["conproxy:0.1.0<br/>+ conproxy.toml"]
Host -->|port 10000 HTTP| Conproxy
Conproxy -->|"http://meilisearch:7700"| Meili[("Meilisearch<br/>v1.8")]
Meili <-->|volume| M[(meili_data)]
```

Two services, one user-defined volume for Meilisearch. conproxy is stateless
and runs as the non-root `conproxy` user (uid 10001).

Meilisearch is chosen for the example because it's **text-native out of the
box** — no FastEmbed or local embedder configuration needed for the demo
flow. Swap it for Qdrant / Elasticsearch / pgvector (see
`examples/multi-upstream-cascade.toml`) when you want vector search.

## Service design choices

| Choice | Reason |
|--------|--------|
| Pinned image tags (`0.1.0`, `meilisearch v1.8`) | Reproducibility — `:latest` drifts |
| Meilisearch healthcheck + `depends_on: service_healthy` | Avoids race on first boot |
| `no-new-privileges` on conproxy | Cheap hardening, blocks trivial escalation |
| `restart: unless-stopped` on conproxy | Default for a long-running daemon |
| HTTP listen `0.0.0.0` (via CLI or `[server]`) | Required for Docker port-mapping |
| Upstream URL uses **Compose DNS name**, not `localhost` | Cross-service networking |
| Single user-defined volume (Meilisearch only) | conproxy is in-memory by default; add a volume if you enable `persistence` |

## Ports

| Port | Service | Used for |
|------|---------|----------|
| `9999` | conproxy gRPC | Programmatic query + admin |
| `10000` | conproxy HTTP | `/query`, `/health`, `/metrics`, `/cache/*`, `/admin/*` |
| `7700` | Meilisearch HTTP | (host-mapped for direct seeding via meili CLI / curl) |

## Customizing the image

For local builds (e.g. CI smoke of a PR), replace the image with a `build:`
key pointing at the repo root or your fork:

```yaml
services:
conproxy:
build:
context: ../..
dockerfile: Dockerfile
# ...rest unchanged
```

This honors the same `release` feature flags baked into the published image
(`mcp` + `persistence` + `embed-api` + `pgvector`).

## Adding more services

Add `meilisearch`, `elasticsearch`, or `pgvector` blocks to the same file
and reference them by service name in `conproxy.toml`:

```yaml
meilisearch:
image: getmeili/meilisearch:v1.8
environment:
MEILI_NO_ANALYTICS: "true"
ports: ["7700:7700"]
```

```toml
[upstreams.meili]
url = "http://meilisearch:7700"
type = "meilisearch"

[[contexts.default.upstreams]]
ref = "meili"
```

For a multi-leg cascade, see
[`examples/multi-upstream-cascade.toml`](../examples/multi-upstream-cascade.toml).

## Production gaps (what compose *won't* give you)

- **HA** — single instance; no leader election, no peer mesh
- **TLS** — gRPC/HTTP are plaintext on the Docker network; terminate at a
reverse proxy or use `--cert` flags for mTLS on the proxy
- **Auth** — `proxy.api_key` is required only if you set one; compose does
not ship one
- **Persistence** — in-memory cache only; restart = cold cache. Enable
`persistence` (redb) and mount a volume for `/var/lib/conproxy/persist`
- **Backups** — Meilisearch data only; conproxy has none to back up
- **Observability** — add `--profile observability` and Prometheus/Grafana
or scrape `/metrics` from a sidecar

For any of the above, use the
[Helm chart](https://github.com/jmcgrath207/conproxy/pkgs/container/charts%2Fconproxy)
or roll your own systemd/k8s manifests — see
[`deployment.md`](deployment.md).

## Troubleshooting

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `conproxy` exits immediately | `conproxy.toml` parse error | `docker compose logs conproxy` |
| `connection refused` on `/query` | Listen on `127.0.0.1` inside container | `[server] listen = "0.0.0.0:9999"` (or pass `--listen`) |
| Qdrant unhealthy | Slow first boot / OOM | Increase `interval`/`retries`; check `docker compose logs qdrant` |
| Miss on every call | Empty Meilisearch index | Create the index + POST docs (see `examples/docker-compose/README.md`) |
| `results: []` even after seeding | Meilisearch adapter defaulted to `search_attributes: ["content"]` — only the `content` field was searched | Fixed in this branch; if using v0.1.0 image, set `search_fields = ["title", "body", "content"]` in `conproxy.toml` |
| Hit/miss ratio looks wrong | `cache_status` shape | See `/metrics` for `conproxy_cache_hit_rate` |

**Known issue (v0.1.0 image):** the Meilisearch adapter defaulted to
`search_attributes: ["content"]`, so only the `content` field was searched.
Documents with `title`/`body` but no `content` returned 0 results even
though Meilisearch had matching docs. Fixed in this branch — empty
`search_fields` now searches all fields. If using the v0.1.0 image, set
`search_fields = ["title", "body", "content"]` in `conproxy.toml` or add
`"content"` to your documents.

## Reference

- [`examples/docker-compose/docker-compose.yml`](../examples/docker-compose/docker-compose.yml) — the runnable file
- [`examples/docker-compose/conproxy.toml`](../examples/docker-compose/conproxy.toml) — in-compose config
- [`deployment.md`](deployment.md) — production deploys (Docker + systemd + k8s)
1 change: 1 addition & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ conproxy stop
- **[Tour](tour.md)** — full feature walkthrough
- **[Configuration](configuration.md)** — all config fields
- **[CLI Reference](cli-reference.md)** — all commands and flags
- **[Docker Compose](docker-compose.md)** — side-by-side proxy + backend stack (no Rust toolchain needed)
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Runnable configs and integration snippets. Copy any file into `.conproxy/conprox
| `multi-upstream-cascade.toml` | Two-upstream priority cascade with RRF fusion. See [Multi-Upstream](../docs/multi-upstream.md#priority-based-cascade). |
| `federated-search.toml` | Local-first federated search with merge modes. See [Federated](../docs/multi-upstream.md#federated-search). |
| `multi-context.toml` | Two contexts share one Meili resource; isolated cache + scope. |
| `docker-compose/` | Side-by-side `conproxy` + `meilisearch` stack with healthcheck. See [Docker Compose](../docs/docker-compose.md). |
| `mcp-claude-desktop.json` | Claude Desktop MCP server registration. See [MCP Integration](../docs/mcp-integration.md). |
| `mcp-opencode.jsonc` | opencode MCP server registration (global config). See [MCP Integration](../docs/mcp-integration.md). |
| `distill-postprocess.sh` | Post-process hook for `conproxy distill`. See [Distill](../docs/distill.md). |
Loading
Loading