-
Notifications
You must be signed in to change notification settings - Fork 0
feat(examples): end-to-end JamJet Cloud SDK example (Java) #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f0c6ee1
51e49f0
80ad35c
0d7c681
0b9a50a
2b9ac11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # JamJet Cloud SDK Demo (Java) | ||
|
|
||
| A Java app using the JamJet Cloud SDK end to end: it emits **spans** (observability: | ||
| model, tokens, cost) to JamJet Cloud and **AgentBoundary Action Receipts** (audit) for | ||
| tool calls. Two flavours: | ||
|
|
||
| - **Spring Boot** (`CloudDemoApplication`): `jamjet-cloud-spring-boot-starter` auto-wires | ||
| span observability; an `ActionReceiptAdvisor` + a `FileActionReceiptEmitter` add receipts | ||
| for `@Tool` calls. Spans are zero-config; receipts are one advisor + emitter bean. | ||
| - **Plain Java** (`PlainJavaCloudDemo`): the raw `JamjetCloud` / `Span` / `ActionReceipt` API. | ||
|
|
||
| ## Build & test (hermetic, no secrets) | ||
|
|
||
| From the repo root: | ||
| ```bash | ||
| mvn -pl examples/jamjet-cloud-sdk-demo -am test | ||
| ``` | ||
| Tests stub JamJet Cloud with WireMock and use a deterministic model, so they need no API keys. | ||
|
|
||
| ## Live mode (see it in the dashboard) | ||
|
|
||
| ```bash | ||
| export JJ_API_KEY=... # your JamJet Cloud key | ||
| export OPENAI_API_KEY=... # for the Spring demo's real model | ||
|
|
||
| # Spring demo | ||
| mvn -pl examples/jamjet-cloud-sdk-demo spring-boot:run | ||
|
|
||
| # Plain-Java demo | ||
| mvn -pl examples/jamjet-cloud-sdk-demo -am package -DskipTests | ||
| java -cp examples/jamjet-cloud-sdk-demo/target/classes:... dev.jamjet.example.cloud.PlainJavaCloudDemo | ||
| ``` | ||
|
Comment on lines
+30
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Plain-Java run command is not copy-paste runnable. At Line 31, 🤖 Prompt for AI Agents |
||
| Then: the **span** appears in the JamJet Cloud dashboard for project `cloud-sdk-demo`, and | ||
| the **receipt** is written to `~/.jamjet/audit/cloud-sdk-demo.jsonl`. | ||
|
|
||
| ## What is and isn't shown | ||
|
|
||
| Spans are the cloud-visible artifact today. Receipts are emitted locally (file/log) -- the | ||
| SDK's cloud-posting receipt emitter is planned, not yet shipped. The Java Cloud SDK does | ||
| **not** yet include local policy enforcement, PII redaction, an approval UX, or cache | ||
| injection; those exist in the TypeScript/Python SDKs and the `jamjet-policy` tooling. | ||
|
|
||
| ## Build note | ||
|
|
||
| This example is a reactor module so it builds against the in-repo SDK. Once | ||
| `jamjet-cloud-sdk` is published to Maven Central it can be split out as a standalone project. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
|
|
||
| <parent> | ||
| <groupId>dev.jamjet</groupId> | ||
| <artifactId>jamjet-runtime-java-parent</artifactId> | ||
| <version>0.3.1</version> | ||
| <relativePath>../../pom.xml</relativePath> | ||
| </parent> | ||
|
|
||
| <artifactId>jamjet-cloud-sdk-demo</artifactId> | ||
| <packaging>jar</packaging> | ||
| <name>JamJet Cloud SDK Demo</name> | ||
| <description>End-to-end example: Java app using the JamJet Cloud SDK (spans + AgentBoundary receipts).</description> | ||
|
|
||
| <properties> | ||
| <spring-boot.version>3.3.5</spring-boot.version> | ||
| <spring-ai.version>1.0.0</spring-ai.version> | ||
| <wiremock.version>3.9.1</wiremock.version> | ||
| </properties> | ||
|
|
||
| <dependencyManagement> | ||
| <dependencies> | ||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-dependencies</artifactId> | ||
| <version>${spring-boot.version}</version> | ||
| <type>pom</type> | ||
| <scope>import</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.springframework.ai</groupId> | ||
| <artifactId>spring-ai-bom</artifactId> | ||
| <version>${spring-ai.version}</version> | ||
| <type>pom</type> | ||
| <scope>import</scope> | ||
| </dependency> | ||
| </dependencies> | ||
| </dependencyManagement> | ||
|
|
||
| <dependencies> | ||
| <dependency> | ||
| <groupId>dev.jamjet</groupId> | ||
| <artifactId>jamjet-cloud-spring-boot-starter</artifactId> | ||
| <version>${project.version}</version> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.springframework.ai</groupId> | ||
| <artifactId>spring-ai-starter-model-openai</artifactId> | ||
| </dependency> | ||
| <!-- langchain4j-core is optional in the starter but the JamjetCloudAutoConfiguration | ||
| return type references JamjetChatModelListener which imports it; include here so | ||
| Spring's class introspection can resolve it at context startup. --> | ||
| <dependency> | ||
| <groupId>dev.langchain4j</groupId> | ||
| <artifactId>langchain4j-core</artifactId> | ||
| <version>0.36.2</version> | ||
| </dependency> | ||
|
Comment on lines
+62
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Demo module langchain4j-core declaration =="
rg -n -C2 '<artifactId>langchain4j-core</artifactId>|<version>0\.36\.2</version>' examples/jamjet-cloud-sdk-demo/pom.xml
echo
echo "== Starter module references to langchain4j (if any) =="
fd -i 'pom.xml' | xargs rg -n -C2 'langchain4j|JamjetChatModelListener|ConditionalOnClass' || true
echo
echo "== Parent/version properties that might manage langchain4j =="
rg -n -C2 'langchain4j|spring-ai.version|spring-boot.version' pom.xml examples/jamjet-cloud-sdk-demo/pom.xmlRepository: jamjet-labs/jamjet-runtime-java Length of output: 6368 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== examples/jamjet-cloud-sdk-demo/pom.xml (around langchain4j-core) =="
nl -ba examples/jamjet-cloud-sdk-demo/pom.xml | sed -n '1,140p'
echo
echo "== jamjet-cloud-spring-boot-starter/pom.xml (around langchain4j-core) =="
nl -ba jamjet-cloud-spring-boot-starter/pom.xml | sed -n '1,140p'
echo
echo "== repository root pom.xml (dependencyManagement relevant bits) =="
nl -ba pom.xml | sed -n '1,220p'
echo
echo "== any dependencyManagement for langchain4j-core in repo =="
rg -n --glob 'pom.xml' 'dependencyManagement' pom.xml examples jamjet-cloud-* | head -n 50
echo
echo "== any centralized property for langchain4j-core version =="
rg -n --glob 'pom.xml' 'langchain4j.*version' pom.xml examples jamjet-cloud-* | head -n 50Repository: jamjet-labs/jamjet-runtime-java Length of output: 207 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== examples/jamjet-cloud-sdk-demo/pom.xml (around langchain4j-core) =="
cat -n examples/jamjet-cloud-sdk-demo/pom.xml | sed -n '1,120p'
echo
echo "== jamjet-cloud-spring-boot-starter/pom.xml (around langchain4j-core) =="
cat -n jamjet-cloud-spring-boot-starter/pom.xml | sed -n '1,140p'
echo
echo "== repository root pom.xml (dependencyManagement relevant bits) =="
cat -n pom.xml | sed -n '1,240p'
echo
echo "== dependencyManagement blocks mentioning langchain4j-core =="
rg -n --glob 'pom.xml' 'dependencyManagement' pom.xml examples jamjet-cloud-* || true
echo
echo "== centralized properties for langchain4j version (if any) =="
rg -n --glob 'pom.xml' 'langchain4j.*version' pom.xml examples jamjet-cloud-* || true
echo
echo "== any direct langchain4j-core pinning =="
rg -n --glob 'pom.xml' '<artifactId>langchain4j-core</artifactId>' pom.xml examples jamjet-cloud-* | head -n 50Repository: jamjet-labs/jamjet-runtime-java Length of output: 19231 Avoid duplicating/hard-pinning
🤖 Prompt for AI Agents |
||
|
|
||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-test</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.wiremock</groupId> | ||
| <artifactId>wiremock-standalone</artifactId> | ||
| <version>${wiremock.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.assertj</groupId> | ||
| <artifactId>assertj-core</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| </dependencies> | ||
|
|
||
| <build> | ||
| <plugins> | ||
| <plugin> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-maven-plugin</artifactId> | ||
| <version>${spring-boot.version}</version> | ||
| </plugin> | ||
| </plugins> | ||
| </build> | ||
| </project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package dev.jamjet.example.cloud; | ||
|
|
||
| import dev.jamjet.cloud.spring.ActionReceiptAdvisor; | ||
| import org.springframework.ai.chat.client.ChatClient; | ||
| import org.springframework.ai.chat.model.ChatModel; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Configuration | ||
| public class ChatConfig { | ||
|
|
||
| @Bean | ||
| public ChatClient chatClient(ChatModel chatModel, ActionReceiptAdvisor advisor, LookupTool lookupTool) { | ||
| return ChatClient.builder(chatModel) | ||
| .defaultAdvisors(advisor) | ||
| .defaultTools(lookupTool) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package dev.jamjet.example.cloud; | ||
|
|
||
| import org.springframework.ai.chat.client.ChatClient; | ||
| import org.springframework.boot.CommandLineRunner; | ||
| import org.springframework.boot.SpringApplication; | ||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Profile; | ||
|
|
||
| @SpringBootApplication | ||
| public class CloudDemoApplication { | ||
| public static void main(String[] args) { | ||
| SpringApplication.run(CloudDemoApplication.class, args); | ||
| } | ||
|
|
||
| /** Live run only (not under test): make one governed call. */ | ||
| @Bean | ||
| @Profile("!test") | ||
| CommandLineRunner demo(ChatClient chatClient) { | ||
| return args -> { | ||
| String reply = chatClient.prompt() | ||
| .user("What is the status of order A-100? Use the orderStatus tool.") | ||
| .call() | ||
| .content(); | ||
| System.out.println("Agent reply: " + reply); | ||
| System.out.println("A span was sent to JamJet Cloud; a receipt was written to ~/.jamjet/audit/cloud-sdk-demo.jsonl"); | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package dev.jamjet.example.cloud; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import dev.jamjet.cloud.agentboundary.ActionReceipt; | ||
| import dev.jamjet.cloud.agentboundary.ActionReceiptEmitter; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.UncheckedIOException; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.StandardOpenOption; | ||
|
|
||
| /** | ||
| * Appends each Action Receipt as one JSON line to a file (JSONL), mirroring the | ||
| * {@code ~/.jamjet/audit/} pattern used by the TypeScript/Python SDKs. The Java | ||
| * Cloud SDK does not yet ship a cloud-posting emitter, so this makes the audit | ||
| * artifact tangible and inspectable. | ||
| */ | ||
| public final class FileActionReceiptEmitter implements ActionReceiptEmitter { | ||
|
|
||
| private static final ObjectMapper MAPPER = new ObjectMapper().findAndRegisterModules(); | ||
|
|
||
| private final Path file; | ||
| private final Object lock = new Object(); | ||
|
|
||
| public FileActionReceiptEmitter(Path file) { | ||
| this.file = file; | ||
| } | ||
|
|
||
| @Override | ||
| public void emit(ActionReceipt receipt) { | ||
| try { | ||
| String line = MAPPER.writeValueAsString(receipt) + System.lineSeparator(); | ||
| synchronized (lock) { | ||
| if (file.getParent() != null) { | ||
| Files.createDirectories(file.getParent()); | ||
| } | ||
| Files.writeString(file, line, StandardCharsets.UTF_8, | ||
| StandardOpenOption.CREATE, StandardOpenOption.APPEND); | ||
| } | ||
| } catch (IOException e) { | ||
| throw new UncheckedIOException("failed to write receipt to " + file, e); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package dev.jamjet.example.cloud; | ||
|
|
||
| import org.springframework.ai.tool.annotation.Tool; | ||
| import org.springframework.ai.tool.annotation.ToolParam; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class LookupTool { | ||
| @Tool(description = "Look up the shipping status of an order by its id.") | ||
| public String orderStatus(@ToolParam(description = "The order id, e.g. A-100") String orderId) { | ||
| return "Order " + orderId + " is shipped."; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package dev.jamjet.example.cloud; | ||
|
|
||
| import dev.jamjet.cloud.JamjetCloud; | ||
| import dev.jamjet.cloud.JamjetCloudConfig; | ||
| import dev.jamjet.cloud.Span; | ||
| import dev.jamjet.cloud.agentboundary.*; | ||
|
|
||
| import java.nio.file.Path; | ||
| import java.time.Instant; | ||
| import java.util.UUID; | ||
|
|
||
| /** | ||
| * Plain-Java (no Spring) demo of the raw JamJet Cloud SDK API: | ||
| * open a span (observability) and emit an AgentBoundary receipt (audit). | ||
| */ | ||
| public final class PlainJavaCloudDemo { | ||
|
|
||
| private PlainJavaCloudDemo() {} | ||
|
|
||
| /** Testable entry point: configure the SDK, emit a span, build + emit a receipt. */ | ||
| public static void run(JamjetCloudConfig config, ActionReceiptEmitter emitter) { | ||
| JamjetCloud.configure(config); | ||
|
|
||
| // 1) Observability: a span for a (simulated) model call. | ||
| Span span = JamjetCloud.newSpan("model", "demo-call"); | ||
| span.model("gpt-4o-mini").inputTokens(120).outputTokens(35).costUsd(0.0004); | ||
| span.finish(); | ||
|
|
||
| // 2) Audit: an AgentBoundary Action Receipt for a (simulated) tool call. | ||
| String argsJson = "{\"orderId\":\"A-100\"}"; | ||
| String argsHash = ReceiptHashes.computeArgumentsHash(argsJson); | ||
| String receiptId = UUID.randomUUID().toString(); | ||
| String now = Instant.now().toString(); | ||
| Actor actor = new Actor(ActorType.AGENT, "cloud-sdk-demo", null); | ||
| Agent agent = new Agent("jamjet-runtime-java", "0.3.1", "gpt-4o-mini", null); | ||
| Tool tool = new Tool("orderStatus", null, "orderStatus"); | ||
| Target target = new Target("cloud-sdk-demo", Environment.DEV, "A-100"); | ||
| Policy policy = new Policy("default.allow", "1", PolicyDecision.ALLOW); | ||
| Execution execution = new Execution(ExecutionStatus.SUCCESS, now, null, "shipped"); | ||
| String receiptHash = ReceiptHashes.computeReceiptHash( | ||
| ActionReceipt.CURRENT_VERSION, receiptId, now, actor, agent, tool, target, | ||
| argsHash, policy, null, execution); | ||
| ActionReceipt receipt = new ActionReceipt(ActionReceipt.CURRENT_VERSION, receiptId, now, | ||
| actor, agent, tool, target, argsHash, policy, null, execution, receiptHash); | ||
| java.util.List<String> errors = new ActionReceiptValidator().validate(receipt); | ||
| if (!errors.isEmpty()) { | ||
| throw new IllegalStateException("receipt failed schema validation: " + errors); | ||
| } | ||
| emitter.emit(receipt); | ||
| } | ||
|
|
||
| public static void main(String[] args) { | ||
| String apiKey = System.getenv().getOrDefault("JJ_API_KEY", ""); | ||
| String apiUrl = System.getenv().getOrDefault("JAMJET_API_URL", "https://api.jamjet.dev"); | ||
| Path receipts = Path.of(System.getProperty("user.home"), ".jamjet", "audit", "cloud-sdk-demo.jsonl"); | ||
| if (apiKey.isBlank()) { | ||
| System.err.println("Set JJ_API_KEY to send the span to JamJet Cloud (api.jamjet.dev)."); | ||
| } | ||
| JamjetCloudConfig cfg = JamjetCloudConfig.builder() | ||
| .apiKey(apiKey).apiUrl(apiUrl).project("cloud-sdk-demo").build(); | ||
| run(cfg, new FileActionReceiptEmitter(receipts)); | ||
| System.out.println("Span sent to " + apiUrl + "; receipt written to " + receipts); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,25 @@ | ||||||||||||||||||||||
| package dev.jamjet.example.cloud; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import dev.jamjet.cloud.agentboundary.ActionReceiptEmitter; | ||||||||||||||||||||||
| import dev.jamjet.cloud.spring.ActionReceiptAdvisor; | ||||||||||||||||||||||
| import org.springframework.context.annotation.Bean; | ||||||||||||||||||||||
| import org.springframework.context.annotation.Configuration; | ||||||||||||||||||||||
| import org.springframework.core.Ordered; | ||||||||||||||||||||||
| import org.springframework.core.env.Environment; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import java.nio.file.Path; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Configuration | ||||||||||||||||||||||
| public class ReceiptConfig { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Bean | ||||||||||||||||||||||
| public ActionReceiptEmitter actionReceiptEmitter() { | ||||||||||||||||||||||
| Path file = Path.of(System.getProperty("user.home"), ".jamjet", "audit", "cloud-sdk-demo.jsonl"); | ||||||||||||||||||||||
| return new FileActionReceiptEmitter(file); | ||||||||||||||||||||||
|
Comment on lines
+16
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make receipt file path configurable instead of hardcoding Line 17 persists receipts to a fixed user-home path, which can leak unredacted demo/test data and breaks hermetic test isolation. Prefer a property-backed path with a safe default, then override it in Proposed diff+import org.springframework.beans.factory.annotation.Value;
...
`@Bean`
- public ActionReceiptEmitter actionReceiptEmitter() {
- Path file = Path.of(System.getProperty("user.home"), ".jamjet", "audit", "cloud-sdk-demo.jsonl");
- return new FileActionReceiptEmitter(file);
+ public ActionReceiptEmitter actionReceiptEmitter(
+ `@Value`("${jamjet.cloud.receipt-file:${user.home}/.jamjet/audit/cloud-sdk-demo.jsonl}") String receiptFile) {
+ return new FileActionReceiptEmitter(Path.of(receiptFile));
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Bean | ||||||||||||||||||||||
| public ActionReceiptAdvisor actionReceiptAdvisor(Environment env, ActionReceiptEmitter emitter) { | ||||||||||||||||||||||
| return new ActionReceiptAdvisor(env, emitter, Ordered.LOWEST_PRECEDENCE); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| /** Runnable example: a Java app using the JamJet Cloud SDK end to end. */ | ||
| package dev.jamjet.example.cloud; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| spring: | ||
| ai: | ||
| openai: | ||
| api-key: test-key | ||
| base-url: http://localhost:0 | ||
| jamjet: | ||
| cloud: | ||
| api-key: test-key | ||
| api-url: http://localhost:0 | ||
| batch: | ||
| size: 1 | ||
| interval-ms: 100 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make the demo pointer a clickable Markdown link.
At Line 144, use a link (
[examples/jamjet-cloud-sdk-demo](examples/jamjet-cloud-sdk-demo/)) so readers can navigate directly from rendered docs.🤖 Prompt for AI Agents