From 37b3c77c8153f7f4220f0925e64711ede3d69332 Mon Sep 17 00:00:00 2001 From: marco sappe griot Date: Thu, 4 Jun 2026 15:58:13 +0200 Subject: [PATCH 1/2] [741] Create the lra-ai-dashboard quickstart Using mutiny 3.1.1 for compatibility to quarkus-langchain4j-ollama --- pom.xml | 4 + rts/lra/lra-ai-dashboard/README.md | 246 ++++++++++++++++++ rts/lra/lra-ai-dashboard/pom.xml | 92 +++++++ .../io/narayana/lra/ai/LraAiChatResource.java | 58 +++++ .../java/io/narayana/lra/ai/LraAssistant.java | 100 +++++++ .../java/io/narayana/lra/ai/LraTools.java | 163 ++++++++++++ .../resources/META-INF/resources/index.html | 103 ++++++++ .../src/main/resources/application.properties | 49 ++++ rts/lra/pom.xml | 1 + 9 files changed, 816 insertions(+) create mode 100644 rts/lra/lra-ai-dashboard/README.md create mode 100644 rts/lra/lra-ai-dashboard/pom.xml create mode 100644 rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraAiChatResource.java create mode 100644 rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraAssistant.java create mode 100644 rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraTools.java create mode 100644 rts/lra/lra-ai-dashboard/src/main/resources/META-INF/resources/index.html create mode 100644 rts/lra/lra-ai-dashboard/src/main/resources/application.properties diff --git a/pom.xml b/pom.xml index 99acfb452b..4a56ca2ff9 100644 --- a/pom.xml +++ b/pom.xml @@ -170,6 +170,10 @@ 3.0.2 6.1.0 3.15.0 + + 3.1.1 10.9.1 2.26.0 1.10.2.Final diff --git a/rts/lra/lra-ai-dashboard/README.md b/rts/lra/lra-ai-dashboard/README.md new file mode 100644 index 0000000000..a6d6e3d3a5 --- /dev/null +++ b/rts/lra/lra-ai-dashboard/README.md @@ -0,0 +1,246 @@ +# LRA AI Dashboard + +A Quarkus application that puts an LLM in front of the Narayana LRA coordinator REST API. +Operators ask natural-language questions; the LLM calls coordinator endpoints as tools, correlates the results, and explains what is happening — including root causes and remediation steps — in plain English. + +--- + +## Architecture + +``` +Browser / curl + │ POST /chat {"message": "Why is LRA X stuck?"} + ▼ +LraAiChatResource (JAX-RS endpoint on port 8082) + │ assistant.chat(message) + ▼ +LraAssistant (LangChain4j AI Service interface) + │ System prompt encodes the full LRA state machine and recovery protocol + │ LangChain4j dispatches tool calls to LraTools as needed + ▼ +LLM (Ollama / OpenAI) + │ selects and calls one or more tools + ▼ +LraTools (@ApplicationScoped CDI bean) + │ java.net.http.HttpClient — plain HTTP GET requests + ▼ +LRA Coordinator (http://localhost:8080/lra-coordinator) +``` + +Tool calls (read or write) execute synchronously before the LLM begins streaming its text +response. The LLM may call multiple tools in sequence, correlating results across calls to +diagnose multi-participant failure cascades that are not apparent from any single API call. + +--- + +## Prerequisites + +| Component | Version | Notes | +|-----------|---------|-------| +| Java | 17+ | | +| Maven | 3.9+ | | +| LRA Coordinator | any | `quay.io/jbosstm/lra-coordinator:latest`, running on `localhost:8080` by default | +| Ollama | any | Running on `localhost:11434` by default | +| llama3.1 (or compatible) | — | Must support tool/function calling | + +### Install and start Ollama + +```bash +# macOS +brew install ollama + +# Linux +curl -fsSL https://ollama.com/install.sh | sh + +# Pull the configured model +ollama pull llama3.1 + +# Start the server (if not already running as a service) +ollama serve +``` + +> **Tool-calling requirement:** The LLM must support Ollama's tool-calling API. +> `llama3.1` (the default) and `llama3.2`, `mistral-nemo`, `qwen2.5` all work. +> `llama3` (without `.1`) does **not** support tool calling and will return HTTP 400. +> To switch model: change `quarkus.langchain4j.ollama.chat-model.model-id` in +> `application.properties` and run `ollama pull `. + +--- + +## Quick start + +### Step 1 — Start the LRA coordinator + +```bash +podman run --network host quay.io/jbosstm/lra-coordinator:latest + +# Confirm it is running (should return a JSON array) +curl http://localhost:8080/lra-coordinator +``` + +### Step 2 — Start Ollama + +```bash +ollama serve & # no-op if already running as a service +``` + +### Step 3 — Start the AI dashboard + +```bash +cd rts/lra/lra-ai-dashboard +mvn quarkus:dev +``` + +Open **http://localhost:8082** for the browser chat UI, or use curl: + +```bash +curl -s -X POST http://localhost:8082/chat \ + -H "Content-Type: application/json" \ + -d '{"message": "Are there any stuck transactions?"}' | jq . +``` + +--- + +## Configuration + +All settings are in `src/main/resources/application.properties`. + +| Property | Default | Purpose | +|----------|---------|---------| +| `quarkus.http.port` | `8082` | Avoids conflict with coordinator on 8080 | +| `lra.coordinator.url` | `http://localhost:8080/lra-coordinator` | Injected into `LraTools` as the base URL | +| `quarkus.langchain4j.ollama.chat-model.model-id` | `llama3.1` | Ollama model name (must support tool calling) | +| `quarkus.langchain4j.ollama.base-url` | `http://localhost:11434` | Ollama server URL | +| `quarkus.langchain4j.ollama.timeout` | `120s` | Generous timeout for local inference | + +To override at startup without editing the file: + +```bash +mvn quarkus:dev \ + -Dlra.coordinator.url=http://coordinator-host:8080/lra-coordinator \ + -Dquarkus.langchain4j.ollama.chat-model.model-id=mistral-nemo +``` + +--- + +## Switching to OpenAI + +For cloud deployments where Ollama is unavailable: + +1. In `pom.xml`, swap the commented/active LangChain4j dependency: + + ```xml + + + io.quarkiverse.langchain4j + quarkus-langchain4j-ollama + + + + + io.quarkiverse.langchain4j + quarkus-langchain4j-openai + + ``` + +2. In `application.properties`, comment out the Ollama block and uncomment the OpenAI block. + +3. Export your key and start: + + ```bash + export LRA_AI_API_KEY=sk-... + mvn quarkus:dev + ``` + +--- + +## Source files + +### `LraTools.java` + +Six `@Tool`-annotated methods that form the saga-domain tool schema described in the patent. +Each method makes a single blocking HTTP GET to the coordinator and returns the raw JSON response +for the LLM to reason over. + +| Method | Coordinator endpoint | When the LLM calls it | +|--------|---------------------|----------------------| +| `listAllLRAs()` | `GET /lra-coordinator/` | Overview of all transactions | +| `listLRAsByStatus(status)` | `GET /lra-coordinator/?Status=X` | Narrow focus to a specific state | +| `getLRADetails(lraId)` | `GET {lraId}` | Full participant breakdown for one LRA | +| `getLRAStatus(lraId)` | `GET {lraId}/status` | Cheap status-only check | +| `listRecoveringLRAs()` | `GET /lra-coordinator/recovery` | Confirm auto-recovery is running | +| `listFailedLRAs()` | `GET /lra-coordinator/recovery/failed` | Find transactions needing manual action | +| `closeLRA(lraId)` | `PUT {lraId}/close` | Operator-requested completion | +| `cancelLRA(lraId)` | `PUT {lraId}/cancel` | Operator-requested compensation | +| `startLRA(clientId, timeLimitMs)` | `POST /lra-coordinator/start` | Operator-requested new transaction | + +The write tools (`closeLRA`, `cancelLRA`) are guarded in the system prompt: the LLM will +only call them when the operator explicitly requests it and will echo the target LRA ID +before acting. + +The LRA ID is the full resource URI (e.g. `http://localhost:8080/lra-coordinator/0_ffff...`), +so `getLRADetails` and `getLRAStatus` are plain GETs on that URI with no path manipulation. +`java.net.http.HttpClient` is used directly to avoid classpath conflicts with the `lra-client` +module's RESTEasy dependency. + +### `LraAssistant.java` + +A LangChain4j AI Service interface. The `@SystemMessage` annotation encodes: +- All LRA lifecycle states and their transitions +- All participant status values and what each means +- The recovery protocol (automatic retry, when manual intervention is needed) +- Nested LRA failure propagation +- How to approach diagnosis (gather data first, then correlate and explain) + +LangChain4j binds this interface to the configured LLM at startup and injects `LraTools` +as the tool provider. The result is a CDI bean injectable anywhere in the application. + +### `LraAiChatResource.java` + +A single `POST /chat` endpoint. +No `@Blocking` is needed: `Multi` is handled natively by RESTEasy Reactive without +occupying the I/O thread. + +--- + +## Example queries + +``` +Show me all active LRAs. + +How many LRAs are currently in each state? + +Are there any failed or stuck transactions? + +Why is LRA http://localhost:8080/lra-coordinator/0_ffff7f000001_... stuck? + +Is the recovery coordinator doing anything right now? + +Which LRAs need manual intervention? +``` + +### Multi-step reasoning example + +**Query:** *"Is there anything wrong right now?"* + +A typical LLM reasoning chain: +1. Calls `listAllLRAs()` → spots several in `FailedToCancel` +2. Calls `listLRAsByStatus("FailedToCancel")` → gets IDs +3. Calls `getLRADetails(id)` for each → finds one participant with `FailedToCompensate` +4. Calls `listRecoveringLRAs()` → confirms that LRA is in the recovery queue +5. **Response:** *"There are 3 LRAs in FailedToCancel state. LRA `0_ffff...` has been + stuck since participant `https://payment-service/compensate` returned FailedToCompensate. + The recovery coordinator is actively retrying it. If the payment service is still down + you will need to restore it and wait for the next retry cycle (approx. 2 minutes), + or use the recovery coordinator API to force a terminal state manually."* + +--- + +## Extending the PoC + +| Extension | What to add | +|-----------|-------------| +| **forceRecovery tool** | `PUT /lra-coordinator/recovery/{id}` to force a stuck participant to a terminal state | +| **Proactive alerts** | Quarkus `@Scheduled` job calls `listFailedLRAs()` periodically; LLM generates alert if count > threshold | +| **Chat memory** | Add `@MemoryId` parameter for multi-turn operator sessions | +| **Multi-coordinator** | Aggregate state from all cluster nodes (see HA patent) for a cluster-wide view | diff --git a/rts/lra/lra-ai-dashboard/pom.xml b/rts/lra/lra-ai-dashboard/pom.xml new file mode 100644 index 0000000000..d85a429c35 --- /dev/null +++ b/rts/lra/lra-ai-dashboard/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + + org.jboss.narayana.quickstart.rts.lra + lra-quickstarts + 7.3.5.Final-SNAPSHOT + ../pom.xml + + + lra-ai-dashboard + jar + LRA AI Dashboard + LLM-Tool-Assisted Distributed Saga Coordination — chat-driven LRA operator dashboard + + + + + io.quarkus + quarkus-bom + ${quarkus.platform.version} + pom + import + + + io.quarkus.platform + quarkus-langchain4j-bom + ${quarkus.platform.version} + pom + import + + + + io.smallrye.reactive + mutiny + ${version.mutiny} + + + + + + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-rest-jackson + + + + io.quarkiverse.langchain4j + quarkus-langchain4j-ollama + + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${version.maven-compiler-plugin} + + + + diff --git a/rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraAiChatResource.java b/rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraAiChatResource.java new file mode 100644 index 0000000000..69aa3afbd7 --- /dev/null +++ b/rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraAiChatResource.java @@ -0,0 +1,58 @@ +/* + Copyright The Narayana Authors + SPDX-License-Identifier: Apache-2.0 + */ + +package io.narayana.lra.ai; + +import io.smallrye.mutiny.Multi; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.jboss.logging.Logger; + +/** + * JAX-RS endpoint exposing the LRA AI assistant as a streaming chat API. + * + * POST /chat {"message": "Why is LRA abc-123 stuck?"} + * → chunked text/plain stream of tokens + * + * Plain TEXT_PLAIN is used rather than SSE: RESTEasy Reactive streams each + * Multi item as a chunk immediately, and the client appends raw bytes + * with no format-stripping. SSE would require the client to parse data: lines + * and re-join newlines split by the SSE encoder, causing spaces and line breaks + * to be lost. + * + * No @Blocking is needed: Multi is reactive and RESTEasy Reactive handles + * it natively without blocking the I/O thread. + */ +@Path("/chat") +@ApplicationScoped +public class LraAiChatResource { + + private static final Logger log = Logger.getLogger(LraAiChatResource.class); + + @Inject + LraAssistant assistant; + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) + public Multi chat(ChatRequest request) { + if (request == null || request.message() == null || request.message().isBlank()) { + throw new BadRequestException("Message must not be empty"); + } + return assistant.chat(request.message()) + .onFailure().recoverWithItem(t -> { + log.errorf(t, "LLM call failed"); + return "\n\n[Error: " + t.getMessage() + "]"; + }); + } + + public record ChatRequest(String message) {} +} diff --git a/rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraAssistant.java b/rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraAssistant.java new file mode 100644 index 0000000000..69445cc7b7 --- /dev/null +++ b/rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraAssistant.java @@ -0,0 +1,100 @@ +/* + Copyright The Narayana Authors + SPDX-License-Identifier: Apache-2.0 + */ + +package io.narayana.lra.ai; + +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.UserMessage; +import io.quarkiverse.langchain4j.RegisterAiService; +import io.smallrye.mutiny.Multi; +import jakarta.enterprise.context.ApplicationScoped; + +/** + * LLM-backed assistant for the Narayana LRA coordinator. + * + * Returns Multi so the response streams token-by-token to the client + * via SSE. Tool calls (coordinator REST queries, close/cancel actions) still + * execute synchronously before the final text starts streaming. + */ +@RegisterAiService(tools = LraTools.class) +@ApplicationScoped +public interface LraAssistant { + + @SystemMessage(""" + You are an expert assistant for the Narayana LRA (Long Running Actions) coordinator, + a distributed saga transaction manager implementing the MicroProfile LRA specification. + + LRA LIFECYCLE STATES: + Active — transaction is in progress, participants are being enlisted + Closing — all participants asked to complete (happy path) + Closed — all participants successfully completed + Cancelling — all participants asked to compensate (failure path) + Cancelled — all participants successfully compensated + FailedToClose — one or more participants could not complete; needs investigation + FailedToCancel — one or more participants could not compensate; needs manual intervention + + PARTICIPANT STATUS VALUES: + Completing — participant received complete request, still processing + Completed — participant successfully completed (terminal) + Compensating — participant received compensate request, still processing + Compensated — participant successfully compensated (terminal) + FailedToComplete — participant completion failed permanently; blocks LRA from closing + FailedToCompensate — participant compensation failed permanently; blocks LRA from cancelling + + RECOVERY PROTOCOL: + The recovery coordinator automatically retries FailedToComplete and FailedToCompensate + participants on a periodic schedule (typically every 2 minutes). An LRA appears in the + recovering list while at least one participant has not reached a terminal state. + Persistent failures (service unreachable, business logic rejection) require manual + operator action: investigate the participant's service health, fix the root cause, + then optionally use the recovery coordinator API to force a terminal state. + + NESTED LRAs: + A child LRA failure propagates to its parent. If a child LRA fails to close, + the parent is asked to cancel, triggering a compensation cascade up the hierarchy. + + FAILED LRA STORE: + FailedToCancel and FailedToClose LRAs are NOT visible in the main listing returned + by listAllLRAs(). Once an LRA reaches one of these terminal failure states the + coordinator moves it permanently to a separate failed store (recovery/failed). + It stays there indefinitely until an operator explicitly deletes it. + ALWAYS call listFailedLRAs() in addition to listAllLRAs() whenever the operator + asks whether anything is wrong, stuck, or needs attention. Reporting "nothing is + wrong" based only on listAllLRAs() is incorrect — silent failed LRAs in the store + are the most critical problem class and must never be omitted from a health check. + + RECOVERING A FAILED LRA: + Once an LRA is FailedToCancel or FailedToClose the coordinator has permanently given + up. The recovery module no longer scans it. Calling cancelLRA or closeLRA on it + returns 412 Precondition Failed. There is NO automatic retry path. + The correct manual resolution process is: + 1. Identify the stuck participant(s) via getLRADetails on the failed LRA. + 2. Instruct the operator to call the participant's compensate (or complete) endpoint + directly — the operator or their tooling must drive the business-level action. + 3. Once the operator confirms that the participant has compensated (or completed), + call deleteFailedLRA to remove the coordinator record and close the books. + Do NOT call cancelLRA or closeLRA on a FailedToCancel/FailedToClose LRA. + deleteFailedLRA is the only valid write operation on a failed LRA. + + DIAGNOSTIC APPROACH: + 1. For any health or status question: call BOTH listAllLRAs() AND listFailedLRAs(). + 2. Use the available tools to gather live coordinator state — do not guess. + 3. Correlate data across multiple tool calls to trace failure cascades. + 4. Identify exactly which participant(s) are blocking the LRA and why. + 5. Explain the root cause in terms of the LRA state machine. + 6. Suggest specific, actionable remediation steps appropriate to the failure mode. + + WRITE OPERATIONS (startLRA / closeLRA / cancelLRA): + These tools change coordinator state and cannot be undone easily. + Only call them when the operator explicitly asks you to start, close, or cancel an LRA. + Before calling closeLRA or cancelLRA, restate the LRA ID you are about to act on. + For startLRA, confirm the clientId and timeout with the operator before proceeding. + Never call write tools speculatively during diagnosis. + + Always reference LRA IDs and participant URIs precisely when diagnosing specific transactions. + If the coordinator is unreachable, say so clearly and suggest checking if it is running. + """) + Multi chat(@UserMessage String message); +} diff --git a/rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraTools.java b/rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraTools.java new file mode 100644 index 0000000000..c8968cc992 --- /dev/null +++ b/rts/lra/lra-ai-dashboard/src/main/java/io/narayana/lra/ai/LraTools.java @@ -0,0 +1,163 @@ +/* + Copyright The Narayana Authors + SPDX-License-Identifier: Apache-2.0 + */ + +package io.narayana.lra.ai; + +import dev.langchain4j.agent.tool.P; +import dev.langchain4j.agent.tool.Tool; +import jakarta.enterprise.context.ApplicationScoped; +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * LangChain4j tool set for querying the Narayana LRA coordinator. + * + * Each method maps to one coordinator REST endpoint and carries a natural-language + * description so the LLM knows when and why to call it. + * + * The LRA ID is always a full URI (e.g. http://host/lra-coordinator/), + * so detail/status calls reduce to a plain GET on that URI. + */ +@ApplicationScoped +public class LraTools { + + @ConfigProperty(name = "lra.coordinator.url", defaultValue = "http://localhost:8080/lra-coordinator") + String coordinatorUrl; + + private final HttpClient http = HttpClient.newHttpClient(); + + @Tool("Returns all currently tracked LRAs — active, closing, cancelled, and recently completed — with their status, clientId, and recovery flag. Use this to get an overview of the coordinator state or count how many LRAs are in each lifecycle phase. IMPORTANT: FailedToCancel and FailedToClose LRAs are NOT included here — they are permanently moved to the failed store. Always call listFailedLRAs() in addition to this tool whenever checking for problems.") + public String listAllLRAs() { + return get(coordinatorUrl); + } + + @Tool("Returns all LRAs in a specific lifecycle state. Valid states: Active, Closing, Closed, Cancelling, Cancelled, FailedToClose, FailedToCancel. Use this to narrow focus to problematic states such as FailedToClose or FailedToCancel.") + public String listLRAsByStatus( + @P("The LRA lifecycle state to filter by. One of: Active, Closing, Closed, Cancelling, Cancelled, FailedToClose, FailedToCancel") String status) { + return get(coordinatorUrl + "?Status=" + status); + } + + @Tool("Returns the complete record for a specific LRA: all participant URIs, per-participant status, timeout, top-level flag, and parent/child relationships. Use this to diagnose why a specific LRA is stuck or to inspect its participant list.") + public String getLRADetails( + @P("The full LRA ID, which is a URI such as http://host/lra-coordinator/") String lraId) { + return get(lraId); + } + + @Tool("Returns only the current lifecycle status string for a specific LRA (e.g. Cancelling, FailedToCancel). Cheaper than getLRADetails when you only need the status. Use this for quick status checks or to poll progress during multi-step diagnosis.") + public String getLRAStatus( + @P("The full LRA ID, which is a URI such as http://host/lra-coordinator/") String lraId) { + return get(lraId + "/status"); + } + + @Tool("Returns all LRAs currently undergoing automatic recovery — transactions interrupted mid-flight that the recovery coordinator is retrying. Use this to understand recovery load and to confirm whether a given LRA is being retried.") + public String listRecoveringLRAs() { + return get(coordinatorUrl + "/recovery"); + } + + @Tool("Returns all LRAs in terminal failed states (FailedToClose, FailedToCancel) that the recovery coordinator cannot resolve automatically and that require manual operator intervention.") + public String listFailedLRAs() { + return get(coordinatorUrl + "/recovery/failed"); + } + + @Tool("Deletes the failed record of a FailedToCancel or FailedToClose LRA from the coordinator's ObjectStore, acknowledging that the heuristic has been resolved out-of-band. " + + "This is the ONLY way to remove a failed LRA — calling cancelLRA or closeLRA on a terminal-failed LRA returns 412. " + + "Only call this after the operator has confirmed that all participants have been manually compensated or completed. " + + "The LRA ID must be the full URI (e.g. http://host/lra-coordinator/).") + public String deleteFailedLRA( + @P("The full LRA ID URI of the failed LRA to delete, e.g. http://host/lra-coordinator/") String lraId) { + String uid = lraId.substring(lraId.lastIndexOf('/') + 1); + return delete(coordinatorUrl + "/recovery/" + uid); + } + + @Tool("Closes an LRA by asking all enrolled participants to complete their work (the happy-path end). Only call this when the operator explicitly requests it and after confirming the LRA ID. Returns the coordinator's response including the new LRA status.") + public String closeLRA( + @P("The full LRA ID URI to close, e.g. http://host/lra-coordinator/") String lraId) { + return put(lraId + "/close"); + } + + @Tool("Cancels an LRA by asking all enrolled participants to compensate (roll back). Only call this when the operator explicitly requests it and after confirming the LRA ID. Returns the coordinator's response including the new LRA status.") + public String cancelLRA( + @P("The full LRA ID URI to cancel, e.g. http://host/lra-coordinator/") String lraId) { + return put(lraId + "/cancel"); + } + + @Tool("Starts a new LRA and returns its full LRA ID URI. The new LRA is in Active state with no participants yet enrolled. " + + "Only call this when the operator explicitly asks to create a new transaction.") + public String startLRA( + @P("A short descriptive name for this LRA, used to identify it in logs and listings (e.g. 'payment-saga-test'). Must not be empty.") String clientId, + @P("Optional timeout in milliseconds after which the LRA is automatically cancelled if not closed. Use 0 for no timeout.") long timeLimitMs) { + String url = coordinatorUrl + "/start?ClientID=" + URLEncoder.encode(clientId, StandardCharsets.UTF_8) + "&TimeLimit=" + timeLimitMs; + return post(url); + } + + private String get(String url) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/json") + .GET() + .build(); + HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 404) { + return "Not found: " + url; + } + return response.body(); + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); + return "Error reaching coordinator at " + url + ": " + e.getMessage(); + } + } + + private String put(String url) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/json") + .PUT(HttpRequest.BodyPublishers.noBody()) + .build(); + HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString()); + return "HTTP " + response.statusCode() + ": " + response.body(); + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); + return "Error reaching coordinator at " + url + ": " + e.getMessage(); + } + } + + private String delete(String url) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/json") + .DELETE() + .build(); + HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString()); + return "HTTP " + response.statusCode() + ": " + response.body(); + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); + return "Error reaching coordinator at " + url + ": " + e.getMessage(); + } + } + + private String post(String url) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "text/plain") + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString()); + return "HTTP " + response.statusCode() + ": " + response.body(); + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); + return "Error reaching coordinator at " + url + ": " + e.getMessage(); + } + } +} diff --git a/rts/lra/lra-ai-dashboard/src/main/resources/META-INF/resources/index.html b/rts/lra/lra-ai-dashboard/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000000..1336da5a4a --- /dev/null +++ b/rts/lra/lra-ai-dashboard/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,103 @@ + + + + + LRA AI Assistant + + + +
LRA AI Assistant — Narayana LRA Coordinator Diagnostics
+
+
Hello! I can help you diagnose and manage your Narayana LRA coordinator. + +Try asking: +• "Show me all active LRAs" +• "Are there any stuck or failed transactions?" +• "Why is LRA <paste-lra-id> stuck?" +• "Cancel LRA <paste-lra-id>"
+
+
+ + +
+ + + diff --git a/rts/lra/lra-ai-dashboard/src/main/resources/application.properties b/rts/lra/lra-ai-dashboard/src/main/resources/application.properties new file mode 100644 index 0000000000..75fff0e8be --- /dev/null +++ b/rts/lra/lra-ai-dashboard/src/main/resources/application.properties @@ -0,0 +1,49 @@ +# LRA AI Dashboard — application configuration + +# HTTP port (coordinator uses 8080 by default) +quarkus.http.port=8082 + +# LRA Coordinator base URL (without trailing slash) +lra.coordinator.url=http://localhost:8080/lra-coordinator + +# ── Ollama backend (local / air-gapped mode) ───────────────────────────────── +# Requires Ollama running locally: https://ollama.com +# Pull a tool-calling capable model first: +# ollama pull llama3.1 +quarkus.langchain4j.ollama.chat-model.model-id=llama3.1 +quarkus.langchain4j.ollama.base-url=http://localhost:11434 +quarkus.langchain4j.ollama.timeout=120s +quarkus.langchain4j.ollama.log-requests=true +quarkus.langchain4j.ollama.log-responses=true + +# ── OpenAI backend (cloud mode) ────────────────────────────────────────────── +# To use OpenAI instead of Ollama: +# 1. Swap the dependency in pom.xml (comment out ollama, uncomment openai) +# 2. Comment out the Ollama properties above +# 3. Uncomment the properties below and export LRA_AI_API_KEY=sk-... +# +# quarkus.langchain4j.openai.api-key=${LRA_AI_API_KEY:demo} +# quarkus.langchain4j.openai.chat-model.model-name=gpt-4o-mini +# quarkus.langchain4j.openai.chat-model.temperature=0.3 +# quarkus.langchain4j.openai.timeout=60s +# quarkus.langchain4j.openai.log-requests=true +# quarkus.langchain4j.openai.log-responses=true + +# ── Anthropic Claude backend (cloud mode) ──────────────────────────────────── +# To use Claude instead of Ollama: +# 1. Add the dependency in pom.xml: +# +# io.quarkiverse.langchain4j +# quarkus-langchain4j-anthropic +# +# and remove (or comment out) quarkus-langchain4j-ollama. +# 2. Comment out the Ollama properties above. +# 3. Uncomment the properties below and export ANTHROPIC_API_KEY=sk-ant-... +# (obtain your key at https://console.anthropic.com) +# +# quarkus.langchain4j.anthropic.api-key=${ANTHROPIC_API_KEY} +# quarkus.langchain4j.anthropic.chat-model.model-name=claude-sonnet-4-6 +# quarkus.langchain4j.anthropic.chat-model.temperature=0.3 +# quarkus.langchain4j.anthropic.timeout=60s +# quarkus.langchain4j.anthropic.log-requests=true +# quarkus.langchain4j.anthropic.log-responses=true diff --git a/rts/lra/pom.xml b/rts/lra/pom.xml index c0d7c8e79c..aa99b7bee7 100644 --- a/rts/lra/pom.xml +++ b/rts/lra/pom.xml @@ -54,5 +54,6 @@ trip-controller trip-client lra-test + lra-ai-dashboard \ No newline at end of file From 056fb7f782a00d94660d2af6b977f67be9248cb9 Mon Sep 17 00:00:00 2001 From: marco sappe griot Date: Tue, 9 Jun 2026 16:19:37 +0200 Subject: [PATCH 2/2] Update readme --- rts/lra/lra-ai-dashboard/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rts/lra/lra-ai-dashboard/README.md b/rts/lra/lra-ai-dashboard/README.md index a6d6e3d3a5..8c0155c184 100644 --- a/rts/lra/lra-ai-dashboard/README.md +++ b/rts/lra/lra-ai-dashboard/README.md @@ -52,11 +52,12 @@ brew install ollama # Linux curl -fsSL https://ollama.com/install.sh | sh +# Start the server (if not already running as a service) +ollama serve & + # Pull the configured model ollama pull llama3.1 -# Start the server (if not already running as a service) -ollama serve ``` > **Tool-calling requirement:** The LLM must support Ollama's tool-calling API.