You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ac...@apache.org on 2026/07/24 07:12:03 UTC
(camel) branch main updated: CAMEL-24221: Add camel_runtime_ai_trace MCP tool
This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 3f8d7e84cef7 CAMEL-24221: Add camel_runtime_ai_trace MCP tool
3f8d7e84cef7 is described below
commit 3f8d7e84cef70cb7de7a41cca38820bc58ba800a
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Fri Jul 24 09:11:55 2026 +0200
CAMEL-24221: Add camel_runtime_ai_trace MCP tool
New MCP tool that traces AI-specific exchange flow in running Camel applications. Combines message history, top processor stats, and route structure to surface AI metrics: token usage, model IDs, guardrail outcomes, completion reasons, chunk counts, and per-component latency for Bedrock, LangChain4j, Docling, Textract, OpenAI, KServe, DJL, TensorFlow Serving, and HuggingFace components.
Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
.../dsl/jbang/core/commands/mcp/AiTraceTools.java | 327 +++++++++++++++++++++
.../jbang/core/commands/mcp/AiTraceToolsTest.java | 231 +++++++++++++++
2 files changed, 558 insertions(+)
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiTraceTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiTraceTools.java
new file mode 100644
index 000000000000..c4597cc8a22c
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiTraceTools.java
@@ -0,0 +1,327 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import io.quarkiverse.mcp.server.Tool;
+import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolContext;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolExecutionException;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.jboss.logging.Logger;
+
+/**
+ * MCP Tool for tracing AI-specific exchange flow in running Camel applications.
+ * <p>
+ * Combines data from the message history, top processor statistics, and route structure to surface AI-specific metrics:
+ * token usage, model IDs, guardrail outcomes, completion reasons, and per-component latency for AI components (Bedrock,
+ * LangChain4j, Docling, Textract, OpenAI, KServe, DJL, TensorFlow Serving, HuggingFace).
+ */
+@ApplicationScoped
+public class AiTraceTools {
+
+ private static final Logger LOG = Logger.getLogger(AiTraceTools.class);
+
+ private static final String NAME_OR_PID_DESC
+ = "Name or PID of the Camel process. Leave empty to auto-detect (works when exactly one Camel process is running)";
+
+ private static final Set<String> AI_COMPONENT_SCHEMES = Set.of(
+ "aws-bedrock", "aws-bedrock-agent", "aws-bedrock-agent-runtime",
+ "aws2-textract", "docling",
+ "langchain4j-chat", "langchain4j-embeddings", "langchain4j-embeddingstore",
+ "langchain4j-tools", "langchain4j-agent", "langchain4j-web-search",
+ "openai", "kserve", "tensorflow-serving", "djl",
+ "huggingface", "ai-tool");
+
+ private static final Set<String> AI_HEADER_PREFIXES = Set.of(
+ "CamelAwsBedrock", "CamelAwsTextract",
+ "CamelLangChain4j", "CamelLangchain4j",
+ "CamelDocling", "CamelOpenAI",
+ "CamelKServe", "CamelDjl", "CamelTensorFlowServing", "CamelHuggingFace");
+
+ @Inject
+ RuntimeService runtimeService;
+
+ @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false),
+ description = "Trace AI-specific exchange flow in a running Camel application. "
+ + "Shows token usage, model IDs, guardrail outcomes, completion reasons, "
+ + "streaming chunk counts, and per-component latency for AI components "
+ + "(Bedrock, LangChain4j, Docling, Textract, OpenAI, KServe, DJL, "
+ + "TensorFlow Serving, HuggingFace). "
+ + "Combines message history with processor statistics filtered to AI steps.")
+ public AiTraceResult camel_runtime_ai_trace(
+ @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid,
+ @ToolArg(description = "Route ID filter (exact match or * for all, default: *)") String routeFilter) {
+
+ RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid);
+ ToolContext ctx = new ToolContext();
+ ctx.selectProcess(p.pid());
+
+ String filter = routeFilter != null && !routeFilter.isBlank() ? routeFilter : "*";
+
+ try {
+ JsonObject history = executeRegistryTool("get_history", ctx, Map.of());
+ JsonObject topProcessors = executeRegistryTool("get_top_processors", ctx, Map.of());
+ JsonObject routeStructure
+ = executeRegistryTool("get_route_structure", ctx, Map.of("routeId", filter));
+
+ List<AiComponentInfo> aiComponents = extractAiComponents(routeStructure);
+ List<AiHeaderInfo> aiHeaders = extractAiHeaders(history);
+ List<AiProcessorStat> aiProcessorStats = extractAiProcessorStats(topProcessors);
+ AiTraceSummary summary = buildSummary(aiComponents, aiHeaders, aiProcessorStats);
+
+ return new AiTraceResult(
+ p.name(), p.pid(),
+ aiComponents.isEmpty() ? null : aiComponents,
+ aiHeaders.isEmpty() ? null : aiHeaders,
+ aiProcessorStats.isEmpty() ? null : aiProcessorStats,
+ summary);
+ } catch (ToolCallException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ToolCallException(
+ "Failed to collect AI trace data: " + e.getMessage(), null);
+ }
+ }
+
+ private JsonObject executeRegistryTool(String toolName, ToolContext ctx, Map<String, String> args) {
+ try {
+ Object result = ToolRegistry.execute(toolName, ctx, args);
+ return RuntimeTools.toJsonObject(result);
+ } catch (ToolExecutionException e) {
+ LOG.debugf("AI trace: %s query failed: %s", toolName, e.getMessage());
+ return new JsonObject();
+ }
+ }
+
+ List<AiComponentInfo> extractAiComponents(JsonObject routeStructure) {
+ List<AiComponentInfo> result = new ArrayList<>();
+ collectAiComponentsRecursive(routeStructure, result);
+ return result;
+ }
+
+ private void collectAiComponentsRecursive(Object node, List<AiComponentInfo> result) {
+ if (node instanceof JsonObject jo) {
+ String uri = jo.getString("uri");
+ if (uri != null) {
+ String scheme = extractScheme(uri);
+ if (scheme != null && AI_COMPONENT_SCHEMES.contains(scheme)) {
+ result.add(new AiComponentInfo(
+ scheme, uri,
+ jo.getString("routeId"),
+ jo.getString("nodeId")));
+ }
+ }
+ for (Object value : jo.values()) {
+ collectAiComponentsRecursive(value, result);
+ }
+ } else if (node instanceof JsonArray ja) {
+ for (Object item : ja) {
+ collectAiComponentsRecursive(item, result);
+ }
+ }
+ }
+
+ List<AiHeaderInfo> extractAiHeaders(JsonObject history) {
+ List<AiHeaderInfo> result = new ArrayList<>();
+ collectAiHeadersRecursive(history, result);
+ return result;
+ }
+
+ private void collectAiHeadersRecursive(Object node, List<AiHeaderInfo> result) {
+ if (node instanceof JsonObject jo) {
+ for (Map.Entry<String, Object> entry : jo.entrySet()) {
+ String key = entry.getKey();
+ if (isAiHeader(key) && entry.getValue() != null) {
+ result.add(new AiHeaderInfo(
+ key, truncateValue(entry.getValue().toString(), 200),
+ categorizeHeader(key)));
+ }
+ collectAiHeadersRecursive(entry.getValue(), result);
+ }
+ } else if (node instanceof JsonArray ja) {
+ for (Object item : ja) {
+ collectAiHeadersRecursive(item, result);
+ }
+ }
+ }
+
+ List<AiProcessorStat> extractAiProcessorStats(JsonObject topProcessors) {
+ List<AiProcessorStat> result = new ArrayList<>();
+ collectAiProcessorStatsRecursive(topProcessors, result);
+ return result;
+ }
+
+ private void collectAiProcessorStatsRecursive(Object node, List<AiProcessorStat> result) {
+ if (node instanceof JsonObject jo) {
+ String id = jo.getString("id");
+ String processorUri = jo.getString("uri");
+ if (processorUri != null) {
+ String scheme = extractScheme(processorUri);
+ if (scheme != null && AI_COMPONENT_SCHEMES.contains(scheme)) {
+ result.add(new AiProcessorStat(
+ id != null ? id : "unknown",
+ processorUri,
+ jo.getString("total"),
+ jo.getString("mean"),
+ jo.getString("max"),
+ jo.getString("min"),
+ jo.getString("last")));
+ }
+ }
+ for (Object value : jo.values()) {
+ collectAiProcessorStatsRecursive(value, result);
+ }
+ } else if (node instanceof JsonArray ja) {
+ for (Object item : ja) {
+ collectAiProcessorStatsRecursive(item, result);
+ }
+ }
+ }
+
+ private AiTraceSummary buildSummary(
+ List<AiComponentInfo> components,
+ List<AiHeaderInfo> headers,
+ List<AiProcessorStat> stats) {
+
+ String tokenUsage = headers.stream()
+ .filter(h -> "token_usage".equals(h.category()))
+ .map(AiHeaderInfo::value)
+ .findFirst().orElse(null);
+ String modelId = headers.stream()
+ .filter(h -> "model".equals(h.category()))
+ .map(AiHeaderInfo::value)
+ .findFirst().orElse(null);
+ String completionReason = headers.stream()
+ .filter(h -> "completion".equals(h.category()))
+ .map(AiHeaderInfo::value)
+ .findFirst().orElse(null);
+ String guardrailAction = headers.stream()
+ .filter(h -> "guardrail".equals(h.category()))
+ .map(AiHeaderInfo::value)
+ .findFirst().orElse(null);
+ String chunkCount = headers.stream()
+ .filter(h -> "streaming".equals(h.category()))
+ .map(AiHeaderInfo::value)
+ .findFirst().orElse(null);
+
+ return new AiTraceSummary(
+ components.size(), stats.size(),
+ tokenUsage, modelId, completionReason,
+ guardrailAction, chunkCount,
+ components.isEmpty() ? "No AI components detected in running routes" : null);
+ }
+
+ static String extractScheme(String uri) {
+ if (uri == null) {
+ return null;
+ }
+ int idx = uri.indexOf(':');
+ return idx > 0 ? uri.substring(0, idx) : null;
+ }
+
+ private boolean isAiHeader(String key) {
+ for (String prefix : AI_HEADER_PREFIXES) {
+ if (key.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private String categorizeHeader(String header) {
+ if (header.contains("TokenCount") || header.contains("Usage") || header.contains("TokenUsage")) {
+ return "token_usage";
+ }
+ if (header.contains("Model")) {
+ return "model";
+ }
+ if (header.contains("Guardrail")) {
+ return "guardrail";
+ }
+ if (header.contains("CompletionReason") || header.contains("StopReason")
+ || header.contains("FinishReason")) {
+ return "completion";
+ }
+ if (header.contains("ChunkCount") || header.contains("Stream")) {
+ return "streaming";
+ }
+ if (header.contains("Operation")) {
+ return "operation";
+ }
+ return "other";
+ }
+
+ private static String truncateValue(String s, int maxLen) {
+ return s.length() <= maxLen ? s : s.substring(0, maxLen) + "...";
+ }
+
+ // ---- Result records ----
+
+ record AiTraceResult(
+ String processName,
+ long pid,
+ List<AiComponentInfo> aiComponents,
+ List<AiHeaderInfo> aiHeaders,
+ List<AiProcessorStat> aiProcessorStats,
+ AiTraceSummary summary) {
+ }
+
+ record AiComponentInfo(
+ String scheme,
+ String uri,
+ String routeId,
+ String nodeId) {
+ }
+
+ record AiHeaderInfo(
+ String header,
+ String value,
+ String category) {
+ }
+
+ record AiProcessorStat(
+ String processorId,
+ String uri,
+ String totalExchanges,
+ String meanMs,
+ String maxMs,
+ String minMs,
+ String lastMs) {
+ }
+
+ record AiTraceSummary(
+ int aiComponentCount,
+ int aiProcessorStatCount,
+ String tokenUsage,
+ String modelId,
+ String completionReason,
+ String guardrailAction,
+ String chunkCount,
+ String note) {
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiTraceToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiTraceToolsTest.java
new file mode 100644
index 000000000000..fd4283a29c50
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiTraceToolsTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.List;
+
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AiTraceToolsTest {
+
+ private AiTraceTools tools;
+
+ @BeforeEach
+ void setUp() {
+ tools = new AiTraceTools();
+ }
+
+ @Test
+ void shouldExtractAiComponentsFromRouteStructure() {
+ JsonObject route = new JsonObject();
+ JsonArray steps = new JsonArray();
+
+ JsonObject bedrockStep = new JsonObject();
+ bedrockStep.put("uri", "aws-bedrock:label");
+ bedrockStep.put("routeId", "ai-route");
+ bedrockStep.put("nodeId", "to1");
+ steps.add(bedrockStep);
+
+ JsonObject directStep = new JsonObject();
+ directStep.put("uri", "direct:start");
+ steps.add(directStep);
+
+ JsonObject doclingStep = new JsonObject();
+ doclingStep.put("uri", "docling:convert");
+ doclingStep.put("routeId", "ai-route");
+ doclingStep.put("nodeId", "to2");
+ steps.add(doclingStep);
+
+ route.put("steps", steps);
+
+ List<AiTraceTools.AiComponentInfo> result = tools.extractAiComponents(route);
+
+ assertThat(result).hasSize(2);
+ assertThat(result.get(0).scheme()).isEqualTo("aws-bedrock");
+ assertThat(result.get(0).routeId()).isEqualTo("ai-route");
+ assertThat(result.get(1).scheme()).isEqualTo("docling");
+ }
+
+ @Test
+ void shouldIgnoreNonAiComponents() {
+ JsonObject route = new JsonObject();
+ route.put("uri", "kafka:my-topic");
+
+ List<AiTraceTools.AiComponentInfo> result = tools.extractAiComponents(route);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void shouldExtractAiHeaders() {
+ JsonObject history = new JsonObject();
+ JsonObject headers = new JsonObject();
+ headers.put("CamelAwsBedrockTokenCount", "1500");
+ headers.put("CamelAwsBedrockCompletionReason", "end_turn");
+ headers.put("CamelAwsBedrockChunkCount", "12");
+ headers.put("CamelAwsBedrockGuardrailOutput", "allowed");
+ headers.put("breadcrumbId", "abc-123");
+ history.put("headers", headers);
+
+ List<AiTraceTools.AiHeaderInfo> result = tools.extractAiHeaders(history);
+
+ assertThat(result).hasSize(4);
+ assertThat(result).extracting(AiTraceTools.AiHeaderInfo::header)
+ .containsExactlyInAnyOrder(
+ "CamelAwsBedrockTokenCount",
+ "CamelAwsBedrockCompletionReason",
+ "CamelAwsBedrockChunkCount",
+ "CamelAwsBedrockGuardrailOutput");
+ }
+
+ @Test
+ void shouldCategorizeHeadersCorrectly() {
+ JsonObject history = new JsonObject();
+ history.put("CamelAwsBedrockTokenCount", "500");
+ history.put("CamelAwsBedrockConverseUsage", "{input: 100, output: 400}");
+ history.put("CamelAwsBedrockCompletionReason", "end_turn");
+ history.put("CamelAwsBedrockChunkCount", "5");
+ history.put("CamelAwsBedrockGuardrailAssessments", "[]");
+ history.put("CamelAwsBedrockOperation", "converse");
+
+ List<AiTraceTools.AiHeaderInfo> result = tools.extractAiHeaders(history);
+
+ assertThat(result).anyMatch(h -> "token_usage".equals(h.category()));
+ assertThat(result).anyMatch(h -> "completion".equals(h.category()));
+ assertThat(result).anyMatch(h -> "streaming".equals(h.category()));
+ assertThat(result).anyMatch(h -> "guardrail".equals(h.category()));
+ assertThat(result).anyMatch(h -> "operation".equals(h.category()));
+ }
+
+ @Test
+ void shouldExtractAiProcessorStats() {
+ JsonObject top = new JsonObject();
+ JsonArray processors = new JsonArray();
+
+ JsonObject bedrockProc = new JsonObject();
+ bedrockProc.put("id", "to1");
+ bedrockProc.put("uri", "aws-bedrock:label");
+ bedrockProc.put("total", "42");
+ bedrockProc.put("mean", "1200");
+ bedrockProc.put("max", "3500");
+ bedrockProc.put("min", "450");
+ bedrockProc.put("last", "980");
+ processors.add(bedrockProc);
+
+ JsonObject kafkaProc = new JsonObject();
+ kafkaProc.put("id", "to2");
+ kafkaProc.put("uri", "kafka:output");
+ kafkaProc.put("total", "42");
+ processors.add(kafkaProc);
+
+ top.put("processors", processors);
+
+ List<AiTraceTools.AiProcessorStat> result = tools.extractAiProcessorStats(top);
+
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).uri()).isEqualTo("aws-bedrock:label");
+ assertThat(result.get(0).totalExchanges()).isEqualTo("42");
+ assertThat(result.get(0).meanMs()).isEqualTo("1200");
+ }
+
+ @Test
+ void shouldHandleEmptyInputGracefully() {
+ assertThat(tools.extractAiComponents(new JsonObject())).isEmpty();
+ assertThat(tools.extractAiHeaders(new JsonObject())).isEmpty();
+ assertThat(tools.extractAiProcessorStats(new JsonObject())).isEmpty();
+ }
+
+ @Test
+ void shouldExtractSchemeCorrectly() {
+ assertThat(AiTraceTools.extractScheme("aws-bedrock:label")).isEqualTo("aws-bedrock");
+ assertThat(AiTraceTools.extractScheme("docling:convert")).isEqualTo("docling");
+ assertThat(AiTraceTools.extractScheme("langchain4j-chat:model")).isEqualTo("langchain4j-chat");
+ assertThat(AiTraceTools.extractScheme(null)).isNull();
+ assertThat(AiTraceTools.extractScheme("noscheme")).isNull();
+ }
+
+ @Test
+ void shouldDetectLangChain4jComponents() {
+ JsonObject route = new JsonObject();
+ route.put("uri", "langchain4j-chat:myModel");
+
+ List<AiTraceTools.AiComponentInfo> result = tools.extractAiComponents(route);
+
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).scheme()).isEqualTo("langchain4j-chat");
+ }
+
+ @Test
+ void shouldDetectTextractComponents() {
+ JsonObject route = new JsonObject();
+ route.put("uri", "aws2-textract:detect");
+
+ List<AiTraceTools.AiComponentInfo> result = tools.extractAiComponents(route);
+
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).scheme()).isEqualTo("aws2-textract");
+ }
+
+ @Test
+ void shouldExtractOpenAiHeadersWithCorrectCasing() {
+ JsonObject history = new JsonObject();
+ history.put("CamelOpenAIModel", "gpt-4o");
+ history.put("CamelOpenAIFinishReason", "stop");
+ history.put("breadcrumbId", "abc-123");
+
+ List<AiTraceTools.AiHeaderInfo> result = tools.extractAiHeaders(history);
+
+ assertThat(result).hasSize(2);
+ assertThat(result).extracting(AiTraceTools.AiHeaderInfo::header)
+ .containsExactlyInAnyOrder("CamelOpenAIModel", "CamelOpenAIFinishReason");
+ }
+
+ @Test
+ void shouldDetectWebSearchAndEmbeddingStoreComponents() {
+ JsonObject route = new JsonObject();
+ JsonArray steps = new JsonArray();
+ JsonObject ws = new JsonObject();
+ ws.put("uri", "langchain4j-web-search:search");
+ steps.add(ws);
+ JsonObject es = new JsonObject();
+ es.put("uri", "langchain4j-embeddingstore:store");
+ steps.add(es);
+ route.put("steps", steps);
+
+ List<AiTraceTools.AiComponentInfo> result = tools.extractAiComponents(route);
+
+ assertThat(result).hasSize(2);
+ assertThat(result).extracting(AiTraceTools.AiComponentInfo::scheme)
+ .containsExactlyInAnyOrder("langchain4j-web-search", "langchain4j-embeddingstore");
+ }
+
+ @Test
+ void shouldExtractTextractHeaders() {
+ JsonObject history = new JsonObject();
+ history.put("CamelAwsTextractJobId", "job-123");
+
+ List<AiTraceTools.AiHeaderInfo> result = tools.extractAiHeaders(history);
+
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).header()).isEqualTo("CamelAwsTextractJobId");
+ }
+}