This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch langchain4j-provider in repository https://gitbox.apache.org/repos/asf/camel-jbang-examples.git
commit 07d156ce4bbf3345f0905295685b9f6fb5cd8b11 Author: Claus Ibsen <[email protected]> AuthorDate: Tue Oct 20 10:03:52 2026 +0200 ai - the LangChain4j chat model is configured by provider instead of a bean of the model class Camel 4.23 creates the langchain4j-chat model from a provider and the options every provider has (CAMEL-24820), and camel run downloads the provider's LangChain4j module. ai/langchain4j-chat and ai/ticket-classifier set provider, baseUrl, modelName and temperature on the endpoint, so their Citrus tests, which pass only the URL and the model name, need no change. ai/docling-langchain4j-rag (four endpoints) and security/ocsf set them as component options in application.properties. The OllamaChatModel builder beans are gone, and the READMEs, the log line numbers they show and the catalog descriptions say the model is configured by provider. Closes #91. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- README.md | 2 +- ai/README.md | 2 +- ai/docling-langchain4j-rag/README.md | 25 ++++++++++---------- ai/docling-langchain4j-rag/application.properties | 6 +++++ .../docling-langchain4j-rag.yaml | 18 ++------------- ai/langchain4j-chat/README.md | 27 ++++++++++++---------- ai/langchain4j-chat/langchain4j-chat.camel.yaml | 20 ++++++---------- ai/langchain4j-chat/metadata.json | 2 +- ai/ticket-classifier/README.md | 17 +++++++------- ai/ticket-classifier/ticket-classifier.camel.yaml | 15 ++++-------- camel-jbang-example-catalog.json | 2 +- security/ocsf/README.md | 3 ++- security/ocsf/application.properties | 6 +++++ security/ocsf/ocsf.camel.yaml | 11 --------- 14 files changed, 68 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 278df37..0ff9534 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ A local model writing text, routes exposed as MCP tools, RAG over documents, PII | Example | What you will see | Needs | |---|---|---| -| [LangChain4j chat](ai/langchain4j-chat/) | A local Ollama model started with camel infra writes the shipping notification for each of the three orders; the chat model is a bean built from properties, the prompt comes from the order, and the log shows the reply with its token counts. | `camel infra run ollama`, a local model: `ollama pull granite4:3b` | +| [LangChain4j chat](ai/langchain4j-chat/) | A local Ollama model started with camel infra writes the shipping notification for each of the three orders; the chat model is configured by provider on the endpoint, the prompt comes from the order, and the log shows the reply with its token counts. | `camel infra run ollama`, a local model: `ollama pull granite4:3b` | | [Ticket classifier](ai/ticket-classifier/) | A local model reads each of three support tickets and names its category, shipping, billing or account; the route checks the answer and files the ticket in tickets-out/<category>/, and an answer outside the three, or no answer within the circuit breaker's timeout, files it under unknown. | `camel infra run ollama`, a local model: `ollama pull granite4:3b` | | [MCP server](ai/mcp-server/) | Two routes are exposed as MCP tools, stock_level by SKU and order_status by order id, on http://localhost:8080/mcp with nothing but properties to switch the server on; any MCP client, a coding agent included, can list and call them, and the log shows each call. | nothing | | [OpenAI PII Redaction](ai/openai-pii-redaction/) | Text typed on standard input is sent to an OpenAI-compatible model with a JSON schema that asks for the personal identifiers redacted, and the redacted text is printed on standard output. | an OpenAI-compatible API: set `OPENAI_API_KEY` | diff --git a/ai/README.md b/ai/README.md index bdccb88..1e16d73 100644 --- a/ai/README.md +++ b/ai/README.md @@ -5,7 +5,7 @@ A local model writing text, routes exposed as MCP tools, RAG over documents, PII | Example | What you will see | Needs | |---|---|---| -| [LangChain4j chat](langchain4j-chat/) | A local Ollama model started with camel infra writes the shipping notification for each of the three orders; the chat model is a bean built from properties, the prompt comes from the order, and the log shows the reply with its token counts. | `camel infra run ollama`, a local model: `ollama pull granite4:3b` | +| [LangChain4j chat](langchain4j-chat/) | A local Ollama model started with camel infra writes the shipping notification for each of the three orders; the chat model is configured by provider on the endpoint, the prompt comes from the order, and the log shows the reply with its token counts. | `camel infra run ollama`, a local model: `ollama pull granite4:3b` | | [Ticket classifier](ticket-classifier/) | A local model reads each of three support tickets and names its category, shipping, billing or account; the route checks the answer and files the ticket in tickets-out/<category>/, and an answer outside the three, or no answer within the circuit breaker's timeout, files it under unknown. | `camel infra run ollama`, a local model: `ollama pull granite4:3b` | | [MCP server](mcp-server/) | Two routes are exposed as MCP tools, stock_level by SKU and order_status by order id, on http://localhost:8080/mcp with nothing but properties to switch the server on; any MCP client, a coding agent included, can list and call them, and the log shows each call. | nothing | | [OpenAI PII Redaction](openai-pii-redaction/) | Text typed on standard input is sent to an OpenAI-compatible model with a JSON schema that asks for the personal identifiers redacted, and the redacted text is printed on standard output. | an OpenAI-compatible API: set `OPENAI_API_KEY` | diff --git a/ai/docling-langchain4j-rag/README.md b/ai/docling-langchain4j-rag/README.md index f6e6ad0..e500fae 100644 --- a/ai/docling-langchain4j-rag/README.md +++ b/ai/docling-langchain4j-rag/README.md @@ -9,13 +9,13 @@ questions against the latest document. Both services are started with `camel inf ```text $ cp sample.md documents/ -INFO ... docling-langchain4j-rag.yaml:27 : Processing document: sample.md -INFO ... docling-langchain4j-rag.yaml:38 : Converting document to Markdown with Docling... -INFO ... docling-langchain4j-rag.yaml:47 : Document converted to Markdown successfully -INFO ... docling-langchain4j-rag.yaml:77 : Analyzing document with AI model... -INFO ... docling-langchain4j-rag.yaml:89 : AI analysis completed -INFO ... docling-langchain4j-rag.yaml:120 : Analysis report saved: sample.md_analysis.md -INFO ... docling-langchain4j-rag.yaml:137 : Processing complete for: sample.md +INFO ... docling-langchain4j-rag.yaml:17 : Processing document: sample.md +INFO ... docling-langchain4j-rag.yaml:28 : Converting document to Markdown with Docling... +INFO ... docling-langchain4j-rag.yaml:37 : Document converted to Markdown successfully +INFO ... docling-langchain4j-rag.yaml:67 : Analyzing document with AI model... +INFO ... docling-langchain4j-rag.yaml:78 : AI analysis completed +INFO ... docling-langchain4j-rag.yaml:109 : Analysis report saved: sample.md_analysis.md +INFO ... docling-langchain4j-rag.yaml:126 : Processing complete for: sample.md ``` and `output/sample.md_analysis.md` holds the report: the file name and date, the model's summary, key topics @@ -57,9 +57,10 @@ Stop the example with `ctrl` + `c` and the services with `camel infra stop docli ## How it works -- `docling-langchain4j-rag.yaml` starts with the bean `chatModel`, an `OllamaChatModel` built through its - LangChain4j builder from the URL and model name in `application.properties`; the `langchain4j-chat` - component picks it up as the one chat model in the registry. +- The chat model is configured by provider in `application.properties`: + `camel.component.langchain4j-chat.provider=ollama` with the URL and model name, so every `langchain4j-chat` + endpoint creates the same model and the CLI downloads `langchain4j-ollama` for it. For OpenAI, set + `provider=openai`, an `api-key` and the `model-name`; `base-url` points it at an OpenAI-compatible server. - `document-analysis-workflow` is the main route: a `file` consumer on `documents` with `include` for the supported extensions. The body becomes the file's absolute path, the `docling` endpoint with `CONVERT_TO_MARKDOWN` (and `allowFilePathSource`, as Docling reads a path in the body only when told to, @@ -86,8 +87,8 @@ Ask your assistant, or type it yourself, one step at a time, and run after each, 1. A route from `file:documents` that logs the file name. 2. Set the body to the file's absolute path and send it to `docling` with `CONVERT_TO_MARKDOWN` against the Docling URL; log the Markdown. -3. The `chatModel` bean from properties, a prompt around the Markdown, and a `langchain4j-chat` step; log the - answer. +3. The `camel.component.langchain4j-chat.*` provider options, a prompt around the Markdown, and a + `langchain4j-chat` step; log the answer. 4. Write the answer and the Markdown to `output` as `<name>_analysis.md` with a `file` producer. 5. A `platform-http` route on `/api/ask` that converts the newest document and asks the model the question in the request body. diff --git a/ai/docling-langchain4j-rag/application.properties b/ai/docling-langchain4j-rag/application.properties index 4507026..d6b79de 100644 --- a/ai/docling-langchain4j-rag/application.properties +++ b/ai/docling-langchain4j-rag/application.properties @@ -42,3 +42,9 @@ camel.server.port=8080 # the folders the routes read and write hold data, not sources: with camel run --source-dir in dev mode, # a change in them does not reload the routes camel.jbang.dataDir=documents,output + +# the chat model of the langchain4j-chat endpoints, by provider: camel run downloads langchain4j-ollama for it +camel.component.langchain4j-chat.provider=ollama +camel.component.langchain4j-chat.base-url={{ollama.base.url}} +camel.component.langchain4j-chat.model-name={{ollama.model.name}} +camel.component.langchain4j-chat.temperature=0.3 diff --git a/ai/docling-langchain4j-rag/docling-langchain4j-rag.yaml b/ai/docling-langchain4j-rag/docling-langchain4j-rag.yaml index 27889e0..5f5d519 100644 --- a/ai/docling-langchain4j-rag/docling-langchain4j-rag.yaml +++ b/ai/docling-langchain4j-rag/docling-langchain4j-rag.yaml @@ -1,18 +1,8 @@ # Documents dropped in a directory are converted by Docling, analysed by a # local model and written to an output directory; an HTTP endpoint answers -# questions against the latest document. The chat model is a bean built from -# application.properties. Start the services with: +# questions against the latest document. The chat model is configured by +# provider in application.properties. Start the services with: # camel infra run docling ollama -- beans: - - name: chatModel - type: dev.langchain4j.model.ollama.OllamaChatModel - builderClass: dev.langchain4j.model.ollama.OllamaChatModel$OllamaChatModelBuilder - builderMethod: build - properties: - baseUrl: "{{ollama.base.url}}" - modelName: "{{ollama.model.name}}" - temperature: 0.3 - - route: id: document-analysis-workflow from: @@ -80,7 +70,6 @@ uri: langchain4j-chat parameters: chatId: analysis - chatModel: "#chatModel" - setVariable: name: aiAnalysis expression: @@ -215,7 +204,6 @@ uri: langchain4j-chat parameters: chatId: qa - chatModel: "#chatModel" - setHeader: name: Content-Type expression: @@ -293,7 +281,6 @@ uri: langchain4j-chat parameters: chatId: summary - chatModel: "#chatModel" - log: message: "Summary: ${body}" - route: @@ -383,7 +370,6 @@ uri: langchain4j-chat parameters: chatId: extract - chatModel: "#chatModel" - setHeader: name: Content-Type expression: diff --git a/ai/langchain4j-chat/README.md b/ai/langchain4j-chat/README.md index f68e82a..801b9ca 100644 --- a/ai/langchain4j-chat/README.md +++ b/ai/langchain4j-chat/README.md @@ -1,14 +1,15 @@ # LangChain4j chat -A local model writes the shipping notification for each order. The chat model is a bean built from -`application.properties`, the route turns the order into a prompt, and the reply comes back as the body with the -token counts in headers. +A local model writes the shipping notification for each order. The chat model is configured by provider on the +endpoint, the route turns the order into a prompt, and the reply comes back as the body with the token counts in +headers. ## What you will see ```text -INFO ... langchain4j-chat.camel.yaml:45 : Notification for ORD-1002 (107 in, 35 out): Dear Customer C-207, -Your order ORD-1002 containing 3 CAMEL-MUGs has been shipped today. We hope you enjoy your new items! +INFO ... langchain4j-chat.camel.yaml:39 : Notification for ORD-1002 (107 in, 33 out): Dear Customer C-207, + +Your order ORD-1002, containing 3 Camel Mugs, has been shipped today. We hope you enjoy your purchase! ``` The wording is the model's; `granite4:3b` writes plainly, a larger model writes better. @@ -40,11 +41,11 @@ seconds per reply; a larger model or a GPU is faster. Stop the container with `c ## How it works -- The `beans` block builds the chat model: LangChain4j models come from builders, so the bean names the - `builderClass`, the `builderMethod` and the builder's properties, with the URL and model name read from - `application.properties`. The CLI downloads `langchain4j-ollama` from the class name. -- `langchain4j-chat` with `chatModel: "#chatModel"` sends the body as a single user message and replaces the - body with the reply; `CamelLangChain4jChatInputTokenCount` and `...OutputTokenCount` say what it cost. +- `langchain4j-chat` creates the chat model itself from `provider: ollama` and the options every provider has: + `baseUrl`, `modelName` and `temperature`, with the URL and model name read from `application.properties`. The + CLI downloads `langchain4j-ollama` for the provider, so there is no bean of the model class to declare. +- It sends the body as a single user message and replaces the body with the reply; + `CamelLangChain4jChatInputTokenCount` and `...OutputTokenCount` say what it cost. - The prompt is built with the Simple language from the order fields; `${body[lines]}` prints the list of lines. - The order id is kept in a variable because the reply replaces the body. @@ -61,8 +62,10 @@ seconds per reply; a larger model or a GPU is faster. Stop the container with `c `gen_ai.operation.name`, the model and the token usage. - Switch to `CHAT_SINGLE_MESSAGE_WITH_PROMPT` with a `CamelLangChain4jChatPromptTemplate` header and pass the order fields as variables instead of building the prompt yourself. -- Point `ollama.base.url` at an OpenAI-compatible server by swapping the bean for `OpenAiChatModel` from - `langchain4j-open-ai`. +- Use another provider: `provider: openai` with an `apiKey` (and `baseUrl` for an OpenAI-compatible server), or + `anthropic`, `mistral`, `gemini` and the others; `model.<property>` sets a property of the provider's own + builder. The options can also go in `application.properties` for every endpoint, as + `camel.component.langchain4j-chat.provider=ollama` and so on. ## Integration testing diff --git a/ai/langchain4j-chat/langchain4j-chat.camel.yaml b/ai/langchain4j-chat/langchain4j-chat.camel.yaml index a20a226..02c18f1 100644 --- a/ai/langchain4j-chat/langchain4j-chat.camel.yaml +++ b/ai/langchain4j-chat/langchain4j-chat.camel.yaml @@ -1,17 +1,8 @@ # A local model writes the shipping notification for each order. The chat -# model is a bean built from application.properties; the route builds the -# prompt from the order and logs the reply. Start the model with: +# model is configured by provider on the endpoint, with the URL and model name +# from application.properties; the route builds the prompt from the order and +# logs the reply. Start the model with: # camel infra run ollama -- beans: - - name: chatModel - type: dev.langchain4j.model.ollama.OllamaChatModel - builderClass: dev.langchain4j.model.ollama.OllamaChatModel$OllamaChatModelBuilder - builderMethod: build - properties: - baseUrl: "{{ollama.base.url}}" - modelName: "{{ollama.model}}" - temperature: 0.2 - - route: id: shipping-notifications from: @@ -41,6 +32,9 @@ uri: langchain4j-chat parameters: chatId: notifications - chatModel: "#chatModel" + provider: ollama + baseUrl: "{{ollama.base.url}}" + modelName: "{{ollama.model}}" + temperature: 0.2 - log: message: "Notification for ${variable.orderId} (${header.CamelLangChain4jChatInputTokenCount} in, ${header.CamelLangChain4jChatOutputTokenCount} out): ${body}" diff --git a/ai/langchain4j-chat/metadata.json b/ai/langchain4j-chat/metadata.json index 577a99a..cc9f4a2 100644 --- a/ai/langchain4j-chat/metadata.json +++ b/ai/langchain4j-chat/metadata.json @@ -1,6 +1,6 @@ { "title": "LangChain4j chat", - "description": "A local Ollama model started with camel infra writes the shipping notification for each of the three orders; the chat model is a bean built from properties, the prompt comes from the order, and the log shows the reply with its token counts.", + "description": "A local Ollama model started with camel infra writes the shipping notification for each of the three orders; the chat model is configured by provider on the endpoint, the prompt comes from the order, and the log shows the reply with its token counts.", "level": "ai", "order": 1, "teaches": { diff --git a/ai/ticket-classifier/README.md b/ai/ticket-classifier/README.md index 6f87bb9..0cc9c29 100644 --- a/ai/ticket-classifier/README.md +++ b/ai/ticket-classifier/README.md @@ -7,12 +7,12 @@ answer in time, files the ticket under `unknown`. ## What you will see ```text -INFO ... ticket-classifier.camel.yaml:26 : Ticket T-1: My order ORD-1001 has not arrived yet. It is two weeks now, where is my parcel? -INFO ... ticket-classifier.camel.yaml:72 : Ticket T-1 is shipping -INFO ... ticket-classifier.camel.yaml:26 : Ticket T-2: I was charged twice for order ORD-1002, please refund one payment. -INFO ... ticket-classifier.camel.yaml:72 : Ticket T-2 is billing -INFO ... ticket-classifier.camel.yaml:26 : Ticket T-3: How do I change the email address and the password of my account? -INFO ... ticket-classifier.camel.yaml:72 : Ticket T-3 is account +INFO ... ticket-classifier.camel.yaml:16 : Ticket T-1: My order ORD-1001 has not arrived yet. It is two weeks now, where is my parcel? +INFO ... ticket-classifier.camel.yaml:65 : Ticket T-1 is shipping +INFO ... ticket-classifier.camel.yaml:16 : Ticket T-2: I was charged twice for order ORD-1002, please refund one payment. +INFO ... ticket-classifier.camel.yaml:65 : Ticket T-2 is billing +INFO ... ticket-classifier.camel.yaml:16 : Ticket T-3: How do I change the email address and the password of my account? +INFO ... ticket-classifier.camel.yaml:65 : Ticket T-3 is account ``` and the tickets in `tickets-out/shipping/T-1.json`, `tickets-out/billing/T-2.json` and @@ -43,8 +43,9 @@ Stop the example with `ctrl` + `c`, and the container with `camel infra stop oll ## How it works -- The `beans` entry builds the chat model, an `OllamaChatModel`, from `application.properties`. Temperature 0 makes - the answers as steady as the model allows. +- `langchain4j-chat` creates the chat model from `provider: ollama`, with the URL and model name from + `application.properties`; the CLI downloads `langchain4j-ollama` for the provider. Temperature 0 makes the + answers as steady as the model allows. - The route builds the prompt in the variable `prompt` and calls `langchain4j-chat` with `variableSend: prompt` and `variableReceive: answer`. The ticket stays the body. - The answer is normalized to lower-case letters only, so "Shipping." becomes `shipping`. diff --git a/ai/ticket-classifier/ticket-classifier.camel.yaml b/ai/ticket-classifier/ticket-classifier.camel.yaml index d5904a1..951d861 100644 --- a/ai/ticket-classifier/ticket-classifier.camel.yaml +++ b/ai/ticket-classifier/ticket-classifier.camel.yaml @@ -1,16 +1,6 @@ # A local model sorts the support tickets of the shop: it names the category of each ticket, shipping, billing or # account, and the ticket is filed in that folder. Any other answer, or no answer in time, files it under unknown. # Start the model with: camel infra run ollama -- beans: - - name: chatModel - type: dev.langchain4j.model.ollama.OllamaChatModel - builderClass: dev.langchain4j.model.ollama.OllamaChatModel$OllamaChatModelBuilder - builderMethod: build - properties: - baseUrl: "{{ollama.base.url}}" - modelName: "{{ollama.model}}" - temperature: 0.0 - - route: id: classify-tickets from: @@ -43,7 +33,10 @@ uri: langchain4j-chat parameters: chatId: tickets - chatModel: "#chatModel" + provider: ollama + baseUrl: "{{ollama.base.url}}" + modelName: "{{ollama.model}}" + temperature: 0.0 variableSend: prompt variableReceive: answer - setVariable: diff --git a/camel-jbang-example-catalog.json b/camel-jbang-example-catalog.json index 54d2fc4..d85c4b5 100644 --- a/camel-jbang-example-catalog.json +++ b/camel-jbang-example-catalog.json @@ -49,7 +49,7 @@ { "name": "ai/langchain4j-chat", "title": "LangChain4j chat", - "description": "A local Ollama model started with camel infra writes the shipping notification for each of the three orders; the chat model is a bean built from properties, the prompt comes from the order, and the log shows the reply with its token counts.", + "description": "A local Ollama model started with camel infra writes the shipping notification for each of the three orders; the chat model is configured by provider on the endpoint, the prompt comes from the order, and the log shows the reply with its token counts.", "level": "ai", "teaches": { "components": [ diff --git a/security/ocsf/README.md b/security/ocsf/README.md index c7258fc..992919d 100644 --- a/security/ocsf/README.md +++ b/security/ocsf/README.md @@ -14,7 +14,8 @@ The `camel-ocsf` module requires Camel 4.18.0 or later. camel run ocsf.camel.yaml application.properties --port 8081 ``` -`camel run` adds the dependencies by itself: camel-ocsf, camel-langchain4j-chat and the LangChain4j Ollama model of the `chatModel` bean. +`camel run` adds the dependencies by itself: camel-ocsf, camel-langchain4j-chat and the LangChain4j Ollama module of the +`ollama` provider set in `application.properties`. The server starts on port 8081. Check the health endpoint to verify: diff --git a/security/ocsf/application.properties b/security/ocsf/application.properties index 3eecb16..554436d 100644 --- a/security/ocsf/application.properties +++ b/security/ocsf/application.properties @@ -23,3 +23,9 @@ camel.main.name = OcsfSecurityEventProcessor # Or manually: ollama run granite4:3b ollama.base.url=http://localhost:11434 ollama.model.name=granite4:3b + +# the chat model of the langchain4j-chat endpoints, by provider: camel run downloads langchain4j-ollama for it +camel.component.langchain4j-chat.provider=ollama +camel.component.langchain4j-chat.base-url={{ollama.base.url}} +camel.component.langchain4j-chat.model-name={{ollama.model.name}} +camel.component.langchain4j-chat.temperature=0.3 diff --git a/security/ocsf/ocsf.camel.yaml b/security/ocsf/ocsf.camel.yaml index b2d4d3c..9752af4 100644 --- a/security/ocsf/ocsf.camel.yaml +++ b/security/ocsf/ocsf.camel.yaml @@ -1,13 +1,3 @@ -- beans: - - name: chatModel - type: dev.langchain4j.model.ollama.OllamaChatModel - builderClass: dev.langchain4j.model.ollama.OllamaChatModel$OllamaChatModelBuilder - builderMethod: build - properties: - baseUrl: "{{ollama.base.url}}" - modelName: "{{ollama.model.name}}" - temperature: 0.3 - - route: id: ocsf-detection-finding-receiver from: @@ -471,7 +461,6 @@ uri: langchain4j-chat parameters: chatId: security-analyst - chatModel: "#chatModel" - setProperty: name: aiSummary expression:
