Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions examples/spring-ai-engram-cloud-demo/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Sign up at https://platform.openai.com/api-keys to get an OpenAI key.
OPENAI_API_KEY=sk-...

# Sign up at https://cloud.jamjet.dev, create a project, and copy the API key.
JAMJET_API_KEY=jk_...

# Optional — defaults to the public hosted JamJet Cloud.
# JAMJET_API_URL=https://api.jamjet.dev
5 changes: 5 additions & 0 deletions examples/spring-ai-engram-cloud-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
target/
.env
.idea/
*.iml
.vscode/
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
138 changes: 138 additions & 0 deletions examples/spring-ai-engram-cloud-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Spring AI + Engram + JamJet Cloud Demo

A multi-turn chat agent that **remembers facts across calls** via [Engram](https://github.com/jamjet-labs/jamjet/tree/main/runtime/engram-server) and is **observed end-to-end** by [JamJet Cloud](https://cloud.jamjet.dev) — drop in three Spring Boot starters, get durable memory + cloud observability for free.

## What this demo shows

- **Spring AI 1.0** chat agent using OpenAI for inference
- **`dev.jamjet:engram-spring-boot-starter`** autoconfigures `EngramClient` so the agent's `@Tool` methods can record + recall facts against a real Engram server
- **`dev.jamjet:jamjet-cloud-spring-boot-starter`** auto-instruments every chat call + tool span — no code changes
- **Cross-platform run flow** — works on macOS, Linux, and Windows with the same `mvnw` + `docker compose` commands

## How it's wired

```
User → POST /chat?session=alice ──→ Spring AI ChatClient
├─→ OpenAI chat completion
└─→ @Tool methods (rememberFact / recallFacts)
└─→ EngramClient (autoconfigured)
└─→ Engram REST API (Docker)
```

JamJet Cloud's starter watches the whole flow via Spring AI's Micrometer Observation hooks and ships traces + cost rollups to the dashboard. **Zero observability code in your demo.**

## Prerequisites

- **Java 21+** (`--release 21`; Java 23 also works)
- **Docker Desktop** (or any Docker engine) — for the Engram sidecar
- **An OpenAI API key** — sign up at [platform.openai.com](https://platform.openai.com/api-keys)
- **A JamJet Cloud project** — sign up at [cloud.jamjet.dev](https://cloud.jamjet.dev), create a project, copy the API key

> **Cost note:** The OpenAI key is used twice per chat turn — once by Spring AI for the chat completion, and once by Engram for fact extraction (the LLM that turns "I prefer espresso" into a structured fact). Both calls use `gpt-4o-mini` by default.

## Run it

```bash
git clone https://github.com/jamjet-labs/jamjet-runtime-java
cd jamjet-runtime-java/examples/spring-ai-engram-cloud-demo

cp .env.example .env # Windows: copy .env.example .env
# Edit .env — paste your OPENAI_API_KEY and JAMJET_API_KEY

docker compose up -d # boots Engram on 127.0.0.1:9090
./mvnw spring-boot:run # Windows: mvnw.cmd spring-boot:run
```

The app starts on `127.0.0.1:8080`. `PreflightCheck` validates both env vars and waits for Engram's `/health` endpoint before the server accepts requests — if either is missing or Engram is unreachable, the app exits with a clear error.

In another terminal:

```bash
# Tell the agent a fact — it stores it in Engram
curl -s -X POST "localhost:8080/chat?session=alice" \
-H "Content-Type: text/plain" \
-d "I work at Acme as a Java engineer"
# → {"session":"alice","reply":"Got it, I've stored that you work at Acme as a Java engineer."}

# Ask about it — agent recalls from Engram
curl -s -X POST "localhost:8080/chat?session=alice" \
-H "Content-Type: text/plain" \
-d "Where do I work?"
# → {"session":"alice","reply":"You work at Acme."}

# The first message also contained "Java engineer" — Engram extracted that too
curl -s -X POST "localhost:8080/chat?session=alice" \
-H "Content-Type: text/plain" \
-d "What languages do I use?"
# → {"session":"alice","reply":"Based on what you've shared, you're a Java engineer."}
```

Each response is a JSON object `{"session": "...", "reply": "..."}`.

## See the trace in JamJet Cloud

Open [cloud.jamjet.dev/dashboard/graph](https://cloud.jamjet.dev/dashboard/graph) — each `/chat` call appears as a trace with:

- 1 LLM span (OpenAI chat completion)
- 1 or more Engram tool spans (`rememberFact` or `recallFacts`)
- Cost rollup (per-token, per-call)

## Anatomy

The interesting code is ~120 LOC across 4 files:

| File | What it does |
|---|---|
| `MemoryTools.java` | `@Tool` methods (`rememberFact`, `recallFacts`) backed by autoconfigured `EngramClient` |
| `MemoryAgent.java` | Spring AI `ChatClient` wired with the tools + system prompt |
| `ChatController.java` | `POST /chat?session=X` — accepts `text/plain`, returns `{"session","reply"}` |
| `startup/PreflightCheck.java` | Validates env vars + polls Engram `/health` before the app accepts traffic |

The pom has three starter dependencies. Zero custom plumbing.

## Configuration

| Property | Default | Purpose |
|---|---|---|
| `engram.base-url` | `http://127.0.0.1:9090` | Where the autoconfigured `EngramClient` connects |
| `spring.ai.openai.api-key` | `${OPENAI_API_KEY}` | Spring AI OpenAI key |
| `spring.ai.openai.chat.options.model` | `gpt-4o-mini` | OpenAI model for chat |
| `jamjet.cloud.api-key` | `${JAMJET_API_KEY}` | JamJet Cloud project key |
| `jamjet.cloud.api-url` | `https://api.jamjet.dev` | JamJet Cloud ingest endpoint |

To swap the chat model (e.g. to `gpt-4o`), edit `application.yml`. To use a different LLM provider for Engram's fact extraction, change `ENGRAM_LLM_PROVIDER` in `docker-compose.yml` — see [Engram's provider docs](https://github.com/jamjet-labs/jamjet/tree/main/runtime/engram-server#llm-providers).

## Windows notes

- Use PowerShell or cmd; `mvnw.cmd` is the entry point instead of `./mvnw`.
- WSL2 users: run from the WSL side for cleanest networking with Docker Desktop.
- The `ghcr.io/jamjet-labs/engram-server:0.5.0` image is multi-arch; Docker Desktop on Windows ARM should work with Linux containers enabled.

## Cleaning up

```bash
docker compose down # stops Engram, removes container + volume
# Press Ctrl-C on the Spring app
```

When you're done, rotate or delete your JamJet API key and OpenAI key in their respective dashboards — they are tied to your account quotas.

## Security

- `.env` is in `.gitignore` — never commit your keys.
- Both Engram and the Spring app bind to `127.0.0.1`. Do not expose this demo on a public network.
- For real apps with PII in prompts, enable [JamJet's redaction settings](https://docs.jamjet.dev/redaction) (Team tier and up). The demo runs on the free tier without redaction — do not pipe production traffic through it.

## What's next

- **The new Python Engram rewrite** (`jamjet-engram` 0.1.0) is the next-generation server — more featureful, better benchmarks, but currently has wire-protocol gaps with the Java starter. Once those gaps close, the docker-compose image is a one-line swap.
- **A separate MCP demo** (`examples/mcp-engram-demo/`, coming soon) targets MCP-protocol-native clients (Cursor, Claude Desktop) instead of Spring Boot ergonomics.
- Read the [JamJet Spring AI integration guide](../../docs/spring-ai-integration.md).

## License

Apache 2.0. See [LICENSE](../../LICENSE).
26 changes: 26 additions & 0 deletions examples/spring-ai-engram-cloud-demo/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
services:
engram:
image: ghcr.io/jamjet-labs/engram-server:0.5.0
container_name: engram-demo
# No explicit command — all configuration is via environment variables.
# "serve" is the default entrypoint; mode=rest and port=9090 are set below.
ports:
- "127.0.0.1:9090:9090" # loopback-only — do not expose Engram on the network
environment:
- ENGRAM_MODE=rest # default is "mcp" (stdio); must be "rest" for HTTP
- ENGRAM_LLM_PROVIDER=openai-compatible # real fact extraction so the demo's memory actually works
- ENGRAM_OPENAI_BASE_URL=https://api.openai.com/v1
- OPENAI_API_KEY=${OPENAI_API_KEY:?OPENAI_API_KEY required for Engram fact extraction; copy .env.example to .env and paste your key}
- ENGRAM_EMBEDDING_PROVIDER=mock # mock embeddings (768d, deterministic) — fine for a 3-fact demo without an Ollama prereq
volumes:
- engram-data:/data # ENGRAM_DB_PATH defaults to /data/engram.db inside the image
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/health"]
interval: 2s
timeout: 1s
retries: 15
Comment on lines +17 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Verify wget is available in the engram-server container image.

The Engram server is a Rust binary distributed as ghcr.io/jamjet-labs/engram-server:0.5.0. Minimal Rust containers frequently ship without wget or curl. If the binary is absent, Docker marks the container perpetually unhealthy even when the port is responding correctly — which degrades DX (docker ps always shows unhealthy) and would break any future depends_on: condition: service_healthy usage.

A safer fallback is using /bin/sh -c with a POSIX-compliant /dev/tcp redirect or nc, both of which are more likely to be present:

🛡️ Alternative healthcheck (if wget is absent)
     healthcheck:
-      test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/health"]
+      test: ["CMD", "sh", "-c", "wget -q --spider http://localhost:9090/health || curl -sf http://localhost:9090/health"]
       interval: 2s
       timeout: 1s
       retries: 15
       start_period: 5s

Or, if neither is available, use a side-car healthcheck script bundled in the image. Verify with:

docker run --rm --entrypoint sh ghcr.io/jamjet-labs/engram-server:0.5.0 -c "which wget || which curl || echo NONE"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/health"]
interval: 2s
timeout: 1s
retries: 15
healthcheck:
test: ["CMD", "sh", "-c", "wget -q --spider http://localhost:9090/health || curl -sf http://localhost:9090/health"]
interval: 2s
timeout: 1s
retries: 15
start_period: 5s
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/spring-ai-engram-cloud-demo/docker-compose.yml` around lines 17 -
21, The healthcheck currently uses wget, which may not exist in the
engram-server image (ghcr.io/jamjet-labs/engram-server:0.5.0); update the
docker-compose healthcheck block to use a more portable check: replace the wget
test with a /bin/sh -c command that tries a POSIX /dev/tcp connection or falls
back to checking curl/nc if available, and if neither exists, document adding a
small sidecar or health script into the image; ensure the change is made in the
healthcheck: test entry so the container reports healthy when the server port
responds.

start_period: 5s
restart: unless-stopped

volumes:
engram-data:
Loading
Loading