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
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
__pycache__
tests
*.md
web/node_modules
web/.next
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,27 @@ jobs:
done
docker logs agentops
exit 1
web:
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci
- run: npm run build
manifests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kubectl
run: |
curl -fsSL -o kubectl "https://dl.k8s.io/release/v1.32.2/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/kubectl
- run: kubectl kustomize k8s >/dev/null
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
__pycache__/
.venv/
.pytest_cache/
node_modules/
.next/
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ flowchart LR
ING[/docs ingest/] --> Q[[RabbitMQ]] --> W[Worker: chunk+embed, retries, DLQ] --> DB[(MongoDB)]
T --> DB
```
Built: API, LangGraph agent, MCP server, Redis cache, MongoDB document and chat store, RabbitMQ worker, eval harness, Docker, Render and Fly config, CI. Not built yet: the Next.js UI and Kubernetes manifests.
Built: API, LangGraph agent, MCP server, Redis cache, MongoDB document and chat store, RabbitMQ worker, eval harness, Next.js chat UI, Docker, Render, Fly, and kind manifests, CI.

## Quickstart
```bash
Expand All @@ -28,6 +28,8 @@ docker compose up --build # API, worker, Redis, MongoDB, RabbitMQ
pytest -q # offline tests
python -m app.evals # tool accuracy, golden Q&A, guardrails
python -m app.mcp_server # MCP (stdio) for Cursor/Claude
cd web && npm install
API_URL=http://127.0.0.1:8000 npm run dev # http://localhost:3000
```
Set `OPENAI_API_KEY` (and optionally `MODEL`, default `gpt-4o-mini`) to use the LangGraph ReAct agent. Without a key, a deterministic router picks the tool, so tests and demos run offline.

Expand All @@ -53,6 +55,7 @@ Point Cursor or Claude Desktop at the MCP server:
| `MONGO_DB` | Database name. Default `agentops`. |
| `MONGO_VECTOR_INDEX` | Atlas `$vectorSearch` index on `chunks.embedding`. |
| `RABBITMQ_URL` | Publishes ingest to the worker. Unset ingests inline. |
| `API_URL` | Used by the Next.js server to reach the API. Default `http://127.0.0.1:8000`. |

## Ingest and search
`POST /docs` chunks the text and stores a hashed embedding with each chunk. Without `RABBITMQ_URL` that happens in the request. With it, the API publishes to the `ingest` queue and `python -m app.worker` does the write. A message is attempted three times. The next failure is published to `ingest.dlq`. The broker dead-letters rejected deliveries to that same queue, so a crash does not drop the message.
Expand All @@ -61,8 +64,24 @@ Search keeps chunks that share a query token and breaks ties with cosine similar

Guardrails run before any tool. Empty input, questions over 2,000 characters, and prompt-injection phrasing are refused.

## Chat UI
The Next.js app in [`web/`](web/) stores a document, asks the agent, and shows which tool answered. A repeat question is marked cached.

![Desktop chat: a document hit and a cached calculation](docs/screenshots/chat-desktop.png)

On a narrow screen the document form stacks above the conversation.

![Mobile chat layout](docs/screenshots/chat-mobile.png)

The browser calls same-origin routes under `/api`. Those routes proxy to `API_URL`, so a Vercel deployment does not need browser CORS on the API.

## Deploy
`docker compose up --build` starts the API, the worker, Redis, MongoDB, and RabbitMQ.
`docker compose up --build` starts the API, the worker, the web UI, Redis, MongoDB, and RabbitMQ. The UI is on port 3000.

### Vercel
Import the repo and set the project root to `web`. Set `API_URL` to the public API, such as the Render or Fly URL. [`web/vercel.json`](web/vercel.json) selects the Next.js framework. The API stays on Render or Fly; Vercel only hosts the UI.

### Render and Fly

[render.yaml](render.yaml) is a Render blueprint: a web service on the image, plus a worker running `python -m app.worker`. Supply `OPENAI_API_KEY`, `REDIS_URL` (Upstash), `MONGO_URL` (Atlas), and `RABBITMQ_URL` (CloudAMQP). Set `MONGO_VECTOR_INDEX` when the Atlas index exists.

Expand All @@ -73,13 +92,22 @@ fly secrets set OPENAI_API_KEY=... REDIS_URL=... MONGO_URL=... RABBITMQ_URL=...
fly deploy
```

CI builds the image and checks `/health`, a document ingest, and `/ask`.
CI builds the API image and checks `/health`, a document ingest, and `/ask`. It also builds the Next.js app and renders the Kubernetes manifests with `kubectl kustomize`.

### Kubernetes
Manifests in [`k8s/`](k8s/) run Redis, MongoDB, RabbitMQ, the API, the ingest worker, and the web UI in the `agentops` namespace. Services are headless, so in-cluster DNS returns pod addresses. The API and web images are `agentops-mcp:local` and `agentops-web:local` with `imagePullPolicy: IfNotPresent`, which matches a kind cluster after `kind load`.

[`scripts/kind-smoke.sh`](scripts/kind-smoke.sh) creates a cluster, loads those images plus Redis, MongoDB, and RabbitMQ, waits for rollouts, and checks that a stored document is searchable and that `2*(3+4)` returns 14 through the web proxy.

```bash
./scripts/kind-smoke.sh
```

## Roadmap
- [x] MVP: FastAPI, LangGraph agent, MCP server, Redis cache, Docker, CI
- [x] MongoDB document and chat store, with vector search
- [x] RabbitMQ ingest worker with retries and a dead-letter queue
- [x] Eval harness (tool-call accuracy, golden Q&A) and guardrails
- [ ] Next.js (TypeScript) chat UI
- [x] Deploy config: Render and Fly
- [ ] Kubernetes manifests (kind)
- [x] Next.js (TypeScript) chat UI, deployable to Vercel
- [x] Deploy config: Render, Fly, and Vercel
- [x] Kubernetes manifests, tested on kind
10 changes: 9 additions & 1 deletion app/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def uses_broker() -> bool:
return bool(os.getenv("RABBITMQ_URL"))


def broker_parameters(url: str, *, attempts: int = 8) -> pika.URLParameters:
"""Retry long enough for RabbitMQ to finish starting in Compose or kind."""
params = pika.URLParameters(url)
params.connection_attempts = attempts
params.retry_delay = 2
return params


def declare(channel) -> None:
channel.exchange_declare(exchange=DEAD_LETTER_EXCHANGE, exchange_type="direct", durable=True)
channel.queue_declare(queue=DEAD_LETTER_QUEUE, durable=True)
Expand Down Expand Up @@ -86,7 +94,7 @@ def enqueue(doc_id: str, text: str) -> dict:
invalidate_prefix("ask:")
return {"id": doc_id, "status": "stored"}
body = json.dumps({"id": doc_id, "text": text}).encode()
connection = pika.BlockingConnection(pika.URLParameters(url))
connection = pika.BlockingConnection(broker_parameters(url))
try:
channel = connection.channel()
declare(channel)
Expand Down
4 changes: 2 additions & 2 deletions app/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pika

from .queue import DEAD_LETTER_QUEUE, INGEST_QUEUE, declare, handle_delivery
from .queue import DEAD_LETTER_QUEUE, INGEST_QUEUE, broker_parameters, declare, handle_delivery


def _publish(channel, queue: str, body: bytes, headers: dict) -> None:
Expand Down Expand Up @@ -31,7 +31,7 @@ def on_message(channel, method, properties, body) -> None:

def run_worker(url: str | None = None) -> None:
url = url or os.environ["RABBITMQ_URL"]
connection = pika.BlockingConnection(pika.URLParameters(url))
connection = pika.BlockingConnection(broker_parameters(url, attempts=30))
channel = connection.channel()
declare(channel)
channel.basic_qos(prefetch_count=1)
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ services:
condition: service_healthy
redis:
condition: service_started
web:
build: ./web
ports:
- "3000:3000"
environment:
API_URL: http://api:8000
depends_on:
- api
redis:
image: redis:7-alpine
mongo:
Expand Down
Binary file added docs/screenshots/chat-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/chat-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions k8s/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: agentops-mcp:local
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: agentops-config
startupProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
readinessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 5
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
clusterIP: None
selector:
app: api
ports:
- port: 8000
targetPort: 8000
10 changes: 10 additions & 0 deletions k8s/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: agentops-config
data:
REDIS_URL: redis://redis:6379/0
MONGO_URL: mongodb://mongo:27017
RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/
MODEL: gpt-4o-mini
API_URL: http://api:8000
12 changes: 12 additions & 0 deletions k8s/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: agentops
resources:
- namespace.yaml
- configmap.yaml
- redis.yaml
- mongo.yaml
- rabbitmq.yaml
- api.yaml
- worker.yaml
- web.yaml
42 changes: 42 additions & 0 deletions k8s/mongo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo
spec:
replicas: 1
selector:
matchLabels:
app: mongo
template:
metadata:
labels:
app: mongo
spec:
containers:
- name: mongo
image: mongo:7
ports:
- containerPort: 27017
readinessProbe:
exec:
command: ["mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
cpu: 50m
memory: 256Mi
limits:
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: mongo
spec:
clusterIP: None
selector:
app: mongo
ports:
- port: 27017
targetPort: 27017
4 changes: 4 additions & 0 deletions k8s/namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: agentops
42 changes: 42 additions & 0 deletions k8s/rabbitmq.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: rabbitmq
spec:
replicas: 1
selector:
matchLabels:
app: rabbitmq
template:
metadata:
labels:
app: rabbitmq
spec:
containers:
- name: rabbitmq
image: rabbitmq:3-alpine
ports:
- containerPort: 5672
readinessProbe:
exec:
command: ["rabbitmq-diagnostics", "-q", "ping"]
initialDelaySeconds: 10
periodSeconds: 5
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: rabbitmq
spec:
clusterIP: None
selector:
app: rabbitmq
ports:
- port: 5672
targetPort: 5672
42 changes: 42 additions & 0 deletions k8s/redis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
readinessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 3
periodSeconds: 5
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
clusterIP: None
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
Loading
Loading