Skip to content
Draft
Show file tree
Hide file tree
Changes from 80 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
f45056e
feat(agentic-ai): extract documents from tool call results into user …
maff Apr 20, 2026
e00871f
feat(agentic-ai): use Jackson bean introspection for document extraction
maff Apr 20, 2026
90c7662
fix(agentic-ai): preserve raw document references in gateway tool cal…
maff Apr 20, 2026
90c0bf2
test(agentic-ai): add document extraction tests for gateway tool hand…
maff Apr 20, 2026
b18c43c
feat(agentic-ai): add array support to document extractor walker
maff Apr 20, 2026
7f01c66
feat(agentic-ai): use XML document tags in tool call result messages
maff Apr 20, 2026
ab0c6ea
fix(agentic-ai): warn when MCP content key is missing in raw response
maff Apr 20, 2026
8110a68
feat(agentic-ai): exclude document messages from window message count
maff Apr 20, 2026
9d59267
test(agentic-ai): fix MCP handler test to use raw Map content
maff Apr 20, 2026
ff95b5a
docs(agentic-ai): rename ADR-003 to ADR-004
maff Apr 20, 2026
f3bb277
test(agentic-ai): update E2E tests for XML document tag format
maff Apr 20, 2026
d79fbf6
refactor(agentic-ai): move METADATA_TOOL_CALL_DOCUMENTS to UserMessage
maff Apr 20, 2026
502bf22
refactor(agentic-ai): address CodeQL findings
maff Apr 21, 2026
9dc6928
fix(agentic-ai): escape XML attribute values in document tags
maff Apr 21, 2026
4545c8c
perf(agentic-ai): avoid O(n^2) message counting in window eviction
maff Apr 21, 2026
ea75936
refactor(agentic-ai): improve test assertions and code style
maff Apr 21, 2026
e28558e
docs(agentic-ai): update ADR-004 to reflect implementation
maff Apr 22, 2026
0d28ff0
fix(agentic-ai): guard effectiveCount decrement for document messages
maff Apr 22, 2026
65b2ae1
refactor(agentic-ai): extract DocumentXmlTag record from handler
maff Apr 22, 2026
91024cc
test(e2e): add cross-provider CPT viability test for document tool ca…
maff Apr 22, 2026
f4bd520
fix(agentic-ai): remove duplicate jackson-datatype-document dependency
maff Apr 22, 2026
e835f71
test(e2e): add Ollama providers and convenience toggles for CPT test
maff Apr 23, 2026
2a04d09
refactor(e2e): rename DocumentToolCallResultsCPTTest to DocumentToolC…
maff Apr 23, 2026
9582a51
fix(e2e): remove unused parameter and variable in DocumentToolCallRes…
maff Apr 23, 2026
94ef8c9
refactor(e2e): move IT out of cpt package and rename test resources
maff Apr 23, 2026
11a505f
docs(agentic-ai): update ADR-004 status to Implemented
maff Apr 23, 2026
fb93339
refactor(agentic-ai): per-handler tool call result document extraction
maff Apr 29, 2026
d834d01
refactor(agentic-ai): make extractor the entrypoint, walker a static …
maff Apr 29, 2026
f8e4616
refactor(agentic-ai): minimise transform-method changes in MCP/A2A ha…
maff Apr 29, 2026
dbefe0a
docs(agentic-ai): document new SPI hook + add fidelity test and addre…
maff Apr 29, 2026
1943df9
test(agentic-ai): drop redundant GatewayToolResultDocumentSerializati…
maff Apr 29, 2026
fc68c58
chore(agentic-ai): address review nits — JsonNode.fields() deprecatio…
maff Apr 29, 2026
81ba14c
chore(agentic-ai): reformat tests
maff May 4, 2026
6c0009c
docs(agentic-ai): reframe ADR-004 multi-content limitation as provide…
maff May 7, 2026
71456c5
test(e2e): extract document tool-call assertions into reusable helper
maff May 7, 2026
6b664f8
refactor(agentic-ai): drop "unknown" fallback in tool call document e…
maff May 7, 2026
574d322
refactor(agentic-ai): swap DocumentXmlTag parameter order to (toolCal…
maff May 7, 2026
5707929
feat(agentic-ai): add debug logging for tool call document extraction
maff May 7, 2026
b1083d4
docs(agentic-ai): Add ADR to replace langchain4j
nikonovd May 6, 2026
226615e
test(agentic-ai): Add e2e tests against a wiremock as regression test…
nikonovd May 6, 2026
c15b64a
test: verify api key authorization
nikonovd May 6, 2026
e4a04c4
feat(agentic-ai): extend data model to support common llm provider pr…
nikonovd May 6, 2026
35fea05
feat: add blank spi without logic as preparation
nikonovd May 7, 2026
aa13533
fix: add canonical constructor to AgentMetrics.TokenUsage
maff May 7, 2026
98e0011
refactor(agentic-ai): tighten naming and ChatOptions surface
maff May 7, 2026
55886ca
feat(agentic-ai): add ResponseFormat to ChatOptions SPI
maff May 7, 2026
e7a079e
refactor(agentic-ai): dispatch chat factories by providerType string
maff May 7, 2026
f21efbb
feat(agentic-ai): route chat calls through ChatClient SPI
maff May 7, 2026
2d0f303
refactor(agentic-ai): align ChatClient SPI with the phased plan
maff May 7, 2026
f19eec0
docs(agentic-ai): align AGENTS.md and ADR-004 plan with as-built SPI
maff May 7, 2026
304442a
docs(agentic-ai): re-order ADR-004 plan to ship native Anthropic+Open…
maff May 7, 2026
a979a1a
feat(agentic-ai): native Anthropic Messages ChatModelApi (text-only)
maff May 7, 2026
5b94067
feat(agentic-ai): native OpenAI Chat Completions ChatModelApi (text-o…
maff May 7, 2026
eba8a71
feat(agentic-ai): native OpenAI Responses ChatModelApi + apiFamily sw…
maff May 7, 2026
3cdfd6b
fix(agentic-ai): align native baseUrl handling + cover Responses wire…
maff May 7, 2026
df9aea5
feat(agentic-ai): capability matrix + resolver (Phase E1)
maff May 7, 2026
390aa5b
feat(agentic-ai): wire ChatModelApi.capabilities() through resolver (…
maff May 7, 2026
0c7be3c
docs(agentic-ai): renumber ADR-004 → ADR-005 + refresh Phase E plan
maff May 7, 2026
e960aa0
docs(agentic-ai): finalize E3 ToolCallResultStrategy design
maff May 7, 2026
56bf337
feat(agentic-ai): tool-call-result routing + native multimodal emissi…
maff May 7, 2026
0bd7b90
refactor(agentic-ai): rename Modality.PDF to Modality.DOCUMENT
maff May 7, 2026
5e99c3d
fix(agentic-ai): serialise ObjectContent as JSON in native impls
maff May 7, 2026
72cd95e
fix(agentic-ai): serialise Anthropic tool-result content via Connecto…
maff May 7, 2026
f635296
TMP TMP: Update BPMN example to return an image
maff May 7, 2026
92a58f5
fix(agentic-ai): load capability matrix YAML in @Configuration setEnv…
maff May 7, 2026
9935fe0
refactor(agentic-ai): drop L4J factory + provider beans for migrated …
maff May 7, 2026
041074b
docs(agentic-ai): wire Phase F deserializer via @JsonDeserialize, not…
maff May 8, 2026
0415058
refactor(agentic-ai): add AnthropicBackend + sealed AnthropicAuthenti…
nikonovd May 8, 2026
5ae3a8f
refactor(agentic-ai): address review nits on AnthropicAuthentication …
nikonovd May 8, 2026
748f087
refactor(agentic-ai): rename GoogleVertexAi → GoogleGenAi + add Googl…
nikonovd May 8, 2026
36fd7c9
refactor(agentic-ai): update stale SaaS validation message in GoogleG…
nikonovd May 8, 2026
554a52e
refactor(agentic-ai): add Bedrock non-Anthropic model validation (Pha…
nikonovd May 8, 2026
8747dd4
refactor(agentic-ai): consolidate OpenAI configs into single OpenAiPr…
nikonovd May 8, 2026
ba4fb32
refactor(agentic-ai): rename stale Azure bean name to langchain4JOpen…
nikonovd May 8, 2026
de0b0b3
refactor(agentic-ai): update OpenAiChatModelApiFactory for backend br…
nikonovd May 8, 2026
fd675ed
refactor(agentic-ai): require endpoint for FOUNDRY and CUSTOM OpenAI …
nikonovd May 8, 2026
f4b755b
feat(agentic-ai): add ProviderConfigurationDeserializer for backward-…
nikonovd May 8, 2026
8f818e9
refactor(agentic-ai): guard against ClassCastException in deserialize…
nikonovd May 8, 2026
291e4f8
feat(agentic-ai): bump element template v11→v12 with backend dropdowns
nikonovd May 8, 2026
77b5c91
feat(agentic-ai): Implement provider configuration (Phase F)
nikonovd May 8, 2026
7b90612
docs(agentic-ai): catch up ADR-005 plan with E3+E4 and F as built
maff May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions connectors-e2e-test/connectors-e2e-test-agentic-ai/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
<artifactId>camunda-process-test-spring</artifactId>
<version>${version.camunda}</version>
</dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>camunda-process-test-langchain4j</artifactId>
<version>${version.camunda}</version>
<scope>test</scope>
</dependency>

<!-- Test -->
<dependency>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership. Camunda licenses this file to you under the Apache License,
* Version 2.0; 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 io.camunda.connector.e2e.agenticai.aiagent;

import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.Content;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.PdfFileContent;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import io.camunda.connector.agenticai.model.message.DocumentXmlTag;
import io.camunda.connector.document.jackson.DocumentReferenceModel;
import io.camunda.connector.document.jackson.DocumentReferenceModel.CamundaDocumentReferenceModel;
import java.util.function.Consumer;

/**
* Assertion helpers for the synthetic {@link UserMessage} that carries documents extracted from
* tool call results.
*
* <p>The user message has the structure {@code preamble + (xml-tag, content-block)*}, so it can
* carry documents from one or many tool call results. Use {@link
* #assertExtractedDocumentsUserMessage(ChatMessage, ExtractedDocument...)} to assert against any
* number of documents in one go.
*/
public final class ToolCallResultDocumentAssertions {

static final String EXTRACTED_DOCUMENTS_PREAMBLE = "Documents extracted from tool call results:";

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private ToolCallResultDocumentAssertions() {}

/**
* Locates the first Camunda document reference inside a serialized tool call result and
* deserializes it into the production {@link CamundaDocumentReferenceModel}. Recursively descends
* into objects/arrays until it finds an object carrying the {@code camunda.document.type}
* discriminator (set by {@code DocumentSerializer}).
*
* <p>Throws {@link AssertionError} if the text cannot be parsed as JSON or no document reference
* is present.
*/
public static CamundaDocumentReferenceModel parseDocumentReference(String toolResultText) {
final JsonNode root;
try {
root = OBJECT_MAPPER.readTree(toolResultText);
} catch (JsonProcessingException e) {
throw new AssertionError("Failed to parse tool result text as JSON: " + toolResultText, e);
}

final JsonNode docNode = findFirstCamundaDocumentNode(root);
if (docNode == null) {
throw new AssertionError(
"No Camunda document reference found in tool result text: " + toolResultText);
}

try {
return OBJECT_MAPPER.treeToValue(docNode, CamundaDocumentReferenceModel.class);
} catch (JsonProcessingException e) {
throw new AssertionError("Failed to deserialize Camunda document reference: " + docNode, e);
}
}

/**
* Asserts that {@code message} is a {@link UserMessage} with the standard "extracted documents"
* structure: a preamble {@link TextContent} followed by an {@code (xml-tag, content-block)} pair
* per expected document.
*
* <p>The XML tag is built using the production {@link DocumentXmlTag}, so the assertion stays in
* sync with the production format.
*/
public static void assertExtractedDocumentsUserMessage(
ChatMessage message, ExtractedDocument... expectedDocuments) {
assertThat(message).isInstanceOf(UserMessage.class);
final var contents = ((UserMessage) message).contents();

assertThat(contents)
.as("expected preamble + 2 contents per document (%d documents)", expectedDocuments.length)
.hasSize(1 + 2 * expectedDocuments.length);

assertThat(contents.get(0))
.as("preamble TextContent")
.isInstanceOfSatisfying(
TextContent.class, tc -> assertThat(tc.text()).isEqualTo(EXTRACTED_DOCUMENTS_PREAMBLE));

for (int i = 0; i < expectedDocuments.length; i++) {
final var expected = expectedDocuments[i];
final var expectedXml = expected.expectedXmlTag();
final var tagIndex = 1 + 2 * i;
final var contentIndex = tagIndex + 1;

assertThat(contents.get(tagIndex))
.as("XML tag at index %d (document %d)", tagIndex, i)
.isInstanceOfSatisfying(
TextContent.class, tc -> assertThat(tc.text()).isEqualTo(expectedXml));

final var contentBlock = contents.get(contentIndex);
try {
expected.contentBlockAssertion().accept(contentBlock);
} catch (AssertionError e) {
throw new AssertionError(
"Content block at index %d (document %d) failed: %s"
.formatted(contentIndex, i, e.getMessage()),
e);
}
}
}

/**
* Asserts a content block based on a coarse type/mime classification used by tool calling tests:
*
* <ul>
* <li>{@code "text"} → {@link TextContent}
* <li>{@code "application/pdf"} → {@link PdfFileContent} with non-blank base64
* <li>otherwise → {@link ImageContent} with matching mime type and non-blank base64
* </ul>
*/
public static void assertDocumentContentBlock(
Content content, String expectedType, String expectedMimeType) {
if ("text".equals(expectedType)) {
assertThat(content).isInstanceOf(TextContent.class);
} else if ("application/pdf".equals(expectedMimeType)) {
assertThat(content)
.isInstanceOfSatisfying(
PdfFileContent.class, pdf -> assertThat(pdf.pdfFile().base64Data()).isNotBlank());
} else {
assertThat(content)
.isInstanceOfSatisfying(
ImageContent.class,
img -> {
assertThat(img.image().mimeType()).isEqualTo(expectedMimeType);
assertThat(img.image().base64Data()).isNotBlank();
});
}
}

private static JsonNode findFirstCamundaDocumentNode(JsonNode node) {
if (node == null) {
return null;
}
if (node.isObject()) {
if ("camunda".equals(node.path(DocumentReferenceModel.DISCRIMINATOR_KEY).asText(null))) {
return node;
}
final var properties = node.properties();
for (var property : properties) {
final var found = findFirstCamundaDocumentNode(property.getValue());
if (found != null) {
return found;
}
}
} else if (node.isArray()) {
for (var element : node) {
final var found = findFirstCamundaDocumentNode(element);
if (found != null) {
return found;
}
}
}
return null;
}

/**
* Specification of one expected extracted document slot in the synthetic user message: an XML tag
* with optional tool call correlation attributes plus a content block check.
*/
public record ExtractedDocument(
String toolCallId,
String toolName,
CamundaDocumentReferenceModel reference,
Consumer<Content> contentBlockAssertion) {

/** Document extracted from a tool call result. */
public static ExtractedDocument forToolCall(
String toolCallId,
String toolName,
CamundaDocumentReferenceModel reference,
Consumer<Content> contentBlockAssertion) {
return new ExtractedDocument(toolCallId, toolName, reference, contentBlockAssertion);
}

/** The XML tag string this document is expected to render to in the synthetic user message. */
String expectedXmlTag() {
return new DocumentXmlTag(
toolCallId,
toolName,
documentShortId(),
reference.metadata() != null ? reference.metadata().fileName() : null)
.toXml();
}

private String documentShortId() {
final var documentId = reference.documentId();
final int dash = documentId.indexOf('-');
return dash > 0 ? documentId.substring(0, dash) : documentId;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@
import dev.langchain4j.model.output.TokenUsage;
import io.camunda.connector.agenticai.adhoctoolsschema.schema.AdHocToolsSchemaResolver;
import io.camunda.connector.agenticai.aiagent.framework.langchain4j.ChatModelFactory;
import io.camunda.connector.agenticai.aiagent.framework.langchain4j.document.DocumentToContentResponseModel;
import io.camunda.connector.agenticai.aiagent.model.AgentMetrics;
import io.camunda.connector.agenticai.aiagent.model.JobWorkerAgentResponse;
import io.camunda.connector.e2e.ElementTemplate;
import io.camunda.connector.e2e.ZeebeTest;
import io.camunda.connector.e2e.agenticai.aiagent.BaseAiAgentJobWorkerTest;
import io.camunda.connector.e2e.agenticai.assertj.JobWorkerAgentResponseAssert;
import io.camunda.connector.e2e.agenticai.assertj.ToolExecutionRequestEqualsPredicate;
import io.camunda.connector.e2e.agenticai.assertj.ToolExecutionResultMessageEqualsPredicate;
import io.camunda.connector.test.utils.annotation.SlowTest;
import java.time.Duration;
import java.util.ArrayList;
Expand Down Expand Up @@ -317,6 +317,9 @@ protected void assertLastChatRequest(
RecursiveComparisonConfiguration.builder()
.withEqualsForType(
new ToolExecutionRequestEqualsPredicate(), ToolExecutionRequest.class)
.withEqualsForType(
new ToolExecutionResultMessageEqualsPredicate(),
ToolExecutionResultMessage.class)
.build())
.containsExactlyElementsOf(
expectedConversation.subList(0, expectedConversation.size() - 1));
Expand Down Expand Up @@ -376,6 +379,4 @@ protected static ChatInteraction of(
return new ChatInteraction(chatResponse, userFeedback);
}
}

protected record DownloadFileToolResult(int status, DocumentToContentResponseModel document) {}
}
Loading
Loading