diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json index 3adf1145df77a..69f27508f5396 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json @@ -46,7 +46,9 @@ "CamelLangChain4jAgentFinishReason": { "index": 6, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "dev.langchain4j.model.output.FinishReason", "enum": [ "STOP", "LENGTH", "TOOL_EXECUTION", "CONTENT_FILTER", "OTHER" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Finish Reason.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#FINISH_REASON" }, "CamelLangChain4jAgentInputTokenCount": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Input Token Count.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#INPUT_TOKEN_COUNT" }, "CamelLangChain4jAgentOutputTokenCount": { "index": 8, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Output Token Count.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#OUTPUT_TOKEN_COUNT" }, - "CamelLangChain4jAgentTotalTokenCount": { "index": 9, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Total Token Count.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#TOTAL_TOKEN_COUNT" } + "CamelLangChain4jAgentTotalTokenCount": { "index": 9, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Total Token Count.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#TOTAL_TOKEN_COUNT" }, + "CamelLangChain4jAgentSources": { "index": 10, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "RAG sources retrieved during agent invocation.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#SOURCES" }, + "CamelLangChain4jAgentToolExecutions": { "index": 11, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tool executions performed during agent invocation.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#TOOL_EXECUTIONS" } }, "properties": { "agentId": { "index": 0, "kind": "path", "displayName": "Agent Id", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Agent id" }, diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java index bb858e62da52b..c10f933ffa7d7 100644 --- a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java +++ b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java @@ -55,4 +55,12 @@ public class Headers { @Metadata(description = "The Total Token Count.", javaType = "int") public static final String TOTAL_TOKEN_COUNT = "CamelLangChain4jAgentTotalTokenCount"; + + @Metadata(description = "RAG sources retrieved during agent invocation.", + javaType = "java.util.List") + public static final String SOURCES = "CamelLangChain4jAgentSources"; + + @Metadata(description = "Tool executions performed during agent invocation.", + javaType = "java.util.List") + public static final String TOOL_EXECUTIONS = "CamelLangChain4jAgentToolExecutions"; } diff --git a/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json b/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json index 3adf1145df77a..69f27508f5396 100644 --- a/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json +++ b/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json @@ -46,7 +46,9 @@ "CamelLangChain4jAgentFinishReason": { "index": 6, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "dev.langchain4j.model.output.FinishReason", "enum": [ "STOP", "LENGTH", "TOOL_EXECUTION", "CONTENT_FILTER", "OTHER" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Finish Reason.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#FINISH_REASON" }, "CamelLangChain4jAgentInputTokenCount": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Input Token Count.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#INPUT_TOKEN_COUNT" }, "CamelLangChain4jAgentOutputTokenCount": { "index": 8, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Output Token Count.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#OUTPUT_TOKEN_COUNT" }, - "CamelLangChain4jAgentTotalTokenCount": { "index": 9, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Total Token Count.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#TOTAL_TOKEN_COUNT" } + "CamelLangChain4jAgentTotalTokenCount": { "index": 9, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Total Token Count.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#TOTAL_TOKEN_COUNT" }, + "CamelLangChain4jAgentSources": { "index": 10, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "RAG sources retrieved during agent invocation.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#SOURCES" }, + "CamelLangChain4jAgentToolExecutions": { "index": 11, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tool executions performed during agent invocation.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#TOOL_EXECUTIONS" } }, "properties": { "agentId": { "index": 0, "kind": "path", "displayName": "Agent Id", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Agent id" }, diff --git a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java index 13219372ccb69..b03386367032c 100644 --- a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java +++ b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java @@ -128,10 +128,10 @@ public void process(Exchange exchange) throws Exception { ToolProvider toolProvider = createComposedToolProvider(tags, exchange); Result result = agent.chat(aiAgentBody, toolProvider); exchange.getMessage().setBody(result.content()); - populateTokenUsageHeaders(result, exchange); + populateResultHeaders(result, exchange); } - private void populateTokenUsageHeaders(Result result, Exchange exchange) { + private void populateResultHeaders(Result result, Exchange exchange) { Message message = exchange.getMessage(); if (result.finishReason() != null) { @@ -143,6 +143,14 @@ private void populateTokenUsageHeaders(Result result, Exchange exchange) message.setHeader(Headers.OUTPUT_TOKEN_COUNT, result.tokenUsage().outputTokenCount()); message.setHeader(Headers.TOTAL_TOKEN_COUNT, result.tokenUsage().totalTokenCount()); } + + if (result.sources() != null && !result.sources().isEmpty()) { + message.setHeader(Headers.SOURCES, result.sources()); + } + + if (result.toolExecutions() != null && !result.toolExecutions().isEmpty()) { + message.setHeader(Headers.TOOL_EXECUTIONS, result.toolExecutions()); + } } /** diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentResultHeadersTest.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentResultHeadersTest.java new file mode 100644 index 0000000000000..ad359d87a0933 --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentResultHeadersTest.java @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import dev.langchain4j.invocation.InvocationContext; +import dev.langchain4j.model.output.FinishReason; +import dev.langchain4j.model.output.TokenUsage; +import dev.langchain4j.rag.content.Content; +import dev.langchain4j.service.Result; +import dev.langchain4j.service.tool.ToolExecution; +import dev.langchain4j.service.tool.ToolExecutionResult; +import org.apache.camel.Exchange; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.langchain4j.agent.api.Agent; +import org.apache.camel.component.langchain4j.agent.api.Headers; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.spi.Registry; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +class LangChain4jAgentResultHeadersTest extends CamelTestSupport { + + private final AtomicReference agentRef = new AtomicReference<>(); + + @Override + protected void bindToRegistry(Registry registry) { + registry.bind("testAgent", (Agent) (body, exchange) -> agentRef.get().chat(body, exchange)); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .to("langchain4j-agent:test?agent=#testAgent") + .to("mock:result"); + } + }; + } + + @BeforeEach + void resetAgent() { + agentRef.set((body, exchange) -> { + throw new UnsupportedOperationException("Agent stub must be configured per test"); + }); + } + + @Test + void shouldPopulateAllResultHeaders() throws Exception { + Content ragSource = Content.from("Apache Camel documentation excerpt"); + ToolExecution toolExecution = toolExecution("call-1", "weather", "{\"city\":\"London\"}", "Sunny, 22C"); + + agentRef.set((body, exchange) -> Result. builder() + .content("The weather in London is sunny.") + .finishReason(FinishReason.STOP) + .tokenUsage(new TokenUsage(120, 30, 150)) + .sources(List.of(ragSource)) + .toolExecutions(List.of(toolExecution)) + .build()); + + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(1); + + template.sendBody("direct:start", "What is the weather in London?"); + + mock.assertIsSatisfied(10, TimeUnit.SECONDS); + + Exchange exchange = mock.getExchanges().get(0); + assertEquals("The weather in London is sunny.", exchange.getMessage().getBody(String.class)); + assertEquals(FinishReason.STOP, exchange.getMessage().getHeader(Headers.FINISH_REASON)); + assertEquals(120, exchange.getMessage().getHeader(Headers.INPUT_TOKEN_COUNT, Integer.class)); + assertEquals(30, exchange.getMessage().getHeader(Headers.OUTPUT_TOKEN_COUNT, Integer.class)); + assertEquals(150, exchange.getMessage().getHeader(Headers.TOTAL_TOKEN_COUNT, Integer.class)); + + @SuppressWarnings("unchecked") + List sources = exchange.getMessage().getHeader(Headers.SOURCES, List.class); + assertEquals(1, sources.size()); + assertSame(ragSource, sources.get(0)); + + @SuppressWarnings("unchecked") + List toolExecutions = exchange.getMessage().getHeader(Headers.TOOL_EXECUTIONS, List.class); + assertEquals(1, toolExecutions.size()); + assertSame(toolExecution, toolExecutions.get(0)); + assertEquals("weather", toolExecutions.get(0).request().name()); + assertEquals("Sunny, 22C", toolExecutions.get(0).result()); + } + + @Test + void shouldNotSetSourcesOrToolExecutionsWhenEmpty() throws Exception { + agentRef.set((body, exchange) -> Result. builder() + .content("Plain answer") + .finishReason(FinishReason.STOP) + .tokenUsage(new TokenUsage(10, 5, 15)) + .sources(Collections.emptyList()) + .toolExecutions(Collections.emptyList()) + .build()); + + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(1); + + template.sendBody("direct:start", "Hello"); + + mock.assertIsSatisfied(10, TimeUnit.SECONDS); + + Exchange exchange = mock.getExchanges().get(0); + assertEquals("Plain answer", exchange.getMessage().getBody(String.class)); + assertEquals(FinishReason.STOP, exchange.getMessage().getHeader(Headers.FINISH_REASON)); + assertNull(exchange.getMessage().getHeader(Headers.SOURCES)); + assertNull(exchange.getMessage().getHeader(Headers.TOOL_EXECUTIONS)); + } + + @Test + void shouldNotSetSourcesOrToolExecutionsWhenAbsent() throws Exception { + agentRef.set((body, exchange) -> Result. builder() + .content("Minimal answer") + .build()); + + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(1); + + template.sendBody("direct:start", "Hello"); + + mock.assertIsSatisfied(10, TimeUnit.SECONDS); + + Exchange exchange = mock.getExchanges().get(0); + assertEquals("Minimal answer", exchange.getMessage().getBody(String.class)); + assertNull(exchange.getMessage().getHeader(Headers.FINISH_REASON)); + assertNull(exchange.getMessage().getHeader(Headers.INPUT_TOKEN_COUNT)); + assertNull(exchange.getMessage().getHeader(Headers.OUTPUT_TOKEN_COUNT)); + assertNull(exchange.getMessage().getHeader(Headers.TOTAL_TOKEN_COUNT)); + assertNull(exchange.getMessage().getHeader(Headers.SOURCES)); + assertNull(exchange.getMessage().getHeader(Headers.TOOL_EXECUTIONS)); + } + + @Test + void shouldPopulateOnlySourcesWhenNoToolsExecuted() throws Exception { + Content ragSource = Content.from("Product manual section 3.2"); + + agentRef.set((body, exchange) -> Result. builder() + .content("Based on the manual, reset the device.") + .sources(List.of(ragSource)) + .build()); + + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(1); + + template.sendBody("direct:start", "How do I reset?"); + + mock.assertIsSatisfied(10, TimeUnit.SECONDS); + + Exchange exchange = mock.getExchanges().get(0); + @SuppressWarnings("unchecked") + List sources = exchange.getMessage().getHeader(Headers.SOURCES, List.class); + assertEquals(1, sources.size()); + assertSame(ragSource, sources.get(0)); + assertNull(exchange.getMessage().getHeader(Headers.TOOL_EXECUTIONS)); + } + + @Test + void shouldPopulateOnlyToolExecutionsWhenNoRagSources() throws Exception { + ToolExecution toolExecution = toolExecution("call-2", "inventory", "{\"sku\":\"ABC-123\"}", "42 units in stock"); + + agentRef.set((body, exchange) -> Result. builder() + .content("There are 42 units in stock.") + .toolExecutions(List.of(toolExecution)) + .build()); + + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(1); + + template.sendBody("direct:start", "Stock level for ABC-123?"); + + mock.assertIsSatisfied(10, TimeUnit.SECONDS); + + Exchange exchange = mock.getExchanges().get(0); + assertNull(exchange.getMessage().getHeader(Headers.SOURCES)); + @SuppressWarnings("unchecked") + List toolExecutions = exchange.getMessage().getHeader(Headers.TOOL_EXECUTIONS, List.class); + assertEquals(1, toolExecutions.size()); + assertEquals("inventory", toolExecutions.get(0).request().name()); + assertFalse(toolExecutions.get(0).hasFailed()); + } + + private static ToolExecution toolExecution(String id, String name, String arguments, String resultText) { + return ToolExecution.builder() + .request(ToolExecutionRequest.builder() + .id(id) + .name(name) + .arguments(arguments) + .build()) + .result(ToolExecutionResult.builder().resultText(resultText).build()) + .invocationContext(InvocationContext.builder().build()) + .build(); + } +} diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index e70c63e1c5506..3305ca8373dcc 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -177,6 +177,11 @@ String chat(AiAgentBody aiAgentBody, ToolProvider toolProvider); Result chat(AiAgentBody aiAgentBody, ToolProvider toolProvider); ---- +When RAG or tools are used, the agent producer also exposes `CamelLangChain4jAgentSources` +(`List`) and `CamelLangChain4jAgentToolExecutions` +(`List`) as exchange headers when present in the +`Result`. + === camel-langchain4j-chat The helper classes `OpenAiChatLanguageModelBuilder` and `HugginFaceChatLanguageModelBuilder` have been removed. diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java index f5bcb6644e06e..2bb0c6dfa5072 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java @@ -556,6 +556,33 @@ public String langChain4jAgentOutputTokenCount() { public String langChain4jAgentTotalTokenCount() { return "CamelLangChain4jAgentTotalTokenCount"; } + /** + * RAG sources retrieved during agent invocation. + * + * The option is a: {@code + * java.util.List} type. + * + * Group: producer + * + * @return the name of the header {@code LangChain4jAgentSources}. + */ + public String langChain4jAgentSources() { + return "CamelLangChain4jAgentSources"; + } + /** + * Tool executions performed during agent invocation. + * + * The option is a: {@code + * java.util.List} type. + * + * Group: producer + * + * @return the name of the header {@code + * LangChain4jAgentToolExecutions}. + */ + public String langChain4jAgentToolExecutions() { + return "CamelLangChain4jAgentToolExecutions"; + } } static LangChain4jAgentEndpointBuilder endpointBuilder(String componentName, String path) { class LangChain4jAgentEndpointBuilderImpl extends AbstractEndpointBuilder implements LangChain4jAgentEndpointBuilder, AdvancedLangChain4jAgentEndpointBuilder {