Croway commented on code in PR #23996:
URL: https://github.com/apache/camel/pull/23996#discussion_r3404212232


##########
components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java:
##########
@@ -102,7 +121,13 @@ public void process(Exchange exchange) throws Exception {
 
         ToolProvider toolProvider = createComposedToolProvider(tags, exchange);
         String response = agent.chat(aiAgentBody, toolProvider);
-        exchange.getMessage().setBody(response);
+
+        Class<?> responseType = endpoint.getConfiguration().getResponseType();

Review Comment:
   **`responseType` parsing runs even when the schema was never applied to the 
model.** This lookup makes `process()` parse with `responseType` regardless of 
whether `resolveAndApplyStructuredOutput()` actually ran in `doInit()`. Two 
reachable configurations bypass it:
   
   1. `agent` + `agentConfiguration` both set — both options are 
`autowired=true`, so a registry containing one bean of each auto-injects both. 
The `agent != null` branch wins and the `ResponseFormat` is never applied, yet 
the startup guard passes because `agentConfiguration != null`.
   2. `agentFactory` + `agentConfiguration` + `responseType` — the 
factory-created agent replaces the internal agent on every exchange and never 
receives the `ResponseFormat` that `doInit()` applied to the (discarded) 
internal agent.
   
   In both cases the model gets no JSON schema, answers in free-form prose, and 
`SERVICE_OUTPUT_PARSER.parseText` throws `OutputParsingException` on every 
message at runtime instead of failing fast at startup. Suggest extending the 
`doInit()` validation to reject `responseType` when `agent` or `agentFactory` 
is configured (or fixing it at the root — see the comment on 
`setResponseFormat` below).



##########
components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java:
##########
@@ -102,7 +121,13 @@ public void process(Exchange exchange) throws Exception {
 
         ToolProvider toolProvider = createComposedToolProvider(tags, exchange);
         String response = agent.chat(aiAgentBody, toolProvider);
-        exchange.getMessage().setBody(response);
+
+        Class<?> responseType = endpoint.getConfiguration().getResponseType();
+        if (responseType != null) {
+            
exchange.getMessage().setBody(SERVICE_OUTPUT_PARSER.parseText(responseType, 
response));

Review Comment:
   **No error handling around parsing — raw response is lost on failure.** If 
the model returns a null/blank/unparseable response (content filter, guardrail 
rewrite, provider quirk), `parseText` throws a raw 
`dev.langchain4j.service.output.OutputParsingException` and the original 
response text is gone — `onException` handlers can't log it or route a 
fallback. Consider wrapping in a Camel-friendly exception that carries the raw 
response (or preserving it in a header) so routes keep an escape hatch.



##########
components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java:
##########
@@ -305,41 +330,55 @@ private Map<String, CamelToolSpecification> 
discoverToolsByName(String tags) {
     }
 
     /**
-     * Resolves the jsonSchema endpoint property: loads from 
classpath/filesystem if it is a resource URI, otherwise
-     * treats it as inline JSON. Converts the raw JSON string into a 
langchain4j ResponseFormat and sets it on the
-     * agent. Only called when the agent is created internally from 
agentConfiguration.
+     * Resolves and applies structured output configuration (jsonSchema or 
responseType) to the agent. Only called when
+     * the agent is created internally from agentConfiguration.
      */
-    private void resolveAndApplyJsonSchema() throws Exception {
+    private void resolveAndApplyStructuredOutput() throws Exception {
         String jsonSchema = endpoint.getConfiguration().getJsonSchema();
-        if (ObjectHelper.isEmpty(jsonSchema)) {
+        Class<?> responseType = endpoint.getConfiguration().getResponseType();
+
+        if (ObjectHelper.isEmpty(jsonSchema) && responseType == null) {
             return;
         }
 
-        String resolved = 
endpoint.getCamelContext().resolvePropertyPlaceholders(jsonSchema);
+        if (responseType != null) {
+            JsonSchema schema = JsonSchemas.jsonSchemaFrom(responseType)

Review Comment:
   **Non-POJO `responseType` values fail with a misleading error message.** In 
langchain4j 1.16.2, `JsonSchemas.jsonSchemaFrom()` returns `Optional.empty()` 
for any type whose output parser is not `PojoOutputParser` — `String`, 
`Integer`, enums, `List`/`Set`, primitives. So e.g. 
`responseType=java.lang.Integer` fails startup with "Ensure the class has 
public fields or getters" even though `Integer` has getters; the real 
restriction is POJO-only. Suggest rewording the message (e.g. "responseType 
must be a POJO class; simple types, enums and collections are not supported") 
so users aren't sent chasing the wrong fix.



##########
components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java:
##########
@@ -305,41 +330,55 @@ private Map<String, CamelToolSpecification> 
discoverToolsByName(String tags) {
     }
 
     /**
-     * Resolves the jsonSchema endpoint property: loads from 
classpath/filesystem if it is a resource URI, otherwise
-     * treats it as inline JSON. Converts the raw JSON string into a 
langchain4j ResponseFormat and sets it on the
-     * agent. Only called when the agent is created internally from 
agentConfiguration.
+     * Resolves and applies structured output configuration (jsonSchema or 
responseType) to the agent. Only called when
+     * the agent is created internally from agentConfiguration.
      */
-    private void resolveAndApplyJsonSchema() throws Exception {
+    private void resolveAndApplyStructuredOutput() throws Exception {
         String jsonSchema = endpoint.getConfiguration().getJsonSchema();
-        if (ObjectHelper.isEmpty(jsonSchema)) {
+        Class<?> responseType = endpoint.getConfiguration().getResponseType();
+
+        if (ObjectHelper.isEmpty(jsonSchema) && responseType == null) {
             return;
         }
 
-        String resolved = 
endpoint.getCamelContext().resolvePropertyPlaceholders(jsonSchema);
+        if (responseType != null) {
+            JsonSchema schema = JsonSchemas.jsonSchemaFrom(responseType)
+                    .orElseThrow(() -> new IllegalArgumentException(
+                            "Cannot derive JSON schema from responseType: " + 
responseType.getName()
+                                                                    + ". 
Ensure the class has public fields or getters and is not a raw generic type."));
+            ResponseFormat derivedFormat = ResponseFormat.builder()
+                    .type(ResponseFormatType.JSON)
+                    .jsonSchema(schema)

Review Comment:
   **Schema name diverges from the documented `camel_schema` convention.** 
`JsonSchemas.jsonSchemaFrom()` names the schema `rawClass.getSimpleName()` 
(verified in langchain4j 1.16.2 sources), while the `jsonSchema` branch below 
deliberately uses the fixed name `camel_schema` with a comment citing 
cross-component consistency with camel-openai. Migrating a route from 
`jsonSchema` to `responseType` therefore silently changes the 
`json_schema.name` sent to OpenAI-compatible providers from `camel_schema` to 
e.g. `CarRentalRecommendation`. Consider rebuilding the derived schema with 
`.name("camel_schema")` to keep both options consistent.



##########
components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java:
##########
@@ -56,6 +58,10 @@
 
 public class LangChain4jAgentProducer extends DefaultProducer {
     private static final Logger LOG = 
LoggerFactory.getLogger(LangChain4jAgentProducer.class);
+    // ServiceOutputParser is used intentionally: beyond JSON parsing, it 
strips markdown code fences
+    // (e.g. ```json ... ```) that some LLMs emit even when ResponseFormat is 
explicitly set to JSON.
+    // Plain Jackson would throw a JsonParseException on such responses.
+    private static final ServiceOutputParser SERVICE_OUTPUT_PARSER = new 
ServiceOutputParser();

Review Comment:
   Heads-up: `ServiceOutputParser` is annotated `@Internal` in langchain4j 
1.16.2 (verified via `javap`; `JsonSchemas` by contrast is public API), and 
this is the only place in Camel that uses it. The code-fence-stripping 
rationale in the comment is a fair tradeoff, but its signature can change 
without deprecation on any langchain4j upgrade — worth keeping in mind when 
bumping the dependency.



##########
components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java:
##########
@@ -305,41 +330,55 @@ private Map<String, CamelToolSpecification> 
discoverToolsByName(String tags) {
     }
 
     /**
-     * Resolves the jsonSchema endpoint property: loads from 
classpath/filesystem if it is a resource URI, otherwise
-     * treats it as inline JSON. Converts the raw JSON string into a 
langchain4j ResponseFormat and sets it on the
-     * agent. Only called when the agent is created internally from 
agentConfiguration.
+     * Resolves and applies structured output configuration (jsonSchema or 
responseType) to the agent. Only called when
+     * the agent is created internally from agentConfiguration.
      */
-    private void resolveAndApplyJsonSchema() throws Exception {
+    private void resolveAndApplyStructuredOutput() throws Exception {
         String jsonSchema = endpoint.getConfiguration().getJsonSchema();
-        if (ObjectHelper.isEmpty(jsonSchema)) {
+        Class<?> responseType = endpoint.getConfiguration().getResponseType();
+
+        if (ObjectHelper.isEmpty(jsonSchema) && responseType == null) {
             return;
         }
 
-        String resolved = 
endpoint.getCamelContext().resolvePropertyPlaceholders(jsonSchema);
+        if (responseType != null) {
+            JsonSchema schema = JsonSchemas.jsonSchemaFrom(responseType)
+                    .orElseThrow(() -> new IllegalArgumentException(
+                            "Cannot derive JSON schema from responseType: " + 
responseType.getName()
+                                                                    + ". 
Ensure the class has public fields or getters and is not a raw generic type."));
+            ResponseFormat derivedFormat = ResponseFormat.builder()
+                    .type(ResponseFormatType.JSON)
+                    .jsonSchema(schema)
+                    .build();
+            ((AbstractAgent<?>) agent).setResponseFormat(derivedFormat);

Review Comment:
   **Design suggestion:** rather than the cast+setter, a `responseFormat` field 
on `AgentConfiguration` consumed by `AbstractAgent.configureBuilder()` (which 
already sources guardrails, tools, RAG and memory from the configuration) would 
let factory-created agents and user-provided `AbstractAgent` subclasses receive 
the format too — removing the "requires agentConfiguration" restriction at the 
root and fixing the agentFactory gap noted above. The cast pattern pre-dates 
this PR (CAMEL-23642 introduced it for `jsonSchema`), but this PR adds a second 
cast site, so it may be the right moment to generalize.



##########
components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java:
##########
@@ -305,41 +330,55 @@ private Map<String, CamelToolSpecification> 
discoverToolsByName(String tags) {
     }
 
     /**
-     * Resolves the jsonSchema endpoint property: loads from 
classpath/filesystem if it is a resource URI, otherwise
-     * treats it as inline JSON. Converts the raw JSON string into a 
langchain4j ResponseFormat and sets it on the
-     * agent. Only called when the agent is created internally from 
agentConfiguration.
+     * Resolves and applies structured output configuration (jsonSchema or 
responseType) to the agent. Only called when
+     * the agent is created internally from agentConfiguration.
      */
-    private void resolveAndApplyJsonSchema() throws Exception {
+    private void resolveAndApplyStructuredOutput() throws Exception {

Review Comment:
   Minor: both branches of this method duplicate the 
`ResponseFormat.builder().type(JSON).jsonSchema(...).build()` + 
`((AbstractAgent<?>) agent).setResponseFormat(...)` epilogue, and the feature's 
validation is spread over three places (top of `doInit`, inside the 
`agentConfiguration` branch, and the early-return here). Deriving the 
`JsonSchema` per branch and building/applying the format once — plus one 
consolidated validation block — would prevent drift; the two branches have 
already diverged on schema naming.



##########
components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConfiguration.java:
##########
@@ -58,10 +58,18 @@ public class LangChain4jAgentConfiguration implements 
Cloneable {
 
     @UriParam
     @Metadata(description = "JSON schema for structured output validation. "
-                            + "This option works only when using 
agentConfiguration (inline agent creation mode).",
+                            + "This option works only when using 
agentConfiguration (inline agent creation mode). "
+                            + "Mutually exclusive with responseType.",
               supportFileReference = true, largeInput = true, inputLanguage = 
"json")
     private String jsonSchema;
 
+    @UriParam
+    @Metadata(description = "Java class to use as response format for 
structured outputs. "
+                            + "Camel will automatically derive the JSON schema 
from the class structure and unmarshal the response. "
+                            + "This option works only when using 
agentConfiguration (inline agent creation mode). "
+                            + "Mutually exclusive with jsonSchema.")
+    private Class<?> responseType;

Review Comment:
   Consistency note: camel-openai exposes the equivalent feature as 
`outputClass` — a `String` resolved at request time via `ClassResolver`, 
overridable per message via the `CamelOpenAIOutputClass` header, with the body 
left as the raw JSON string. This option differs in name, type, dynamicity and 
body semantics (auto-unmarshalled POJO), even though the PR borrows 
camel-openai's `camel_schema` convention. A `Class<?>` bound at startup also 
bypasses the `ClassResolver` indirection, which matters in 
classloader-partitioned runtimes. Worth aligning the surface (or at least 
documenting the difference) so users moving between camel-ai components aren't 
surprised.



##########
components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java:
##########
@@ -72,15 +78,28 @@ public LangChain4jAgentProducer(LangChain4jAgentEndpoint 
endpoint) {
     protected void doInit() throws Exception {
         super.doInit();
 
+        if (endpoint.getConfiguration().getResponseType() != null
+                && endpoint.getConfiguration().getAgentConfiguration() == 
null) {
+            throw new IllegalArgumentException(
+                    "responseType requires agentConfiguration to be set. "
+                                               + "It only works when Camel 
creates the agent internally (inline agent creation mode).");
+        }
+
         if (endpoint.getConfiguration().getAgent() != null) {
             agent = endpoint.getConfiguration().getAgent();
         } else if (endpoint.getConfiguration().getAgentConfiguration() != 
null) {
+            if 
(ObjectHelper.isNotEmpty(endpoint.getConfiguration().getJsonSchema())

Review Comment:
   **The mutual-exclusion check is unreachable when an `agent` bean is also 
configured.** Because this check is nested inside the `else if 
(agentConfiguration != null)` branch, an endpoint with 
`agent=#myAgent&agentConfiguration=#cfg&jsonSchema=...&responseType=...` starts 
up cleanly with both "mutually exclusive" options set — `jsonSchema` is 
silently dropped while `responseType` still drives response parsing. Moving the 
check to the top-level validation in `doInit()` (alongside the 
`responseType`-requires-`agentConfiguration` guard) would make the documented 
contract hold on every path.



##########
components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java:
##########
@@ -102,7 +121,13 @@ public void process(Exchange exchange) throws Exception {
 
         ToolProvider toolProvider = createComposedToolProvider(tags, exchange);

Review Comment:
   Pre-existing issue adjacent to this change (not introduced by this PR, 
noting for a follow-up JIRA): a few lines above, `agent = 
agentFactory.createAgent(exchange)` writes the shared instance field from 
`process()` on a singleton producer, so concurrent exchanges race — thread A 
can end up chatting with thread B's factory-created agent (cross-tenant 
memory/credential leakage with per-tenant factories). The fix is a local 
variable: `Agent agent = agentFactory != null ? 
agentFactory.createAgent(exchange) : this.agent;`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to