feat(examples): spring-ai-engram-cloud-demo — Spring AI + Engram + JamJet Cloud#3
feat(examples): spring-ai-engram-cloud-demo — Spring AI + Engram + JamJet Cloud#3
Conversation
This reverts commit 1c21ef8.
Deviations from original spec (confirmed via CLI inspection): - Added ENGRAM_MODE=rest (default is mcp/stdio; HTTP requires explicit opt-in) - Health endpoint is /health not /healthz (404 confirmed on /healthz) - Added ENGRAM_EMBEDDING_PROVIDER=mock (avoids Ollama dependency for embeddings) - Removed explicit command/port args; port 9090 is already the REST default - Added named volume engram-data for SQLite persistence across restarts - Recall query param is q= not query= (documented for Task 7 EngramClient wiring) - mock LLM provider returns zero facts by design (logged warning from binary)
DemoIntegrationTest: WireMock stubs OpenAI chat completions returning "Acme"; Testcontainers runs real Engram 0.5.0 with mock LLM provider; @SpringBootTest asserts the /chat endpoint returns the stubbed reply. Three companion fixes required to make the test pass: - Upgraded Testcontainers 1.20.4 → 1.21.3 (docker-java 3.4.2) and moved TC BOM before spring-boot-dependencies so the version wins. - Added api.version=1.41 as a Surefire JVM property; TC ≤1.21.x falls back to docker-java API 1.32 when UNKNOWN_VERSION is detected, and OrbStack 2.x dropped support for API < 1.40. - Enabled -parameters compiler flag so Spring MVC resolves @RequestParam names at runtime without relying on debug info. - Excluded JamjetCloudAutoConfiguration (0.2.0 references langchain4j which is not on the test classpath; guards the @SpringBootTest context).
…Autowired on PreflightCheck production constructor
…er auto-registers
…vationHandler directly with context-type filter
- Drop dev.jamjet:jamjet-cloud-spring-boot-starter:0.2.0 dep. Its autoconfig
hard-references LangChain4j classes which forced langchain4j-core onto the
classpath of a Spring-AI-only app.
- Depend on dev.jamjet:jamjet-cloud-sdk:0.2.0 directly + add JamjetCloudConfiguration
in the demo that does the Spring AI half. Cleaner separation; pattern can be
upstreamed as a 'Spring AI only' autoconfig.
- Workaround for jamjet-cloud-sdk:0.2.0 JamjetObservationHandler.supportsContext
bug: it filters by name.startsWith('gen_ai.client'), but Micrometer calls
supportsContext at points where ctx.getName() is still null — handler is never
selected. Replace name-based filter with context-class filter so selection is
name-independent.
- Verified end-to-end: chat-driven gen_ai.client.operation observations now
flow to JamJet Cloud (traces with llm_call counts + cost rollups visible at
/v1/traces). Tested with 3 README curls; 6 demo traces persisted at JamJet.
📝 WalkthroughWalkthroughThis pull request introduces a complete, runnable Spring Boot demonstration application that integrates Spring AI, Engram durable memory storage, and JamJet Cloud observability. It includes Docker Compose orchestration for the Engram service, Maven wrapper infrastructure for cross-platform builds, complete application code with a multi-turn chat agent, startup validation, tests, and comprehensive documentation. ChangesSpring AI + Engram + JamJet Cloud Demo Application
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
examples/spring-ai-engram-cloud-demo/.mvn/wrapper/maven-wrapper.properties (1)
1-3: ⚡ Quick winAdd
distributionSha256Sumfor supply-chain integrity.Without it, the wrapper downloads and executes the Maven binary with no tamper detection. To prevent an attack where a vulnerable repository could distribute malicious Maven artifacts, the downloaded artifacts should be verified against a secure checksum.
distributionSha256Sumhas been supported by Maven Wrapper since 3.2.0.The SHA-256 for
apache-maven-3.9.9-bin.zipis available athttps://downloads.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.zip.sha256.🔒 Proposed addition
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 +distributionSha256Sum=<sha256-of-apache-maven-3.9.9-bin.zip>🤖 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/.mvn/wrapper/maven-wrapper.properties` around lines 1 - 3, Add the distributionSha256Sum property to the Maven wrapper properties so the downloaded Maven distribution is verified; specifically, update the existing properties block that contains wrapperVersion, distributionType, and distributionUrl by adding distributionSha256Sum with the SHA-256 checksum for apache-maven-3.9.9-bin.zip (obtain the exact value from https://downloads.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.zip.sha256) to ensure the wrapper verifies integrity before extracting the distribution.examples/spring-ai-engram-cloud-demo/src/main/resources/application.yml (1)
24-26: ⚡ Quick win
app.engram.health-urlreadsENGRAM_BASE_URLdirectly rather than referencingengram.base-url.If someone overrides
engram.base-urlvia a Spring profile, system property, or config server (rather than the env var),app.engram.health-urlwon't track the change andPreflightCheckwill poll the wrong endpoint. Referencing the resolved property keeps the two in sync:♻️ Proposed fix
-app: - engram: - health-url: ${ENGRAM_BASE_URL:http://127.0.0.1:9090}/health +app: + engram: + health-url: ${engram.base-url}/health🤖 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/src/main/resources/application.yml` around lines 24 - 26, Change app.engram.health-url to reference the resolved engram.base-url property instead of reading ENGRAM_BASE_URL directly: update the YAML so app.engram.health-url uses the engram.base-url property (with the existing ENGRAM_BASE_URL fallback as the default) so that any overrides to engram.base-url via profiles/config server/system properties are picked up; verify PreflightCheck (or any code reading app.engram.health-url) will therefore poll the correct endpoint.examples/spring-ai-engram-cloud-demo/README.md (1)
14-24: 💤 Low valueAdd a language specifier to the architecture diagram code fence.
The fenced code block at line 14 has no language tag, which triggers a markdownlint MD040 warning. Adding
textsilences the linter.📝 Proposed fix
-``` +```text User → POST /chat?session=alice ──→ Spring AI ChatClient🤖 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/README.md` around lines 14 - 24, Add a language specifier "text" to the fenced code block that contains the ASCII architecture diagram (the triple backticks before the User → POST /chat?session=alice diagram) in the README so the markdown linter MD040 is satisfied; locate the opening triple-backtick for the diagram in README.md and change it to ```text (leaving the diagram contents and closing backticks unchanged).examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java (1)
63-66: ⚡ Quick winMagic string for
ChatModelObservationContextclass name is fragile.Since
spring-ai-coreis already on the classpath for this demo, a direct class reference is type-safe and will produce a compile error if the class moves in a future Spring AI release, rather than silently failing to match any context.♻️ Proposed fix: use class literal
+import org.springframework.ai.chat.observation.ChatModelObservationContext; ... public boolean supportsContext(Observation.Context ctx) { - return ctx != null - && ctx.getClass().getName().equals("org.springframework.ai.chat.observation.ChatModelObservationContext"); + return ctx instanceof ChatModelObservationContext; }Using
instanceofalso handles subclasses correctly and is idiomatic.🤖 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/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java` around lines 63 - 66, The supportsContext(Observation.Context ctx) method uses a fragile string comparison for "org.springframework.ai.chat.observation.ChatModelObservationContext"; replace that with a direct type check using the actual class (org.springframework.ai.chat.observation.ChatModelObservationContext) via instanceof so subclasses are handled and the compiler will catch refactors—update the import and change the return to use ctx instanceof ChatModelObservationContext in the supportsContext method.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@examples/spring-ai-engram-cloud-demo/docker-compose.yml`:
- Around line 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.
In `@examples/spring-ai-engram-cloud-demo/pom.xml`:
- Line 18: The project pins spring-boot.version to 3.3.5 which brings vulnerable
tomcat-embed-core; either upgrade the Spring Boot property <spring-boot.version>
to a secure 3.5.x release (e.g., 3.5.14) or add an explicit dependencyManagement
override for tomcat-embed-core to a patched 10.1.54+ version; update the
<spring-boot.version> value in the POM if choosing Option B or add/modify the
tomcat-embed-core artifact entry under <dependencyManagement> to the safe
version if choosing Option A, then run a build to verify no dependency
conflicts.
In
`@examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java`:
- Around line 44-51: The static JamjetCloud.configure call and repeated
observationRegistry.observationConfig().observationHandler registrations in
initJamjetCloud cause duplicate handlers across Spring context reloads; make the
initialization idempotent by guarding the logic (e.g., add a private static
AtomicBoolean initialized flag checked/set in initJamjetCloud) or refactor
registration into a `@Bean` method annotated with `@ConditionalOnMissingBean` so
JamjetCloud.configure(...) and the observation handler are only registered once;
alternatively check observationRegistry.observationConfig() to avoid re-adding
the same handler instance before calling observationHandler().
In
`@examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/MemoryAgent.java`:
- Around line 24-29: The chat(...) method can return null because
chatClient.prompt().call().content() may be null; update the
MemoryAgent.chat(String sessionId, String userMessage) method to capture the
result of chatClient.prompt()...call(), check whether .content() (i.e., the
AssistantMessage content) is null, and return a safe non-null value (for example
an empty string or a default message) to the controller instead of returning
null; reference the existing chat(...) method and use a local variable to hold
the response before performing the null guard and returning the safe value.
---
Nitpick comments:
In `@examples/spring-ai-engram-cloud-demo/.mvn/wrapper/maven-wrapper.properties`:
- Around line 1-3: Add the distributionSha256Sum property to the Maven wrapper
properties so the downloaded Maven distribution is verified; specifically,
update the existing properties block that contains wrapperVersion,
distributionType, and distributionUrl by adding distributionSha256Sum with the
SHA-256 checksum for apache-maven-3.9.9-bin.zip (obtain the exact value from
https://downloads.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.zip.sha256)
to ensure the wrapper verifies integrity before extracting the distribution.
In `@examples/spring-ai-engram-cloud-demo/README.md`:
- Around line 14-24: Add a language specifier "text" to the fenced code block
that contains the ASCII architecture diagram (the triple backticks before the
User → POST /chat?session=alice diagram) in the README so the markdown linter
MD040 is satisfied; locate the opening triple-backtick for the diagram in
README.md and change it to ```text (leaving the diagram contents and closing
backticks unchanged).
In
`@examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java`:
- Around line 63-66: The supportsContext(Observation.Context ctx) method uses a
fragile string comparison for
"org.springframework.ai.chat.observation.ChatModelObservationContext"; replace
that with a direct type check using the actual class
(org.springframework.ai.chat.observation.ChatModelObservationContext) via
instanceof so subclasses are handled and the compiler will catch
refactors—update the import and change the return to use ctx instanceof
ChatModelObservationContext in the supportsContext method.
In `@examples/spring-ai-engram-cloud-demo/src/main/resources/application.yml`:
- Around line 24-26: Change app.engram.health-url to reference the resolved
engram.base-url property instead of reading ENGRAM_BASE_URL directly: update the
YAML so app.engram.health-url uses the engram.base-url property (with the
existing ENGRAM_BASE_URL fallback as the default) so that any overrides to
engram.base-url via profiles/config server/system properties are picked up;
verify PreflightCheck (or any code reading app.engram.health-url) will therefore
poll the correct endpoint.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5613f99e-9eb5-466c-9698-229148b18899
📒 Files selected for processing (17)
examples/spring-ai-engram-cloud-demo/.env.exampleexamples/spring-ai-engram-cloud-demo/.gitignoreexamples/spring-ai-engram-cloud-demo/.mvn/wrapper/maven-wrapper.propertiesexamples/spring-ai-engram-cloud-demo/README.mdexamples/spring-ai-engram-cloud-demo/docker-compose.ymlexamples/spring-ai-engram-cloud-demo/mvnwexamples/spring-ai-engram-cloud-demo/mvnw.cmdexamples/spring-ai-engram-cloud-demo/pom.xmlexamples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/ChatController.javaexamples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/DemoApplication.javaexamples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/MemoryAgent.javaexamples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/MemoryTools.javaexamples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.javaexamples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/startup/PreflightCheck.javaexamples/spring-ai-engram-cloud-demo/src/main/resources/application.ymlexamples/spring-ai-engram-cloud-demo/src/test/java/dev/jamjet/demo/springaiengram/DemoIntegrationTest.javaexamples/spring-ai-engram-cloud-demo/src/test/java/dev/jamjet/demo/springaiengram/PreflightCheckTest.java
| healthcheck: | ||
| test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/health"] | ||
| interval: 2s | ||
| timeout: 1s | ||
| retries: 15 |
There was a problem hiding this comment.
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: 5sOr, 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.
| 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.
| <properties> | ||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
| <maven.compiler.release>21</maven.compiler.release> | ||
| <spring-boot.version>3.3.5</spring-boot.version> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Is Spring AI 1.0.0 compatible with Spring Boot 3.5.x?
💡 Result:
Yes, Spring AI 1.0.0 is compatible with Spring Boot 3.5.x. The official Spring AI reference documentation explicitly states that Spring AI supports Spring Boot 3.4.x and 3.5.x [1][2]. Subsequent 1.0.x patch releases, such as 1.0.4, 1.0.6, and 1.1.x, upgrade their Spring Boot dependency to versions like 3.5.11 and 3.5.14, confirming ongoing compatibility with the 3.5.x line [3][4][5][6]. The Spring AI BOM for 1.0.0 is designed to work alongside Spring Boot's BOM without direct version enforcement on Spring Boot, allowing use with 3.5.x [1][7]. The project's README notes Spring AI 1.1.x (close to 1.0) targets Spring Boot 3.5.x [5][8].
Citations:
- 1: https://docs.spring.io/spring-ai/reference/getting-started.html
- 2: https://docs.spring.io/spring-ai/reference/1.0/getting-started.html
- 3: https://github.com/spring-projects/spring-ai/releases/tag/v1.0.4
- 4: https://github.com/spring-projects/spring-ai/releases/tag/v1.0.6
- 5: https://github.com/spring-projects/spring-ai/blob/main/README.md
- 6: https://github.com/spring-projects/spring-ai/releases/tag/v1.1.5
- 7: https://central.sonatype.com/artifact/org.springframework.ai/spring-ai-bom
- 8: https://libraries.io/maven/org.springframework.ai:spring-ai-bom
Upgrade Spring Boot to address CRITICAL Tomcat CVEs.
Spring Boot 3.5.14 upgrades to Tomcat 10.1.54, while the pinned 3.3.5 ships tomcat-embed-core 10.1.31, which carries 20+ CRITICAL/HIGH vulnerabilities flagged by OSV Scanner — including potential RCE via partial PUT (GHSA-83qj-6fr2-vhqg), multiple TOCTOU races, path traversal, DoS, and auth-bypass CVEs. Spring Boot 3.3 is past its OSS support window, so no patches will land there.
Even as a demo that binds to localhost, the README guides users to run it with real OpenAI and JamJet API keys, which makes the risk non-trivial.
Two remediation paths, fastest-first:
Option A — override Tomcat only (unblocks immediately, minimal compatibility risk):
🔒 Proposed fix — override Tomcat version in properties
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>
<spring-boot.version>3.3.5</spring-boot.version>
+ <tomcat.version>10.1.54</tomcat.version>
<spring-ai.version>1.0.0</spring-ai.version>Option B — upgrade Spring Boot (recommended; also resolves the spring-boot/spring-core HIGH CVEs):
🔒 Proposed fix — upgrade to Spring Boot 3.5.x
- <spring-boot.version>3.3.5</spring-boot.version>
+ <spring-boot.version>3.5.14</spring-boot.version>Spring AI 1.0.0 is compatible with Spring Boot 3.5.x per the official Spring AI documentation; subsequent releases (1.0.4+, 1.1.x) also target 3.5.x, confirming stable support.
📝 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.
| <spring-boot.version>3.3.5</spring-boot.version> | |
| <spring-boot.version>3.3.5</spring-boot.version> | |
| <tomcat.version>10.1.54</tomcat.version> |
| <spring-boot.version>3.3.5</spring-boot.version> | |
| <spring-boot.version>3.5.14</spring-boot.version> |
🤖 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/pom.xml` at line 18, The project pins
spring-boot.version to 3.3.5 which brings vulnerable tomcat-embed-core; either
upgrade the Spring Boot property <spring-boot.version> to a secure 3.5.x release
(e.g., 3.5.14) or add an explicit dependencyManagement override for
tomcat-embed-core to a patched 10.1.54+ version; update the
<spring-boot.version> value in the POM if choosing Option B or add/modify the
tomcat-embed-core artifact entry under <dependencyManagement> to the safe
version if choosing Option A, then run a build to verify no dependency
conflicts.
| @PostConstruct | ||
| void initJamjetCloud() { | ||
| JamjetCloud.configure(JamjetCloudConfig.builder() | ||
| .apiKey(apiKey) | ||
| .apiUrl(apiUrl) | ||
| .agentName("spring-ai-engram-demo") | ||
| .build()); | ||
|
|
There was a problem hiding this comment.
JamjetCloud.configure and handler registration are not idempotent across Spring context reloads.
JamjetCloud.configure(...) is a static call and observationRegistry.observationConfig().observationHandler(...) appends to a global list. In integration tests that spin up multiple Spring contexts (e.g., DemoIntegrationTest with WireMock + Testcontainers), each context initialization adds another handler instance to the same ObservationRegistry, potentially producing duplicate trace spans per LLM call.
Consider guarding with an AtomicBoolean or extracting to a @Bean with @ConditionalOnMissingBean, or tracking test suite behavior to confirm this isn't observed in practice.
Also applies to: 60-69
🤖 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/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java`
around lines 44 - 51, The static JamjetCloud.configure call and repeated
observationRegistry.observationConfig().observationHandler registrations in
initJamjetCloud cause duplicate handlers across Spring context reloads; make the
initialization idempotent by guarding the logic (e.g., add a private static
AtomicBoolean initialized flag checked/set in initJamjetCloud) or refactor
registration into a `@Bean` method annotated with `@ConditionalOnMissingBean` so
JamjetCloud.configure(...) and the observation handler are only registered once;
alternatively check observationRegistry.observationConfig() to avoid re-adding
the same handler instance before calling observationHandler().
| public String chat(String sessionId, String userMessage) { | ||
| return chatClient.prompt() | ||
| .user("[session=" + sessionId + "] " + userMessage) | ||
| .call() | ||
| .content(); | ||
| } |
There was a problem hiding this comment.
content() can return null; guard before returning to the controller.
content() is shorthand for .chatResponse().getResult().getOutput().getContent(), and getContent() on AssistantMessage is nullable. If the model satisfies the request via a tool call and emits no text in the final turn, chat() returns null, which the controller will pass directly as the HTTP response body.
🛡️ Proposed fix
public String chat(String sessionId, String userMessage) {
- return chatClient.prompt()
+ String content = chatClient.prompt()
.user("[session=" + sessionId + "] " + userMessage)
.call()
.content();
+ return content != null ? content : "";
}📝 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.
| public String chat(String sessionId, String userMessage) { | |
| return chatClient.prompt() | |
| .user("[session=" + sessionId + "] " + userMessage) | |
| .call() | |
| .content(); | |
| } | |
| public String chat(String sessionId, String userMessage) { | |
| String content = chatClient.prompt() | |
| .user("[session=" + sessionId + "] " + userMessage) | |
| .call() | |
| .content(); | |
| return content != null ? content : ""; | |
| } |
🤖 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/src/main/java/dev/jamjet/demo/springaiengram/MemoryAgent.java`
around lines 24 - 29, The chat(...) method can return null because
chatClient.prompt().call().content() may be null; update the
MemoryAgent.chat(String sessionId, String userMessage) method to capture the
result of chatClient.prompt()...call(), check whether .content() (i.e., the
AssistantMessage content) is null, and return a safe non-null value (for example
an empty string or a default message) to the controller instead of returning
null; reference the existing chat(...) method and use a local variable to hold
the response before performing the null guard and returning the safe value.
Summary
Adds the first reference demo for the JamJet JVM stack: a Spring AI 1.0 chat agent with durable memory backed by Engram and cloud observability shipped to JamJet via the existing
JamjetObservationHandler.Three Spring Boot starters wire the whole thing:
org.springframework.ai:spring-ai-starter-model-openai:1.0.0dev.jamjet:engram-spring-boot-starter:0.2.0(autoconfiguresEngramClientagainstengram.base-url)dev.jamjet:jamjet-cloud-sdk:0.2.0(cloud observability — wired explicitly via a small demo-sideJamjetCloudConfiguration; see "Workarounds" below)Demo narrative
Each chat call produces a JamJet trace with: 1 LLM span (OpenAI), N Engram tool spans (
rememberFact/recallFacts), cost rollup. Verified end-to-end againstcloud.jamjet.dev.Workarounds documented
While building this demo I hit two real bugs in
dev.jamjet:jamjet-cloud-spring-boot-starter:0.2.0that are filed as upstream followups:ChatModelListenerPostProcessorreferencesdev.langchain4j.model.chat.listener.ChatModelListenerdirectly without@ConditionalOnClassprotection at the @bean signature level, so Spring-AI-only consumers getClassNotFoundExceptionat startup. Workaround: skip the starter, depend onjamjet-cloud-sdk:0.2.0directly + a tiny demo-sideJamjetCloudConfiguration.javathat does the Spring AI half.JamjetObservationHandler.supportsContextfilters by name —ctx.getName().startsWith("gen_ai.client")is checked when Micrometer's lifecycle still has a null context name, so the handler is never selected for any observation. Workaround: wrap with a context-class filter (ChatModelObservationContext) so selection is name-independent.Both workarounds are confined to
JamjetCloudConfiguration.javaand are clearly documented in the source. Once the starter is fixed (separate PR), this demo simplifies back to usingjamjet-cloud-spring-boot-starterdirectly.What's in
examples/real-agent/convention — no parent, own groupIddev.jamjet.examples)docker-compose.yml— Engram (Rust 0.5.0) on port 9090, configured for OpenAI-compatible LLM provider so fact extraction works.env.example— placeholder keys with sign-up URLsPreflightCheck— cross-platform Java env-var validator + Engram health-poller (replaces arun.shshell script idea)MemoryTools+MemoryAgent+ChatController— idiomatic Spring AIJamjetCloudConfiguration— explicit cloud observability wiring (workarounds documented above)DemoIntegrationTest—@SpringBootTest+ WireMock OpenAI + Testcontainers Engram (Rust 0.5.0 with mock LLM provider for determinism)Test plan
./mvnw verify— all tests pass (PreflightCheck unit tests + DemoIntegrationTest)https://api.jamjet.dev/v1/tracestaggedagents=['spring-ai-engram-demo']with proper LLM call counts and cost rollups (~$0.000045–$0.00018 per chat turn)docker compose downremoves container + network)Path forward
This demo is immediately mergeable. The
JamjetObservationHandler-based wiring works correctly given the workarounds.Once the related
jamjet-cloud:feat/otlp-ingestPR ships (OTLP/JSON intake), this demo can be simplified to use Spring AI's standard OTLP exporter pointed at/v1/otlp/v1/traces— same trivial pattern as Koog. That cleanup is filed as a followup, not blocking this merge.Stacked PR
The Kotlin Koog demo (
feat/kotlin-koog-engram-demo) is stacked on this branch. It will be opened as a separate PR after this one merges.Summary by CodeRabbit
Release Notes