diff --git a/samples/agent-support-service/src/main/resources/frontend/index.html b/samples/agent-support-service/src/main/resources/frontend/index.html
index 8bbceee..1f4e037 100644
--- a/samples/agent-support-service/src/main/resources/frontend/index.html
+++ b/samples/agent-support-service/src/main/resources/frontend/index.html
@@ -157,6 +157,7 @@
let selectedPlanVersion = (runtimeUrl.searchParams.get('planVersion') || '').trim();
let agentCatalog = null;
let sessionId = `session-${Date.now()}`;
+ let ticketUpdatePollTimer = null;
let audioContext = null;
let mediaStream = null;
let sourceNode = null;
@@ -168,6 +169,7 @@
let realtimePollInFlight = false;
const handledTranscriptKeys = new Set();
const displayedVoiceMessages = new Set();
+ const displayedTicketUpdateKeys = new Set();
let lastRelayError = '';
let lastPollError = '';
let lastPollErrorAt = 0;
@@ -810,13 +812,17 @@
function renderTicketWidget(data) {
const ticketId = normalizeDisplayText(String(data.ticketId ?? 'N/A'));
const status = normalizeDisplayText(String(data.status ?? 'OPEN'));
+ const action = normalizeDisplayText(String(data.action ?? ''));
const assignedQueue = normalizeDisplayText(String(data.assignedQueue ?? 'L1-SUPPORT'));
const summary = normalizeDisplayText(String(data.summary ?? ''));
const message = normalizeDisplayText(String(data.message ?? ''));
const card = document.createElement('div');
card.className = 'ticket';
+ const title = action
+ ? `Ticket ${action.charAt(0).toUpperCase()}${action.slice(1).toLowerCase()}`
+ : 'Ticket Update';
card.innerHTML = `
-
Ticket Created
+
${title}
ID: ${ticketId}
Status: ${status}
Queue: ${assignedQueue}
@@ -826,6 +832,63 @@
Ticket Created
return card;
}
+ function clearTicketUpdatePolling() {
+ if (ticketUpdatePollTimer) {
+ clearInterval(ticketUpdatePollTimer);
+ ticketUpdatePollTimer = null;
+ }
+ }
+
+ function ticketUpdateKey(event) {
+ return [
+ event?.timestamp || '',
+ event?.taskId || '',
+ event?.source || '',
+ event?.text || ''
+ ].join('|');
+ }
+
+ async function pollTicketUpdates() {
+ if (!sessionId) {
+ return;
+ }
+ try {
+ const params = new URLSearchParams({
+ conversationId: sessionId,
+ sessionId,
+ limit: '120'
+ });
+ const response = await fetch(`/audit/conversation/session-data?${params.toString()}`);
+ if (!response.ok) {
+ return;
+ }
+ const payload = await response.json();
+ const events = Array.isArray(payload?.events) ? payload.events : [];
+ for (const event of events) {
+ if (String(event?.source || '') !== 'a2a.ticketing') {
+ continue;
+ }
+ const key = ticketUpdateKey(event);
+ if (displayedTicketUpdateKeys.has(key)) {
+ continue;
+ }
+ displayedTicketUpdateKeys.add(key);
+ const widget = normalizeWidgetCandidate(event?.payload?.widget) || normalizeWidgetCandidate(event?.payload);
+ tryAddVoiceMessage('agent', String(event?.text || 'Ticket service update'), 'a2a.ticketing', widget, key);
+ }
+ } catch (_) {
+ }
+ }
+
+ function startTicketUpdatePolling() {
+ clearTicketUpdatePolling();
+ displayedTicketUpdateKeys.clear();
+ ticketUpdatePollTimer = setInterval(() => {
+ pollTicketUpdates();
+ }, 2000);
+ pollTicketUpdates();
+ }
+
function parseSseEvents(payload) {
const blocks = payload.split(/\n\n+/).map((chunk) => chunk.trim()).filter(Boolean);
const events = [];
@@ -1838,6 +1901,7 @@
Ticket Created
const previousSessionId = sessionId;
sessionId = `session-${Date.now()}`;
+ startTicketUpdatePolling();
realtimeEventQueue = Promise.resolve();
setVoiceStatus('WebRTC token failed (400), reinitializing session...');
addMessage('agent', `WebRTC token 400 for ${previousSessionId}; retrying with fresh session ${sessionId}.`);
@@ -2939,6 +3003,7 @@
Ticket Created
.then(() => initializeRealtimeSessionContext(buildVoiceSessionPatch()))
.catch(() => {
});
+ startTicketUpdatePolling();
syncInstructionDebugVisibility();
openInstructionDebugForWebRtc();
diff --git a/samples/agent-support-service/src/main/resources/routes/admin-platform.camel.yaml b/samples/agent-support-service/src/main/resources/routes/admin-platform.camel.yaml
index c684349..b23e38f 100644
--- a/samples/agent-support-service/src/main/resources/routes/admin-platform.camel.yaml
+++ b/samples/agent-support-service/src/main/resources/routes/admin-platform.camel.yaml
@@ -283,6 +283,18 @@
id: processSampleAuditConversationAgentMessage
ref: auditConversationAgentMessageProcessor
+- route:
+ id: sample-audit-conversation-session-data-api
+ from:
+ id: fromSampleAuditConversationSessionData
+ uri: undertow
+ parameters:
+ httpURI: http://0.0.0.0:{{agui.rpc.port:8080}}/audit/conversation/session-data?httpMethodRestrict=GET
+ steps:
+ - process:
+ id: processSampleAuditConversationSessionData
+ ref: auditConversationSessionDataProcessor
+
- route:
id: sample-audit-agent-catalog-api
from:
diff --git a/samples/agent-support-service/src/main/resources/routes/kb-search-json.camel.xml b/samples/agent-support-service/src/main/resources/routes/kb-search-json.camel.xml
index 2441a8a..94aa852 100644
--- a/samples/agent-support-service/src/main/resources/routes/kb-search-json.camel.xml
+++ b/samples/agent-support-service/src/main/resources/routes/kb-search-json.camel.xml
@@ -2,8 +2,6 @@
-
- {"ticketId":"TCK-${exchangeId}","status":"OPEN","summary":"${body[query]}","assignedQueue":"L1-SUPPORT","message":"Support ticket created successfully"}
-
+
diff --git a/samples/agent-support-service/src/main/resources/routes/ticket-service.camel.yaml b/samples/agent-support-service/src/main/resources/routes/ticket-service.camel.yaml
new file mode 100644
index 0000000..978fbec
--- /dev/null
+++ b/samples/agent-support-service/src/main/resources/routes/ticket-service.camel.yaml
@@ -0,0 +1,9 @@
+- route:
+ id: support-ticket-manage-route
+ from:
+ id: fromSupportTicketManage
+ uri: direct:support-ticket-manage
+ steps:
+ - process:
+ id: processSupportTicketManage
+ ref: ticketLifecycleProcessor
diff --git a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/AgUiPlaywrightAuditTrailIntegrationTest.java b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/AgUiPlaywrightAuditTrailIntegrationTest.java
index dbeb73d..6278afd 100644
--- a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/AgUiPlaywrightAuditTrailIntegrationTest.java
+++ b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/AgUiPlaywrightAuditTrailIntegrationTest.java
@@ -1,10 +1,6 @@
package io.dscope.camel.agent.samples;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.microsoft.playwright.Browser;
-import com.microsoft.playwright.BrowserType;
-import com.microsoft.playwright.Page;
-import com.microsoft.playwright.Playwright;
import io.dscope.camel.agent.model.AiToolCall;
import io.dscope.camel.agent.runtime.AgentRuntimeBootstrap;
import io.dscope.camel.agent.springai.SpringAiChatGateway;
@@ -18,6 +14,7 @@
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
+import java.util.Map;
import org.apache.camel.main.Main;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -34,50 +31,43 @@ void shouldMatchCopilotPageInputOutputWithAuditTrail() throws Exception {
String queryToken = "pw-audit-" + System.currentTimeMillis();
String prompt = "Please open a support ticket for repeated login failures " + queryToken;
main.bind("springAiChatGateway", new DeterministicTicketGateway());
+ main.bind("ticketLifecycleProcessor", new SupportTicketLifecycleProcessor(new ObjectMapper()));
try {
AgentRuntimeBootstrap.bootstrap(main, "ag-ui-playwright-audit-test.yaml");
+ SampleAdminMcpBindings.bindIfMissing(main, "ag-ui-playwright-audit-test.yaml");
main.start();
- try (Playwright playwright = Playwright.create()) {
- Browser browser = playwright.chromium().launch(
- new BrowserType.LaunchOptions()
- .setHeadless(true)
- );
- try {
- Page page = browser.newPage();
- page.navigate("http://127.0.0.1:" + port + "/agui/ui");
-
- String conversationId = String.valueOf(page.evaluate("() => String(sessionId || '')"));
- Assertions.assertFalse(conversationId.isBlank(), "UI conversation/session id should be initialized");
-
- page.locator("#prompt").fill(prompt);
- page.locator("button[type='submit']").click();
-
- page.locator(".msg.agent").last().waitFor();
- String uiOutput = page.locator(".msg.agent").last().innerText();
-
- Assertions.assertTrue(
- uiOutput.contains("Support ticket created successfully"),
- "UI should render deterministic ticket response"
- );
-
- String conversationAudit = waitForConversationHit(port, conversationId, queryToken);
- Assertions.assertTrue(
- containsAnyIgnoreCase(conversationAudit, conversationId, queryToken),
- "Audit conversation listing should include input/query token"
- );
-
- String auditJson = waitForAudit(port, conversationId, "Support ticket created successfully");
- Assertions.assertTrue(auditJson.contains(conversationId), "Audit trail should include same conversation id");
- Assertions.assertTrue(
- containsAnyIgnoreCase(auditJson, "Support ticket created successfully", "support.ticket.open"),
- "Audit search should include assistant/tool output"
- );
- } finally {
- browser.close();
- }
- }
+ String conversationId = "agui-http-it-" + System.currentTimeMillis();
+ HttpResult uiPage = get(port, "/agui/ui");
+ Assertions.assertEquals(200, uiPage.statusCode());
+ Assertions.assertTrue(uiPage.body().contains("plan-name"), "UI should expose the plan selector");
+
+ ObjectMapper mapper = new ObjectMapper();
+ String requestBody = mapper.writeValueAsString(Map.of(
+ "threadId", conversationId,
+ "sessionId", conversationId,
+ "messages", List.of(Map.of("role", "user", "content", prompt))
+ ));
+ HttpResult aguiResponse = post(port, "/agui/agent", requestBody);
+ Assertions.assertEquals(200, aguiResponse.statusCode());
+ Assertions.assertTrue(
+ aguiResponse.body().contains("Support ticket created successfully"),
+ "AGUI response should include deterministic ticket output"
+ );
+
+ String conversationAudit = waitForConversationHit(port, conversationId, queryToken);
+ Assertions.assertTrue(
+ containsAnyIgnoreCase(conversationAudit, conversationId, queryToken),
+ "Audit conversation listing should include input/query token"
+ );
+
+ String auditJson = waitForAudit(port, conversationId, "Support ticket created successfully");
+ Assertions.assertTrue(auditJson.contains(conversationId), "Audit trail should include same conversation id");
+ Assertions.assertTrue(
+ containsAnyIgnoreCase(auditJson, "Support ticket created successfully", "support.ticket.manage"),
+ "Audit search should include assistant/tool output"
+ );
} finally {
try {
main.stop();
@@ -120,7 +110,7 @@ private static String waitForAudit(int port, String conversationId, String expec
);
if (result.statusCode() == 200) {
lastBody = result.body();
- if (containsAnyIgnoreCase(lastBody, expectedOutput, "support.ticket.open")) {
+ if (containsAnyIgnoreCase(lastBody, expectedOutput, "support.ticket.manage")) {
return lastBody;
}
}
@@ -150,6 +140,18 @@ private static HttpResult get(int port, String path) throws IOException, Interru
return new HttpResult(response.statusCode(), response.body() == null ? "" : response.body());
}
+ private static HttpResult post(int port, String path, String body) throws IOException, InterruptedException {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create("http://127.0.0.1:" + port + path))
+ .timeout(Duration.ofSeconds(10))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(body == null ? "" : body))
+ .build();
+
+ HttpResponse
response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+ return new HttpResult(response.statusCode(), response.body() == null ? "" : response.body());
+ }
+
private static String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
@@ -176,7 +178,7 @@ public SpringAiChatResult generate(String systemPrompt,
Integer maxTokens,
java.util.function.Consumer streamingTokenCallback) {
String query = userContext == null ? "" : userContext;
- AiToolCall call = new AiToolCall("support.ticket.open", mapper.createObjectNode().put("query", query));
+ AiToolCall call = new AiToolCall("support.ticket.manage", mapper.createObjectNode().put("query", query));
return new SpringAiChatResult("", List.of(call), true);
}
}
diff --git a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/McpAdminApiIntegrationTest.java b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/McpAdminApiIntegrationTest.java
index ce5283a..7e84ada 100644
--- a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/McpAdminApiIntegrationTest.java
+++ b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/McpAdminApiIntegrationTest.java
@@ -28,8 +28,10 @@ void shouldServeMcpAdminToolsAndExecuteAuditRoundtrip() throws Exception {
System.setProperty("agent.runtime.test-port", Integer.toString(port));
Main main = new Main();
+ main.bind("ticketLifecycleProcessor", new SupportTicketLifecycleProcessor(MAPPER));
try {
AgentRuntimeBootstrap.bootstrap(main, "ag-ui-playwright-audit-test.yaml");
+ SampleAdminMcpBindings.bindIfMissing(main, "ag-ui-playwright-audit-test.yaml");
main.start();
JsonNode initialize = mcpCall(port, "initialize", Map.of(
@@ -54,6 +56,7 @@ void shouldServeMcpAdminToolsAndExecuteAuditRoundtrip() throws Exception {
Assertions.assertTrue(catalogStructured.path("plans").isArray());
Assertions.assertTrue(containsPlan(catalogStructured.path("plans"), "support"));
Assertions.assertTrue(containsPlan(catalogStructured.path("plans"), "billing"));
+ Assertions.assertTrue(containsPlan(catalogStructured.path("plans"), "ticketing"));
Assertions.assertTrue(isDefaultPlan(catalogStructured.path("plans"), "support"));
Assertions.assertTrue(isDefaultVersion(catalogStructured.path("plans"), "support", "v1"));
diff --git a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/SpringAiAuditTrailIntegrationTest.java b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/SpringAiAuditTrailIntegrationTest.java
index 5f2e8da..8f4f3db 100644
--- a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/SpringAiAuditTrailIntegrationTest.java
+++ b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/SpringAiAuditTrailIntegrationTest.java
@@ -81,7 +81,7 @@ void shouldUseLlmDecisionAndCarryFirstTurnResultIntoSecondTurnContext() {
"Now please file a support ticket using that knowledge base result.");
Assertions.assertEquals("kb.search", toolName(first.events()));
- Assertions.assertEquals("support.ticket.open", toolName(second.events()));
+ Assertions.assertEquals("support.ticket.manage", toolName(second.events()));
Assertions.assertNotNull(gateway.secondTurnContext);
Assertions.assertTrue(gateway.secondTurnContext.contains("Knowledge base result for"),
"Second turn LLM evaluation should include first-turn KB tool result in context");
@@ -134,7 +134,7 @@ void shouldUseKnowledgeBaseBeforeTicketAndCarryContextAcrossTurns() {
.anyMatch(e -> "kb.search".equals(e.payload().path("name").asText())));
Assertions.assertTrue(auditTrail.stream()
.filter(e -> "tool.start".equals(e.type()))
- .anyMatch(e -> "support.ticket.open".equals(e.payload().path("name").asText())));
+ .anyMatch(e -> "support.ticket.manage".equals(e.payload().path("name").asText())));
Assertions.assertTrue(auditTrail.stream()
.filter(e -> "tool.start".equals(e.type()))
.anyMatch(e -> e.payload().path("arguments").path("query").asText("").contains("Knowledge base result for")));
@@ -178,7 +178,7 @@ void shouldNotInjectKnowledgeBaseContextWhenTicketIsFirstPrompt() {
List auditTrail = persistenceFacade.loadConversation(conversationId, 50);
Assertions.assertTrue(auditTrail.stream()
.filter(e -> "tool.start".equals(e.type()))
- .anyMatch(e -> "support.ticket.open".equals(e.payload().path("name").asText())));
+ .anyMatch(e -> "support.ticket.manage".equals(e.payload().path("name").asText())));
Assertions.assertFalse(auditTrail.stream()
.filter(e -> "tool.start".equals(e.type()))
.anyMatch(e -> e.payload().path("arguments").path("query").asText("").contains("Knowledge base result for")));
@@ -194,10 +194,11 @@ private ToolResult executeTool(String toolName, JsonNode arguments, ObjectMapper
ObjectNode data = mapper.createObjectNode().put("answer", "Knowledge base result for " + query);
return new ToolResult(data.path("answer").asText(), data, List.of());
}
- if ("support.ticket.open".equals(toolName)) {
+ if ("support.ticket.manage".equals(toolName)) {
ObjectNode data = mapper.createObjectNode()
.put("ticketId", "TCK-" + UUID.randomUUID())
.put("status", "OPEN")
+ .put("action", "OPEN")
.put("summary", query)
.put("assignedQueue", "L1-SUPPORT")
.put("message", "Support ticket created successfully");
@@ -271,7 +272,7 @@ private List selectTools(String userText, String userContext) {
String query = sawKnowledgeBaseInSecondTurn
? "Escalate with prior context: " + kbResult
: userText;
- return List.of(new AiToolCall("support.ticket.open", objectMapper.createObjectNode().put("query", query)));
+ return List.of(new AiToolCall("support.ticket.manage", objectMapper.createObjectNode().put("query", query)));
}
return List.of();
}
diff --git a/samples/agent-support-service/src/test/resources/ag-ui-playwright-audit-test.yaml b/samples/agent-support-service/src/test/resources/ag-ui-playwright-audit-test.yaml
index 35e8ca9..13c398a 100644
--- a/samples/agent-support-service/src/test/resources/ag-ui-playwright-audit-test.yaml
+++ b/samples/agent-support-service/src/test/resources/ag-ui-playwright-audit-test.yaml
@@ -11,8 +11,12 @@ agent:
audit:
granularity: debug
runtime:
- routes-include-pattern: classpath:routes/kb-search.camel.yaml,classpath:routes/kb-search-json.camel.xml,classpath:routes/ag-ui-platform.camel.yaml,classpath:routes/admin-platform.camel.yaml
+ routes-include-pattern: classpath:routes/kb-search.camel.yaml,classpath:routes/kb-search-json.camel.xml,classpath:routes/ticket-service.camel.yaml,classpath:routes/ag-ui-platform.camel.yaml,classpath:routes/admin-platform.camel.yaml
agent-routes-enabled: true
+ a2a:
+ enabled: true
+ public-base-url: http://127.0.0.1:{{agent.runtime.test-port:18081}}
+ exposed-agents-config: classpath:agents/a2a-exposed-agents.yaml
ai:
mode: spring-ai
agui:
@@ -22,7 +26,7 @@ agent:
agent-endpoint-uri: agent:support?plansConfig={{agent.agents-config}}&blueprint={{agent.blueprint}}
fallback-enabled: false
fallback:
- ticket-keywords: ticket,open,create,support,login
+ ticket-keywords: ticket,open,create,update,close,status,support,login
error-markers: api key is missing,openai api key,set -dopenai.api.key
realtime:
bind-relay: false