-
Notifications
You must be signed in to change notification settings - Fork 227
Quarkus + Langchain4j Dapr Workflows extension + Agent registry #1693
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: master
Are you sure you want to change the base?
Changes from all commits
7260af8
920b13e
cf6e691
378c2f8
5aa83e0
20389f3
6205d97
7331dd1
7347160
14eda0e
8f0284d
6dd5afa
70e0930
dbf26f9
cc54b37
d0b1fa2
5946097
74e11cc
2fc5a0c
768aeae
4c274fc
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,82 @@ | ||
| name: Quarkus Agentic Build | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| push: | ||
| branches: | ||
| - master | ||
| - release-quarkus-* | ||
| paths: | ||
| - 'quarkus/**' | ||
| tags: | ||
| - quarkus-v* | ||
|
|
||
| pull_request: | ||
| branches: | ||
| - master | ||
| - release-quarkus-* | ||
| paths: | ||
| - 'quarkus/**' | ||
|
|
||
| jobs: | ||
| build: | ||
| name: "Build & Test" | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| env: | ||
| JDK_VER: 17 | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
| - name: Set up OpenJDK ${{ env.JDK_VER }} | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| distribution: 'temurin' | ||
| java-version: ${{ env.JDK_VER }} | ||
| - name: Build quarkus modules | ||
| run: | | ||
| cd quarkus | ||
| ../mvnw clean install -B -q -DskipITs=true | ||
| - name: Upload test reports | ||
| if: always() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: quarkus-test-reports | ||
| path: quarkus/**/target/surefire-reports/ | ||
|
|
||
| publish: | ||
| runs-on: ubuntu-latest | ||
| needs: [ build ] | ||
| timeout-minutes: 30 | ||
| env: | ||
| JDK_VER: 17 | ||
| OSSRH_USER_TOKEN: ${{ secrets.OSSRH_USER_TOKEN }} | ||
| OSSRH_PWD_TOKEN: ${{ secrets.OSSRH_PWD_TOKEN }} | ||
| GPG_KEY: ${{ secrets.GPG_KEY }} | ||
| GPG_PWD: ${{ secrets.GPG_PWD }} | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
| - name: Set up OpenJDK ${{ env.JDK_VER }} | ||
| uses: actions/setup-java@v5 | ||
| with: | ||
| distribution: 'temurin' | ||
| java-version: ${{ env.JDK_VER }} | ||
| - name: Get quarkus version | ||
| run: | | ||
| QUARKUS_VERSION=$(./mvnw -B -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec -f quarkus/pom.xml) | ||
| echo "QUARKUS_VERSION=$QUARKUS_VERSION" >> $GITHUB_ENV | ||
| - name: Is SNAPSHOT release ? | ||
| if: contains(github.ref, 'master') && contains(env.QUARKUS_VERSION, '-SNAPSHOT') | ||
| run: | | ||
| echo "DEPLOY_OSSRH=true" >> $GITHUB_ENV | ||
| - name: Is Release version ? | ||
| if: startsWith(github.ref, 'refs/tags/quarkus-v') && !contains(env.QUARKUS_VERSION, '-SNAPSHOT') | ||
| run: | | ||
| echo "DEPLOY_OSSRH=true" >> $GITHUB_ENV | ||
| - name: Publish to ossrh | ||
| if: env.DEPLOY_OSSRH == 'true' | ||
| run: | | ||
| echo ${{ secrets.GPG_PRIVATE_KEY }} | base64 -d > private-key.gpg | ||
| export GPG_TTY=$(tty) | ||
| gpg --batch --import private-key.gpg | ||
| cd quarkus | ||
| ../mvnw -V -B -Dgpg.skip=false -DskipTests -s ../settings.xml deploy |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| name: Quarkus Agentic Release | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| rel_version: | ||
| description: 'Release version (examples: 0.1.0, 0.2.0-rc-1, 0.2.0-SNAPSHOT)' | ||
| required: true | ||
| type: string | ||
|
|
||
| jobs: | ||
| create-release: | ||
| name: Creates quarkus release tag | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| JDK_VER: '17' | ||
| steps: | ||
| - name: Check out code | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| fetch-depth: 0 | ||
| token: ${{ secrets.DAPR_BOT_TOKEN }} | ||
| persist-credentials: false | ||
| - name: Set up OpenJDK ${{ env.JDK_VER }} | ||
| uses: actions/setup-java@v5 | ||
| with: | ||
| distribution: 'temurin' | ||
| java-version: ${{ env.JDK_VER }} | ||
| - name: Update quarkus version and tag | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.DAPR_BOT_TOKEN }} | ||
| REL_VERSION: ${{ inputs.rel_version }} | ||
| run: | | ||
| set -ue | ||
|
|
||
| git config user.email "daprweb@microsoft.com" | ||
| git config user.name "Dapr Bot" | ||
| git remote set-url origin https://x-access-token:${{ secrets.DAPR_BOT_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git | ||
|
|
||
| # Update version in all quarkus pom.xml files | ||
| cd quarkus | ||
| ../mvnw versions:set -DnewVersion=$REL_VERSION -DprocessDependencies=true | ||
| cd .. | ||
|
|
||
| if [[ "$REL_VERSION" == *-SNAPSHOT ]]; then | ||
| git commit -s -m "Update quarkus version to ${REL_VERSION}" -a | ||
| git push origin master | ||
| echo "Updated master with quarkus version ${REL_VERSION}." | ||
| else | ||
| git commit -s -m "Release quarkus-v${REL_VERSION}" -a | ||
| git tag "quarkus-v${REL_VERSION}" | ||
| git push origin master | ||
| git push origin "quarkus-v${REL_VERSION}" | ||
| echo "Tagged and pushed quarkus-v${REL_VERSION}." | ||
| fi |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -749,6 +749,7 @@ | |
| <!-- We are following test containers artifact convention on purpose, don't rename --> | ||
| <module>testcontainers-dapr</module> | ||
| <module>durabletask-client</module> | ||
| <module>quarkus</module> | ||
|
Contributor
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. This new module should be independent from dapr-sdk and have its own release process
Collaborator
Author
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. We can send this code to quarkus-Dapr. @yaron2 which comments, can you be more specific?
Contributor
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. lets keep it here, but not as a module of the sdk, it will have a different release process |
||
| </modules> | ||
|
|
||
| <profiles> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| # Quarkus Agentic Dapr | ||
|
|
||
| A Quarkus extension that bridges [LangChain4j's agentic framework](https://docs.langchain4j.dev/) with [Dapr Workflows](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/) for durable, observable AI agent orchestration. | ||
|
|
||
| ## What it does | ||
|
|
||
| Every LLM call and tool call becomes a **durable Dapr Workflow activity**. If the process crashes, completed activities are recovered from the workflow history. Agent orchestration (sequential, parallel, loop, conditional) is backed by Dapr Workflows with parent-child hierarchy. | ||
|
|
||
| | Capability | Without Dapr | With `quarkus-agentic-dapr` | | ||
| |---|---|---| | ||
| | Durability | Lost on crash | Full workflow history persisted | | ||
| | Observability | Logs only | Dapr dashboard + per-activity tracking | | ||
| | Tool call audit trail | None | Every call recorded with inputs/outputs | | ||
| | LLM call audit trail | None | Every LLM request/response recorded | | ||
| | Code changes | — | **None** — just swap the dependency | | ||
|
|
||
| ## Modules | ||
|
|
||
| ``` | ||
| quarkus/ | ||
| ├── runtime/ # Core extension (interceptors, workflows, activities) | ||
| ├── deployment/ # Build-time processing (annotation scanning, CDI decorators) | ||
| ├── quarkus-agentic-dapr-agents-registry/ # Optional: registers agents in Dapr state store | ||
| ├── quarkus-langchain4j-dapr/ # Optional: Dapr Conversation API as ChatModel provider | ||
| └── examples/ # Built-in examples | ||
| ``` | ||
|
|
||
| ## Supported Agent Types | ||
|
|
||
| All 5 LangChain4j orchestration types are supported: | ||
|
|
||
| | Annotation | Dapr Workflow | | ||
| |------------|---------------| | ||
| | `@Agent` | `AgentRunWorkflow` (per-agent ReAct loop) | | ||
| | `@SequenceAgent` | `SequentialOrchestrationWorkflow` | | ||
| | `@ParallelAgent` | `ParallelOrchestrationWorkflow` | | ||
| | `@LoopAgent` | `LoopOrchestrationWorkflow` | | ||
| | `@ConditionalAgent` | `ConditionalOrchestrationWorkflow` | | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ### 1. Add the dependency | ||
|
|
||
| ```xml | ||
| <dependency> | ||
| <groupId>io.dapr.quarkus</groupId> | ||
| <artifactId>quarkus-agentic-dapr</artifactId> | ||
| <version>0.1.0-SNAPSHOT</version> | ||
| </dependency> | ||
| ``` | ||
|
|
||
| ### 2. Configure | ||
|
|
||
| ```properties | ||
| quarkus.dapr.devservices.enabled=false | ||
| quarkus.dapr.workflow.enabled=false | ||
|
|
||
| # Dapr sidecar endpoints | ||
| dapr.grpc.endpoint=${DAPR_GRPC_ENDPOINT:http://localhost:40001} | ||
| dapr.http.endpoint=${DAPR_HTTP_ENDPOINT:http://localhost:3500} | ||
| ``` | ||
|
|
||
| ### 3. Write agents (standard LangChain4j — no Dapr-specific code) | ||
|
|
||
| ```java | ||
| public interface WeatherAssistant { | ||
|
|
||
| @ToolBox(WeatherTools.class) | ||
| @UserMessage("Check the weather in {{city}}") | ||
| @Agent(name = "weather-assistant", outputKey = "weather") | ||
| String checkWeather(@V("city") String city); | ||
| } | ||
| ``` | ||
|
|
||
| ### 4. Run with Dapr | ||
|
|
||
| ```bash | ||
| # Terminal 1: Dapr sidecar | ||
| dapr run --app-id my-agent --dapr-grpc-port 40001 --dapr-http-port 3500 \ | ||
| --resources-path ./components | ||
|
|
||
| # Terminal 2: Quarkus app | ||
| DAPR_GRPC_ENDPOINT=http://localhost:40001 \ | ||
| DAPR_HTTP_ENDPOINT=http://localhost:3500 \ | ||
| mvn quarkus:dev -Ddebug=false | ||
| ``` | ||
|
|
||
| ## LLM Provider Options | ||
|
|
||
| ### Ollama (direct) | ||
| ```properties | ||
| quarkus.langchain4j.ollama.chat-model.model-id=llama3.1:8b | ||
| ``` | ||
|
|
||
| ### Dapr Conversation API (provider-agnostic) | ||
| ```properties | ||
| quarkus.langchain4j.chat-model.provider=dapr-conversation | ||
| quarkus.langchain4j.dapr.component-name=llm | ||
| ``` | ||
|
|
||
| Swap LLM providers by changing the Dapr component YAML — no Java code changes. | ||
|
|
||
| ## Example Project | ||
|
|
||
| See [travel-planner-agents](https://github.com/javier-aliaga/travel-planner-agents) for a complete example with all agent types, Makefile targets, and Dapr components. | ||
|
|
||
| ## Crash Recovery | ||
|
|
||
| If the process crashes mid-execution, completed agents are not re-run. The in-progress agent is automatically re-run from scratch using its original prompt and tools. | ||
|
|
||
| ### How it works | ||
|
|
||
| 1. **Normal operation**: LangChain4j's AiServices drives the ReAct loop. Each LLM call and tool call is recorded as a Dapr Workflow activity. | ||
| 2. **Crash**: The process dies. Dapr workflow history persists. | ||
| 3. **Restart**: Dapr replays the workflow. Completed activities return cached results instantly. | ||
| 4. **Recovery detection**: The in-progress activity is re-dispatched but fails because the in-memory `AgentRunContext` is gone. `AgentRunWorkflow` catches the failure. | ||
| 5. **Agent re-run**: `RecoveryAgentActivity` re-executes the agent's entire ReAct loop from scratch — calling `ChatModel.chat()` directly and invoking tools via `ToolRegistry`. | ||
|
|
||
| ### Recovery granularity | ||
|
|
||
| | Scope | Behavior | | ||
| |-------|----------| | ||
| | Orchestration (e.g., Agent1 → Agent2 → Agent3) | Completed agents are skipped (Dapr child workflow replay). Only the in-progress agent re-runs. | | ||
| | Single agent (LLM calls + tool calls) | The entire agent re-runs from its original prompt. Individual LLM/tool calls within the agent are not skipped. | | ||
|
|
||
| ### Demo: simulating a crash | ||
|
|
||
| ```bash | ||
| # 1. Start the app and trigger a multi-agent workflow | ||
| curl "http://localhost:8080/travel/plan?origin=NYC&destination=Paris" | ||
|
|
||
| # 2. Kill the process mid-execution (e.g., during the second agent) | ||
| kill -9 <pid> | ||
|
|
||
| # 3. Restart the app — the workflow resumes automatically | ||
| mvn quarkus:dev | ||
| ``` | ||
|
|
||
| In the Dapr dashboard, completed agents show cached results. The crashed agent is detected via `TaskFailedException`, then re-runs and completes. | ||
|
|
||
| ### Key classes | ||
|
|
||
| | Class | Role | | ||
| |-------|------| | ||
| | `AgentRunWorkflow` | Catches `TaskFailedException` on activity failure, triggers recovery | | ||
| | `RecoveryAgentActivity` | Self-contained ReAct loop: ChatModel.chat() + tool dispatch | | ||
| | `ToolRegistry` | CDI bean that discovers @Tool methods at startup for recovery | | ||
| | `AgentToolClassRegistry` | Maps agent names to their @ToolBox classes (populated at build time) | | ||
|
|
||
| ## AgenticScope Checkpointing | ||
|
|
||
| LangChain4j's agentic scope (the shared state and conversation context of a multi-agent | ||
| workflow) can be checkpointed to a Dapr state store — the LangChain4j equivalent of a | ||
| LangGraph checkpointer. Every scope update is persisted, so agentic state survives | ||
| restarts and is shareable across replicas. | ||
|
|
||
| ```properties | ||
| dapr.agentic.scope-store.enabled=true | ||
| dapr.agentic.scope-store.name=kvstore | ||
| ``` | ||
|
|
||
| | Class | Role | | ||
| |-------|------| | ||
| | `DaprAgenticScopeStore` | `AgenticScopeStore` implementation over the Dapr state API | | ||
| | `AgenticScopeStoreInitializer` | Registers the store with LangChain4j's `AgenticScopePersister` at startup | | ||
|
|
||
| ## Known Limitations | ||
|
|
||
| - **Single replica only (current design)**: live agent execution keeps per-run state in | ||
| JVM memory — the `AgentRunContext` registry, the `CompletableFuture` the agent thread | ||
| blocks on, and the per-thread Dapr context. Dapr Workflow, however, | ||
| [randomly load-balances activities across all replicas](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-architecture/) | ||
| of an app-id, with no locality to where a run started. With more than one replica an | ||
| LLM/tool activity can be dispatched to a replica that lacks the in-memory context, fail | ||
| to find it, and surface as a (false) crash-recovery while the originating request blocks | ||
| until the call timeout. **Deploy a single replica** until execution state is moved into | ||
| workflow history (control inversion — see [Crash Recovery](#crash-recovery)); this is the | ||
| same root cause as agent-level (not per-call) recovery. | ||
| - **Recovery granularity**: Agent-level only — individual LLM/tool calls within an agent are re-executed (not skipped) | ||
| - **Same agent name in concurrent _parallel_ orchestrations**: within a single | ||
| orchestration each run's Dapr context is bound to its own run ID — including every | ||
| iteration of a `@LoopAgent` and all sequential agents, which run on the planner's | ||
| thread and route through the context it sets directly. Only when two *different* | ||
| concurrent requests run the same agent name on parallel executor threads at the same | ||
| instant can the FIFO name binding cross-attribute their runs; run IDs stay unique so | ||
| routing is still correct, but observability may interleave. | ||
| - **daprd 1.18.0 workflow race**: a workflow event can be lost by the runtime (the | ||
| workflow completes app-side but stays RUNNING in daprd, its event reminder fires on | ||
| an empty inbox) — fix upstream in | ||
| [dapr/dapr#10054](https://github.com/dapr/dapr/pull/10054). `AgentRunWorkflow` now | ||
| self-heals: each `agent-event` wait has a 60s timeout that re-arms and lets replay | ||
| redeliver the missed event from history, turning a permanent hang into a bounded | ||
| delay. The e2e tests still retry once locally and are skipped on CI (slow runners | ||
| widen the race window) until a fixed runtime ships. | ||
| - **Small models**: llama3.2 (3B) sometimes malforms tool call arguments; llama3.1:8b+ recommended |
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.
It is very important to be sure that the extension can run in native mode, I think we need to add it here.
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.
I will do it in a followup PR, this one is getting too much bigger