Skip to content
Open
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1b062eb
Add GitLab token support and enhance plugin code search functionality…
Apr 10, 2026
5d46250
mount local files to Docker configuration;
Apr 14, 2026
6e2f552
Enhance plugin code search functionality: increase file size limits;
Apr 14, 2026
0873d50
Merge remote-tracking branch 'origin/main' into freva_plugins
Apr 14, 2026
8b18dc2
Refine plugin code search instructions: enhance analysis of source co…
Apr 14, 2026
2528992
Enhance plugin code retrieval: add max character limit for fetched co…
Apr 15, 2026
f371490
Update docker-compose and server configurations: change volume paths …
Apr 27, 2026
626a8ae
Merge remote-tracking branch 'origin/main' into freva_plugins
May 28, 2026
8365f27
Update web search server configuration;
Jun 2, 2026
e5c18bc
Merge remote-tracking branch 'origin/main' into freva_plugins
Jun 2, 2026
31fb7ff
update GitLab access validation logic for plugin tool usage: use repo…
Jun 9, 2026
81c331f
Refactor plugin code search and context retrieval: update function si…
Jul 1, 2026
4b5e76a
Enhance plugin validation and logging:
Jul 1, 2026
76c805c
Merge branch 'main' into freva_plugins
Jul 2, 2026
4665b7c
restore "plugin_code_search" MCP tool source code after merge from main
Jul 2, 2026
cfccc66
Update README and docker-compose for ClimateClaw directory structure,…
Jul 2, 2026
532b751
formatting + linting done via pre-commit hooks
Jul 3, 2026
4c43e7a
Enhance plugin_code_search functionality:
Jul 7, 2026
7b2517a
Refine prompts for code search functionality: more clarity & structur…
Jul 7, 2026
ac27e17
Refine plugin_code_search documentation for its usage scope; enhance …
Jul 8, 2026
720ec01
Refactor MCP Manager and Web Search Server:
Jul 9, 2026
8b2e1a8
Update README.md for clarity and completeness: add MCP server details…
Jul 9, 2026
5462081
satisfy mypy in CI pipeline;
Jul 9, 2026
fac37ef
satify mypyp in CI job
Jul 9, 2026
d291036
added plugin-code-search server to dev scripts
Jul 10, 2026
a1c0102
Rewrite prompt files to markdown, adapt loading in of prompt files
Jul 24, 2026
56dd713
Add matplotlib dependency for development/evaluation purposes and enh…
Jul 27, 2026
5acb87c
Enhance dev benchmark and evaluation tools: refactor benchmark script…
Jul 28, 2026
ddcc6c2
- refined eval scripty for tool calling
Jul 30, 2026
93a7639
Add benchmark prompts for various plugins in JSON format;
Jul 31, 2026
4ae6fbb
Refactor model handling across various scripts: pass model as additio…
Jul 31, 2026
a066f01
fix file format (.md) for ollama system prompts
Jul 31, 2026
7422a06
Update docker-compose and evaluation scripts: change data volume path…
Aug 4, 2026
b94d0d3
Remove model parameter from various functions and update header handl…
Aug 4, 2026
a4a767a
add first results/plots of tool evaluation (tool + plugin selection)
Aug 4, 2026
a2f429c
Update reasoning effort for gpt-5.6-luna model and refine starting pr…
Aug 4, 2026
133a7a1
enhance prompt clarity, and refactor plugin detection logic for plugi…
Aug 6, 2026
e334475
refactored server.py for better coherence; improved system prompts
Aug 13, 2026
3f52de9
Merge branch 'main' into freva_plugins
gekinci Oct 1, 2026
eabbcb2
Clean up formatting changes
gekinci Oct 1, 2026
a6edf6e
Incorporate chnages from first PR Review: Refactor plugin code handli…
Oct 5, 2026
26542d0
Merge latest changes of 'main' into 'freva_plugins' branch
Oct 6, 2026
d3ab367
- Update litellm image version for consistency
Oct 6, 2026
ad20aec
Refactor plugin code handling: update plugin code lookup instructions…
Oct 7, 2026
ff33d33
Improve update file selection logic & prompts for better functionality
Oct 8, 2026
2d9d890
more compact statements in code retrieval logic
Oct 8, 2026
82d7d23
Remove prompt dir for GPT5 (as this will be separately handled in "gp…
Oct 8, 2026
9cc147f
Merge latest changes from branch 'main' into 'freva_plugins'
Oct 8, 2026
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
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ CLIMATECLAW_OLLAMA_KEEP_ALIVE=-1 # Keep Ollama models loaded after use. -1 = in
CLIMATECLAW_CODE_SERVER_PORT=8051
CLIMATECLAW_RAG_SERVER_PORT=8050
CLIMATECLAW_WEB_SEARCH_SERVER_PORT=8052
CLIMATECLAW_PLUGIN_CODE_SEARCH_SERVER_PORT=8053

CLIMATECLAW_AVAILABLE_MCP_SERVERS=rag-server,code-server,web-search-server # The list of servers to connect via backends client
CLIMATECLAW_AVAILABLE_MCP_SERVERS=rag-server,code-server,web-search-server,plugin-code-search-server # The list of servers to connect via backends client
CLIMATECLAW_RAG_SERVER_URL="http://haproxy:8050" # IMPORTANT: Server URLs must be given as 'CLIMATECLAW_{server_name}_SERVER_URL' for successful parsing
CLIMATECLAW_CODE_SERVER_URL="http://haproxy:8051"
CLIMATECLAW_WEB_SEARCH_SERVER_URL="http://haproxy:8052"
CLIMATECLAW_PLUGIN_CODE_SEARCH_SERVER_URL="http://plugin-code-search-server:8053"
CLIMATECLAW_MCP_REQUEST_TIMEOUT_SEC=600

# MONGODB SETTINGS
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/ci_job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ jobs:

- name: Start dev stack
run: |
./dev.sh up code-server web-search-server -d --wait --build --remove-orphans || {
./dev.sh up code-server web-search-server plugin-code-search-server -d --wait --build --remove-orphans || {
docker compose ps -a
docker compose logs web-search-server
docker compose logs plugin-code-search-server
docker compose logs code-server
exit 1
}
Expand Down Expand Up @@ -102,18 +103,20 @@ jobs:
CLIMATECLAW_MONGODB_PASSWORD=ci-psswrd
CLIMATECLAW_MONGODB_DATABASE_NAME=ci-db
CLIMATECLAW_OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
CLIMATECLAW_GITLAB_ACCESS_TOKEN=${{ secrets.GITLAB_ACCESS_TOKEN }}
EOF

- name: Build production base image
run: docker compose --profile build-only build climateclaw-base

- name: Start production stack
run: |
docker compose up climateclaw code-server web-search-server -d --wait --build --remove-orphans || {
docker compose up climateclaw code-server web-search-server plugin-code-search-server -d --wait --build --remove-orphans || {
Comment thread
alex-fischer-97 marked this conversation as resolved.
Outdated
docker compose ps -a
docker compose logs climateclaw
docker compose logs code-server
docker compose logs web-search-server
docker compose logs plugin-code-search-server
Comment thread
alex-fischer-97 marked this conversation as resolved.
Outdated
docker compose logs mongodb
docker compose logs litellm
exit 1
Expand Down
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# ClimateClaw
ClimateClaw is a Python service for building AI-assisted climate-data workflows. It provides the API, conversation handling, model prompting, persistent thread storage, and tool orchestration needed to support interactive work with climate data.

The project integrates LiteLLM-native prompting, MongoDB-backed conversation state, and MCP-based tool execution for retrieval, code execution, and domain-specific automation.
`ClimateClaw` is a Python service for building AI-assisted climate-data workflows. It provides the API, conversation handling, model prompting, persistent thread storage, and tool orchestration needed to support interactive work with climate data.

The project integrates LiteLLM-native prompting, MongoDB-backed conversation states, and MCP-based tool execution for code execution, web/documentation search, code retrieval, and domain-specific automation.

[TOC]

## Highlights

- FastAPI app with strict auth parity to the production Rust service (`/api/chatbot/*`)
- Streaming responses via LiteLLM/OpenAI-compatible SSE (`application/x-ndjson`) with code + image variants
- Persistent conversation threads in MongoDB and JSONL files (`threads/`), plus per-user scratch space (`cache/`)
Expand All @@ -15,21 +19,25 @@ The project integrates LiteLLM-native prompting, MongoDB-backed conversation sta
## Quick Start (deployment)

### Requirements

- `podman` or `docker`
- Credentials & headers for the Freva auth services

### Configure environment

Create `.env` (used by FastAPI, Docker, and MCP servers). See `.env.example` for guidance.

### Full stack via Docker Compose

```bash
./prod.sh up -d --build
```

Services that start:

- `climateclaw`: FastAPI app (debugpy toggle via `DEBUG=true` for remote debugging session)
- `code-server`: MCP server running the sandboxed Jupyter kernel and exposing `code_interpreter`
- `web-search-server`: MCP server doing web search via OpenAI API and exposing `web_search`
- `rag-server`: MCP server exposing `get_context_from_resources`
- `litellm`: LiteLLM proxy that reads `litellm_config.yaml`
- `ollama`: Optional local model runner for LiteLLM backends

Expand All @@ -38,17 +46,21 @@ Bind mounts expose `/work`, logs, threads, and shared `cache` to other Freva ser
## Quick Start (local dev)

### Requirements

- `podman` or `docker`

### Configure environment

Create `.env` (used by FastAPI, Docker, and MCP servers). See `.env.example` for guidance.

### Start docker containers in DEV mode

```bash
./dev.sh up -d --build
```

## Repository Layout

| Path | Purpose |
| --- | --- |
| `src/climateclaw/app.py` | FastAPI entrypoint, CORS policy, router registration, app lifespan hooks |
Expand All @@ -67,12 +79,13 @@ Create `.env` (used by FastAPI, Docker, and MCP servers). See `.env.example` for
| `litellm_config.yaml` | Source of truth for model catalog (consumed by `available_chatbots()`) |

Generated artifacts that persist across runs:

- `threads/` (JSONL transcript per thread id)
- `cache/{user_id}/{thread_id}` (LLM-created files, plots, etc.)
- `logs/` (when mounted in Docker)

## Architecture at a Glance
1. **FastAPI layer** enforces auth via `AuthRequired` (Bearer tokens validated against `x-freva-rest-url`), derives stable UUIDv5 pseudonymous user IDs from usernames, and validates per-request headers.
1. **FastAPI layer** enforces auth via `AuthRequired` (Bearer tokens validated against `x-freva-rest-url`), derives stable UUIDv5 pseudonymous user IDs from usernames, and validates per-request headers.
2. **LiteLLM proxy** (`CLIMATECLAW_LITE_LLM_ADDRESS`) provides OpenAI-compatible chat + embeddings endpoints; completions stream into `StreamVariant` classes that normalize assistant text, code blocks, tool hints, images, and server hints.
3. **Persistence** uses MongoDB for storing threads and user feedback.
4. **MCP Manager** (`src/climateclaw/services/mcp/mcp_manager.py`) connects to tool servers listed in `CLIMATECLAW_AVAILABLE_MCP_SERVERS`, discovers tools, exposes OpenAI function schemas to LiteLLM, and routes tool invocations with per-thread session ids.
Expand All @@ -94,14 +107,15 @@ Generated artifacts that persist across runs:
| `POST` | `/api/chatbot/stop` | Initiates stopping of an active conversation | JSON body: `thread_id`; requires auth |

### Streaming contract

- Response type: `application/x-ndjson`
- Each `data:` line is a JSON object with `variant` discriminators (`Assistant`, `Code`, `CodeOutput`, `CodeError`, `Image`, `ServerHint`, `StreamEnd`, etc.).
- Code tool calls stream incremental chunks while LiteLLM emits `tool_calls`. When the MCP tool resolves, results are converted back into JSON events and appended to Mongo/disk storage.
- The first chunk is a `ServerHint` carrying the `thread_id`; conversation variants are stored in-memory during streaming and flushed to MongoDB at the end, ensuring replay safety.
- Clients can call `/api/chatbot/stop?thread_id=...` to move a conversation into `STOPPING`; the streaming loop exits and cancels in-flight MCP requests (code, rag, web-search) via the shared `ActiveRequest` registry.

## Persistence, Prompts, and Assets
- **MongoDB (`mongodb_storage.py`)**: canonical record for threads. Each document stores a UUIDv5 pseudonymous `user_id`, `thread_id`, ISO timestamp, topic (summarized via LiteLLM), and serialized `StreamVariant` list.
- **MongoDB (`mongodb_storage.py`)**: canonical record for threads. Each document stores a UUIDv5 pseudonymous `user_id`, `thread_id`, ISO timestamp, topic (summarized via LiteLLM), and serialized `StreamVariant` list.
- **`cache/` scratch**: `create_dir_at_cache()` ensures each user/thread has a writable directory for generated files (plots, CSVs). Entries are sanitized if user IDs contain unsupported characters.
- **Prompt library**: `prompt_library/baseline` contains `starting_prompt.txt`, `summary_prompt.txt`, and `examples.jsonl`. GPT-5 models currently fall back to baseline prompts (warning logged). Customize by adding new prompt sets and updating `_resolve_baseline_dir()` / `_resolve_gpt5_dir_or_placeholder()`.
- **Resources**: `resources/stableclimgen` seeds the RAG MCP server. Drop additional corpora per library folder and list them in `CLIMATECLAW_AVAILABLE_LIBRARIES` inside `src/climateclaw/tools/rag/server.py`.
Expand All @@ -110,6 +124,7 @@ Generated artifacts that persist across runs:
- **Code interpreter** (`src/climateclaw/tools/code/server.py`): spins up per-session Jupyter kernels, sanitizes input, enforces configurable timeouts, and injects Freva config via environment variables. Outputs include stdout/stderr, display data, and structured errors.
- **Web search server** (`src/climateclaw/tools/web_search/server.py`): calls OpenAI Web Search (`gpt-4.1`) constrained to ICON model + DKRZ/HPC docs. Honors request cancellation.
- **RAG server** (`src/climateclaw/tools/rag/server.py`): indexes documentation with custom loaders + splitters, stores embeddings in MongoDB (`embeddings`), and surfaces a single tool `get_context_from_resources`. LiteLLM requests embed queries through the same proxy (`CLIMATECLAW_LITE_LLM_ADDRESS`).
- **Plugin code search server** (`src/climateclaw/tools/plugin_code_search/server.py`): retrieves code from GitLab repositories used as Freva plugins for climate data analysis (included projects: *Coming Decade*, *ClimXtreme*, *RegiKlim*). Returns scraped code, doc and/or config files as query-relevant context.
- **Header gate** (`src/climateclaw/tools/header_gate.py`): wraps each MCP ASGI app so critical headers become ContextVars and requests fail fast when missing/invalid (e.g., missing Mongo URI yields SSE-friendly JSON-RPC errors).
- **Manager** (`src/climateclaw/services/mcp/mcp_manager.py`): caches clients, discovers tool schemas, exports OpenAI function definitions, and pins MCP session ids to thread ids for deterministic tool contexts.

Expand All @@ -128,6 +143,7 @@ Generated artifacts that persist across runs:
- **Ports**: HAProxy binds `CLIMATECLAW_TARGET_PORT` for the backend and `4000` for litellm; MCP frontends bind their configured ports (e.g., 8050/8051/8052) while container instances stay internal.

## Troubleshooting

- **Auth failures**: verify headers include both `Authorization` and `x-freva-rest-url`. Inspect FastAPI logs for the exact HTTP status.
- **Missing models**: ensure `litellm_config.yaml` is readable and contains `model_name` keys. `available_chatbots()` aborts the process if it cannot find any entries.
- **MCP issues**: backend logs warn but continue when tool discovery fails; LiteLLM will simply not emit tool calls. Use `settings.AVAILABLE_MCP_SERVERS` to enable/disable targets explicitly.
Expand Down
88 changes: 70 additions & 18 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ services:
- "8600:${CLIMATECLAW_BACKEND_PORT:-8502}" # localhost:8600/docs give FastAPI docs
- "${CLIMATECLAW_DEBUG_PORT:-5678}:${CLIMATECLAW_DEBUG_PORT:-5678}" # Port for debugpy
volumes:
- /Users/fischer/DKRZ/Coding/ClimateClaw/work:/work:ro # mounts sshfs work directory for easy access to code and resources from climateclaw container
- ./cache/:/app/cache
- ./logs/:/app/logs
networks:
- climateclaw
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:${CLIMATECLAW_BACKEND_PORT:-8502}/healthz')\""]
test:
[
"CMD-SHELL",
'python -c "import urllib.request; urllib.request.urlopen(''http://127.0.0.1:${CLIMATECLAW_BACKEND_PORT:-8502}/healthz'')"',
]
interval: 30s
timeout: 5s
retries: 3
Expand Down Expand Up @@ -72,12 +77,17 @@ services:
- 8051:8051
cpus: 4
volumes:
- /Users/fischer/DKRZ/Coding/ClimateClaw/work:/work:ro
- ./cache/:/app/cache
- ./logs/:/app/logs
networks:
- climateclaw
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8051/healthz')\""]
test:
[
"CMD-SHELL",
'python -c "import urllib.request; urllib.request.urlopen(''http://127.0.0.1:8051/healthz'')"',
]
interval: 30s
timeout: 5s
retries: 3
Expand All @@ -102,7 +112,40 @@ services:
volumes:
- ./logs/:/app/logs
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8052/healthz')\""]
test:
[
"CMD-SHELL",
'python -c "import urllib.request; urllib.request.urlopen(''http://127.0.0.1:8052/healthz'')"',
]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s

plugin-code-search-server:
image: plugin-code-search-server-${CLIMATECLAW_INSTANCE_NAME}
build:
context: .
dockerfile: docker/plugin-code-search-server/Dockerfile
env_file: .env
environment:
CLIMATECLAW_DEV: ${CLIMATECLAW_DEV}
CLIMATECLAW_DEBUG: ${CLIMATECLAW_DEBUG:-0}
CLIMATECLAW_MCP_HOST: 0.0.0.0
CLIMATECLAW_MCP_PORT: 8053
CLIMATECLAW_GITLAB_ACCESS_TOKEN: ${CLIMATECLAW_GITLAB_ACCESS_TOKEN:-}
ports:
- 8053:8053
networks:
- climateclaw
volumes:
- ./logs/:/app/logs
healthcheck:
test:
[
"CMD-SHELL",
'python -c "import urllib.request; urllib.request.urlopen(''http://127.0.0.1:8053/healthz'')"',
]
interval: 30s
timeout: 5s
retries: 3
Expand Down Expand Up @@ -130,7 +173,11 @@ services:
networks:
- climateclaw
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')\""]
test:
[
"CMD-SHELL",
'python -c "import urllib.request; urllib.request.urlopen(''http://localhost:4000/health/liveliness'')"',
]
interval: 30s
timeout: 10s
retries: 3
Expand Down Expand Up @@ -216,7 +263,7 @@ services:
user: 0:0
hostname: volume-prep
restart: "no"
command: find /settings -mindepth 1 -maxdepth 1 -exec rm -rf {} +
command: find /settings -mindepth 1 -maxdepth 1 -exec rm -rf {} +
volumes:
- settings-data:/settings:rw

Expand All @@ -228,7 +275,14 @@ services:
depends_on:
volume-prep:
condition: service_completed_successfully
command: ["clone", "--depth", "1", "https://github.com/freva-org/freva-service-config.git", "/settings"]
command:
[
"clone",
"--depth",
"1",
"https://github.com/freva-org/freva-service-config.git",
"/settings",
]
volumes:
- settings-data:/settings:rw

Expand All @@ -241,11 +295,11 @@ services:
git-prep:
condition: service_completed_successfully
command: >
sh -eu -c '
ls -rlt /settings &&
python /settings/dev-utils.py gen-certs --cert-dir /certs &&
cp -a /settings/keycloak/import/. /import/
'
sh -eu -c '
ls -rlt /settings &&
python /settings/dev-utils.py gen-certs --cert-dir /certs &&
cp -a /settings/keycloak/import/. /import/
'
volumes:
- settings-data:/settings:ro
- certs-data:/certs:rw
Expand Down Expand Up @@ -288,11 +342,11 @@ services:
- "8080:8080"
- "8443:8443"
command: |
start-dev
--hostname-strict=false
--hostname=http://keycloak.localhost:8080
--import-realm
-Dkeycloak.migration.strategy=OVERWRITE_EXISTING
start-dev
--hostname-strict=false
--hostname=http://keycloak.localhost:8080
--import-realm
-Dkeycloak.migration.strategy=OVERWRITE_EXISTING

keycloak-bootstrap:
image: docker.io/alpine
Expand Down Expand Up @@ -355,12 +409,10 @@ services:
volumes:
- certs-data:/certs:ro


networks:
climateclaw:
driver: bridge


volumes:
settings-data:
certs-data:
Expand Down
26 changes: 20 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
services:

climateclaw-base:
# Build manually with:
# docker compose -f docker-compose.dev.yml --profile build-only build climateclaw-base
Expand All @@ -26,7 +25,11 @@ services:
networks:
- climateclaw
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:${CLIMATECLAW_BACKEND_PORT:-8502}/healthz')\""]
test:
[
"CMD-SHELL",
'python -c "import urllib.request; urllib.request.urlopen(''http://127.0.0.1:${CLIMATECLAW_BACKEND_PORT:-8502}/healthz'')"',
]
interval: 5s
timeout: 3s
retries: 20
Expand Down Expand Up @@ -55,7 +58,11 @@ services:
networks:
- climateclaw
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8051/healthz')\""]
test:
[
"CMD-SHELL",
'python -c "import urllib.request; urllib.request.urlopen(''http://127.0.0.1:8051/healthz'')"',
]
interval: 5s
timeout: 3s
retries: 20
Expand All @@ -76,7 +83,11 @@ services:
volumes:
- /container/da/climateclaw-links/${CLIMATECLAW_INSTANCE_NAME}/logs:/app/logs
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8052/healthz')\""]
test:
[
"CMD-SHELL",
'python -c "import urllib.request; urllib.request.urlopen(''http://127.0.0.1:8052/healthz'')"',
]
interval: 5s
timeout: 3s
retries: 20
Expand All @@ -102,7 +113,11 @@ services:
networks:
- climateclaw
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')\""]
test:
[
"CMD-SHELL",
'python -c "import urllib.request; urllib.request.urlopen(''http://localhost:4000/health/liveliness'')"',
]
interval: 30s
timeout: 10s
retries: 3
Expand Down Expand Up @@ -142,7 +157,6 @@ services:
retries: 3
start_period: 15s


networks:
climateclaw:
driver: bridge
Loading
Loading