You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ld...@apache.org on 2026/10/08 09:06:25 UTC
(camel) branch main updated (459f273a62fd -> 7c0fca7a09cb)
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
from 459f273a62fd CAMEL-25356: camel-cm-sms - recognize <, >, the currency sign and form feed as GSM 03.38 characters (#27531)
new d3cf0051628a CAMEL-25382: Introduce expert-owned semantic evaluation contracts
new 2ebad42055d0 CAMEL-25382: Address semantic contract review feedback
new efe1577aaa61 CAMEL-25382: Preserve literal braces in semantic documentation
new 6a5494908279 CAMEL-25382: Reduce semantic registry and declaration overhead
new 6fd800813c38 CAMEL-25382: Fix JBang validation of semantic documentation
new 03d215da95cf CAMEL-25382: Address semantic validation review nits
new 921bea1b1420 CAMEL-25382: Isolate semantic replacement validation
new 7c0fca7a09cb CAMEL-25382: Validate predicates registered during replacement
The 8 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../camel/catalog/docs/semantic-language.adoc | 449 +++++++++----
.../camel/catalog/docs/simple-functions.adoc | 8 +
.../org/apache/camel/catalog/docs/switch-eip.adoc | 2 +-
.../camel/catalog/docs/typesafe-ai-component.adoc | 8 +-
.../apache/camel/catalog/languages/semantic.json | 2 +-
.../org/apache/camel/catalog/languages/simple.json | 141 ++---
.../camel/catalog/schemas/camelYamlDsl-model.json | 82 +--
.../camel/catalog/simple/camel-simple-validator.js | 1 +
components/camel-ai/camel-semantic/pom.xml | 2 +-
.../apache/camel/language/semantic/semantic.json | 2 +-
.../services/org/apache/camel/language.properties | 2 +-
.../src/main/docs/semantic-language.adoc | 449 +++++++++----
.../camel/language/semantic/SemanticLanguage.java | 246 ++++++--
.../org/apache/camel/semantic/SemanticAdapter.java | 28 +-
.../camel/semantic/SemanticCapabilities.java | 508 ++++++++-------
.../apache/camel/semantic/SemanticEvaluation.java | 107 ++++
.../camel/semantic/SemanticEvaluationBuilder.java | 184 ++++++
.../apache/camel/semantic/SemanticEvaluations.java | 194 ++++++
...uilder.java => SemanticEvaluationsBuilder.java} | 74 ++-
.../org/apache/camel/semantic/SemanticExpert.java | 38 +-
.../apache/camel/semantic/SemanticOperation.java | 74 +++
.../apache/camel/semantic/SemanticParameter.java | 58 ++
.../apache/camel/semantic/SemanticQuestion.java | 133 ----
.../camel/semantic/SemanticQuestionBuilder.java | 161 -----
.../apache/camel/semantic/SemanticQuestions.java | 148 -----
.../camel/semantic/SemanticReloadPlugin.java | 15 +-
.../org/apache/camel/semantic/SemanticResult.java | 51 +-
.../apache/camel/semantic/SemanticXmlLoader.java | 10 +-
.../semantic/SemanticXmlRoutesBuilderLoader.java | 131 +++-
.../yaml/SemanticDefinitionDeserializer.java | 226 ++++---
.../apache/camel/semantic/FixedSemanticExpert.java | 84 +++
.../apache/camel/semantic/SemanticBatchTest.java | 149 ++---
.../camel/semantic/SemanticCapabilitiesTest.java | 207 +++---
.../camel/semantic/SemanticContractTest.java | 700 +++++++++++++++++++++
.../camel/semantic/SemanticDeclarationDslTest.java | 280 +++++----
.../org/apache/camel/semantic/SemanticEipTest.java | 38 +-
.../semantic/SemanticEvaluationBuilderTest.java | 144 +++++
.../apache/camel/semantic/SemanticExpertTest.java | 222 +++----
.../camel/semantic/SemanticExpertYamlTest.java | 69 +-
.../camel/semantic/SemanticInitializationTest.java | 67 +-
.../camel/semantic/SemanticLanguageTest.java | 138 ++--
.../camel/semantic/SemanticPropertiesTest.java | 12 +-
.../apache/camel/semantic/SemanticResultTest.java | 49 +-
.../semantic/SemanticRetryAndValidationTest.java | 24 +-
.../semantic/SemanticXmlAutoDiscoveryTest.java | 39 +-
.../camel/semantic/SemanticXmlLoaderTest.java | 10 +-
.../apache/camel/semantic/TestSemanticAdapter.java | 110 ++++
.../src/main/docs/typesafe-ai-component.adoc | 8 +-
.../typesafeai/TypeSafeAiSemanticAdapter.java | 180 +++++-
.../typesafeai/TypeSafeAiApiPathTest.java | 13 +-
.../typesafeai/TypeSafeAiSemanticAdapterTest.java | 202 ++++--
.../main/docs/modules/eips/pages/switch-eip.adoc | 2 +-
.../org/apache/camel/language/simple/simple.json | 141 ++---
.../modules/languages/pages/simple-functions.adoc | 8 +
.../camel/language/simple/SimpleConstants.java | 6 +
...uoteEnd.java => SimplePredicateExpression.java} | 20 +-
.../camel/language/simple/SimpleSyntaxHints.java | 9 +-
.../simple/ast/SimpleFunctionExpression.java | 9 +-
.../camel/language/simple/ast/UnaryExpression.java | 11 +-
.../functions/QueryLanguageFunctionFactory.java | 49 ++
.../language/simple/SimpleNotOperatorTest.java | 47 ++
.../language/simple/SimpleSyntaxHintsTest.java | 8 +
.../java/org/apache/camel/util/SimpleUtils.java | 1 +
.../dsl/jbang/core/commands/ai/eip-samples.json | 2 +-
.../dsl/jbang/core/commands/RouteDslConverter.java | 2 +-
.../dsl/jbang/core/commands/ai/SimpleChecks.java | 1 +
.../jbang/core/commands/RouteDslConverterTest.java | 4 +-
.../core/commands/ai/CatalogDocExcerptTest.java | 4 +-
.../commands/ai/SourceValidatorSimpleTest.java | 57 ++
.../core/commands/mcp/TransformToolsTest.java | 8 +-
.../dsl/yaml/validator/SemanticSchemaTest.java | 90 +--
.../resources/schema/camelYamlDsl-canonical.json | 95 ++-
.../resources/schema/camelYamlDsl-model.json | 82 +--
.../generated/resources/schema/camelYamlDsl.json | 95 ++-
.../camel/dsl/yaml/YamlRoutesBuilderLoader.java | 12 +-
...estionTest.java => SemanticEvaluationTest.java} | 322 +++++++---
76 files changed, 4920 insertions(+), 2444 deletions(-)
create mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java
create mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java
create mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
rename components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/{SemanticQuestionsBuilder.java => SemanticEvaluationsBuilder.java} (59%)
create mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticOperation.java
create mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticParameter.java
delete mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java
delete mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java
delete mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java
create mode 100644 components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/FixedSemanticExpert.java
create mode 100644 components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
create mode 100644 components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEvaluationBuilderTest.java
create mode 100644 components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/TestSemanticAdapter.java
copy core/camel-core-languages/src/main/java/org/apache/camel/language/simple/{ast/DoubleQuoteEnd.java => SimplePredicateExpression.java} (65%)
rename dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/{SemanticQuestionTest.java => SemanticEvaluationTest.java} (50%)
(camel) 03/08: CAMEL-25382: Preserve literal braces in semantic documentation
Posted by ld...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit efe1577aaa616d0a7c155bac846cb31889ee9120
Author: Luigi De Masi <ld...@redhat.com>
AuthorDate: Wed Oct 7 16:31:32 2026 +0200
CAMEL-25382: Preserve literal braces in semantic documentation
Protect inline placeholder-escaping examples from AsciiDoc substitutions.
This fixes the missing attribute warning in PR documentation validation
and preserves the closing escape backslash shown in the YAML example.
Regenerate the catalog documentation mirror.
Co-authored-by: Codex <no...@openai.com>
Signed-off-by: Luigi De Masi <ld...@redhat.com>
---
.../resources/org/apache/camel/catalog/docs/semantic-language.adoc | 4 ++--
.../camel-ai/camel-semantic/src/main/docs/semantic-language.adoc | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
index 5083db4bc638..699f055934d8 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
@@ -127,8 +127,8 @@ For example, a nested expert policy can externalise its cutoff without convertin
----
String parameters, including prompts, use Camel property placeholder escaping for literal braces.
-For example, YAML `instructions: 'Explain \{{example\}}'` gives the expert `Explain {{example}}`.
-In Java, use `.parameter("instructions", "Explain \\{{example\\}}")` for the same literal text.
+For example, YAML `+instructions: 'Explain \{{example\}}'+` gives the expert `+Explain {{example}}+`.
+In Java, use `+.parameter("instructions", "Explain \\{{example\\}}")+` for the same literal text.
Camel validates recognised parameter names, required values, types and declared constraints.
The expert checks operation-specific combinations and compatibility with its bean configuration.
diff --git a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
index 5083db4bc638..699f055934d8 100644
--- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
+++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
@@ -127,8 +127,8 @@ For example, a nested expert policy can externalise its cutoff without convertin
----
String parameters, including prompts, use Camel property placeholder escaping for literal braces.
-For example, YAML `instructions: 'Explain \{{example\}}'` gives the expert `Explain {{example}}`.
-In Java, use `.parameter("instructions", "Explain \\{{example\\}}")` for the same literal text.
+For example, YAML `+instructions: 'Explain \{{example\}}'+` gives the expert `+Explain {{example}}+`.
+In Java, use `+.parameter("instructions", "Explain \\{{example\\}}")+` for the same literal text.
Camel validates recognised parameter names, required values, types and declared constraints.
The expert checks operation-specific combinations and compatibility with its bean configuration.
(camel) 02/08: CAMEL-25382: Address semantic contract review feedback
Posted by ld...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 2ebad42055d06d12dc4257a6fd75da4445ed341a
Author: Luigi De Masi <ld...@redhat.com>
AuthorDate: Wed Oct 7 16:05:23 2026 +0200
CAMEL-25382: Address semantic contract review feedback
Use evaluation consistently across the preview API and declaration DSLs.
Keep the generic immutable declaration free of TypeSafe-specific enums,
constructors and policy getters, and migrate provider access and fixtures.
Preserve existing Simple value-based negation with an explicit opt-in for
semantic predicate semantics. Move predicate adaptation out of evaluation
and activate managed experts only when compiling a declaration.
Resolve typed numeric parameters after placeholders, document literal
prompt escaping, and retain YAML resource context on pre-parse failures.
Update DSL tooling, schemas, documentation and generated catalog copies.
Co-authored-by: Codex <no...@openai.com>
Signed-off-by: Luigi De Masi <ld...@redhat.com>
---
.../camel/catalog/docs/semantic-language.adoc | 148 +++++++-----
.../org/apache/camel/catalog/docs/switch-eip.adoc | 2 +-
.../camel/catalog/docs/typesafe-ai-component.adoc | 2 +-
.../apache/camel/catalog/languages/semantic.json | 2 +-
.../camel/catalog/schemas/camelYamlDsl-model.json | 42 ----
components/camel-ai/camel-semantic/pom.xml | 2 +-
.../apache/camel/language/semantic/semantic.json | 2 +-
.../services/org/apache/camel/language.properties | 2 +-
.../src/main/docs/semantic-language.adoc | 148 +++++++-----
.../camel/language/semantic/SemanticLanguage.java | 105 ++++----
.../org/apache/camel/semantic/SemanticAdapter.java | 17 +-
.../camel/semantic/SemanticCapabilities.java | 2 +-
...manticQuestion.java => SemanticEvaluation.java} | 93 +------
...Builder.java => SemanticEvaluationBuilder.java} | 42 ++--
...nticQuestions.java => SemanticEvaluations.java} | 92 +++----
...uilder.java => SemanticEvaluationsBuilder.java} | 62 +++--
.../camel/semantic/SemanticReloadPlugin.java | 13 +-
.../apache/camel/semantic/SemanticXmlLoader.java | 10 +-
.../semantic/SemanticXmlRoutesBuilderLoader.java | 64 ++---
.../yaml/SemanticDefinitionDeserializer.java | 102 +++-----
.../apache/camel/semantic/FixedSemanticExpert.java | 14 +-
.../apache/camel/semantic/SemanticBatchTest.java | 146 +++++------
.../camel/semantic/SemanticCapabilitiesTest.java | 9 +-
.../camel/semantic/SemanticContractTest.java | 99 ++++----
.../camel/semantic/SemanticDeclarationDslTest.java | 269 +++++++++++----------
.../org/apache/camel/semantic/SemanticEipTest.java | 36 +--
...est.java => SemanticEvaluationBuilderTest.java} | 36 ++-
.../apache/camel/semantic/SemanticExpertTest.java | 122 +++++-----
.../camel/semantic/SemanticExpertYamlTest.java | 22 +-
.../camel/semantic/SemanticInitializationTest.java | 22 +-
.../camel/semantic/SemanticLanguageTest.java | 128 ++++++----
.../camel/semantic/SemanticPropertiesTest.java | 12 +-
.../semantic/SemanticRetryAndValidationTest.java | 20 +-
.../semantic/SemanticXmlAutoDiscoveryTest.java | 36 +--
.../camel/semantic/SemanticXmlLoaderTest.java | 7 +-
.../apache/camel/semantic/TestSemanticAdapter.java | 14 +-
.../src/main/docs/typesafe-ai-component.adoc | 2 +-
.../typesafeai/TypeSafeAiSemanticAdapter.java | 78 +++---
.../typesafeai/TypeSafeAiApiPathTest.java | 13 +-
.../typesafeai/TypeSafeAiSemanticAdapterTest.java | 98 ++++----
.../main/docs/modules/eips/pages/switch-eip.adoc | 2 +-
.../language/simple/SimplePredicateExpression.java | 32 +++
.../simple/ast/SimpleFunctionExpression.java | 9 +-
.../camel/language/simple/ast/UnaryExpression.java | 9 +-
.../functions/QueryLanguageFunctionFactory.java | 24 +-
.../language/simple/SimpleNotOperatorTest.java | 47 ++++
.../dsl/jbang/core/commands/ai/eip-samples.json | 2 +-
.../dsl/jbang/core/commands/RouteDslConverter.java | 2 +-
.../jbang/core/commands/RouteDslConverterTest.java | 4 +-
.../core/commands/mcp/TransformToolsTest.java | 8 +-
.../dsl/yaml/validator/SemanticSchemaTest.java | 83 ++++---
.../resources/schema/camelYamlDsl-canonical.json | 41 ----
.../resources/schema/camelYamlDsl-model.json | 42 ----
.../generated/resources/schema/camelYamlDsl.json | 41 ----
.../camel/dsl/yaml/YamlRoutesBuilderLoader.java | 10 +-
...estionTest.java => SemanticEvaluationTest.java} | 140 ++++++-----
56 files changed, 1311 insertions(+), 1320 deletions(-)
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
index 0598a8714749..5083db4bc638 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
@@ -2,7 +2,7 @@
:doctitle: Semantic Evaluation
:shortname: semantic
:artifactid: camel-semantic
-:description: Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters
+:description: Invoke named evaluations of message content to produce boolean decisions, categories, scores and label sets through provider adapters
:since: 4.23
:supportlevel: Preview
:tabs-sync-option:
@@ -24,7 +24,7 @@ concurrency limits and transport resources. TypeSafe AI uses
`camel.component.typesafe-ai.*` settings even when a route contains no TypeSafe AI endpoint.
An *expert* is a configured semantic evaluator, implemented by `SemanticAdapter`. It is not
-an autonomous agent. Each named question can select a registry bean using `expert`.
+an autonomous agent. Each named evaluation can select a registry bean using `expert`.
Without that field, `camel.language.semantic.default-expert=myExpert` selects the default bean.
The existing `camel.language.semantic.adapter=myAdapter` setting remains supported as the
default when `default-expert` is absent; it also accepts a fully qualified implementation class name.
@@ -35,7 +35,7 @@ implementation class, including advertised superclasses of registered subclasses
configured instances remain distinct candidates.
No eligible expert, or multiple candidates, is an initialization error listing the candidates.
Unknown explicit names and invalid defaults fail without fallback. Selection never uses the
-question's result type: two boolean experts can evaluate entirely different properties.
+evaluation's result type: two boolean experts can evaluate entirely different properties.
Use plain names without `#bean:` or `#class:` prefixes. Registry lookup takes precedence over
class resolution. Class selection uses Camel's class resolver and injector.
Created adapters are registered as `camelSemanticAdapter` and managed by the context.
@@ -99,19 +99,42 @@ Declaring an evaluation does not execute it.
`parameters` can contain strings, booleans, numbers, nested maps and lists. Camel copies them
into immutable declarations. Java, YAML and XML builders resolve property placeholders in string
parameter values, including nested list and map values; map keys stay literal. Resolved strings
-remain strings: generic numeric parameters require numbers, while the `threshold` and `uncertainty`
-shorthand options also accept numeric placeholders. Direct `SemanticQuestion` construction takes
+remain strings. For numeric parameters, including values inside nested maps or lists, use an
+explicit numeric value after placeholder resolution. YAML accepts `!number "{{limit:0.8}}"`,
+XML accepts `<number>{{limit:0.8}}</number>`, and Java accepts
+`Double.valueOf(getContext().resolvePropertyPlaceholders("{{limit:0.8}}"))` as a parameter value.
+These forms work for any parameter name and expert. The `threshold` and `uncertainty` shorthand
+options also accept numeric placeholders. Direct `SemanticEvaluation` construction takes
already resolved parameter values and requires no Camel context. Omitted parameters stay absent: their behaviour belongs to the
expert, and Camel does not insert threshold or confidence defaults. Parameter contracts use
`String`, `Boolean`, `Number`, `Map` and `List` data shapes. Use `Number` with `integer = true`
for whole-number parameters; do not use JVM numeric wrapper classes such as `Integer` in a
contract. A provider consumes numeric values through `Number` across all DSLs.
+For example, a nested expert policy can externalise its cutoff without converting string labels:
+
+[source,yaml]
+----
+- semantic:
+ expert: moderation
+ evaluation:
+ contentPolicy:
+ operation: categories
+ parameters:
+ policy:
+ cutoff: !number "{{content.cutoff:0.8}}"
+ label: "{{content.label:privacy}}"
+----
+
+String parameters, including prompts, use Camel property placeholder escaping for literal braces.
+For example, YAML `instructions: 'Explain \{{example\}}'` gives the expert `Explain {{example}}`.
+In Java, use `.parameter("instructions", "Explain \\{{example\\}}")` for the same literal text.
+
Camel validates recognised parameter names, required values, types and declared constraints.
The expert checks operation-specific combinations and compatibility with its bean configuration.
Startup validates all declarations, including unused evaluations, without inference. Route
resource replacements also validate before publication, retaining previous declarations on failure.
-The low-level `SemanticQuestions.replace(String, Map)` API also validates new and changed
+The low-level `SemanticEvaluations.replace(String, Map)` API also validates new and changed
unused declarations before publishing them on a started context.
=== Calling an evaluation
@@ -191,9 +214,9 @@ Java uses the same declaration model and validation path:
[source,java]
----
-import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
-semanticQuestions(this).expert("moderation").state("${body}")
+semanticEvaluations(this).expert("moderation").state("${body}")
.evaluation("responseSafety").operation("response_safety").end()
.evaluation("safetyCategories").operation("categories")
.parameter("policy", Map.of("labels", List.of("privacy", "unsafe"), "threshold", 0.8))
@@ -224,19 +247,20 @@ parameter to request the expert's omission behaviour.
== Instruction-driven shorthand
-The `question` syntax and Java convenience methods below target instruction-driven operations
+The shorthand fields and Java convenience methods below target instruction-driven operations
named `boolean`, `choice` and `score`. Their fields become expert parameters and are validated by
the selected expert. TypeSafe AI implements these operations and requires nonblank instructions.
-Specialised operations normally use `evaluation` and `parameters` as shown above.
+All operations use the same `evaluation` declaration syntax; specialised operations supply their
+expert-owned `parameters` as shown above.
-== Named questions
+== Named evaluations
The xref:others:yaml-dsl.adoc[YAML DSL] supports declarations alongside routes, including declarations after their use:
[source,yaml]
----
- semantic:
- question:
+ evaluation:
department:
type: choice
instructions: Which department should handle this message?
@@ -287,7 +311,7 @@ The xref:others:yaml-dsl.adoc[YAML DSL] supports declarations alongside routes,
----
Names are context-wide. Duplicate declarations across resources and unknown references fail.
-Reloading a resource replaces its complete set of questions, including removing declarations
+Reloading a resource replaces its complete set of evaluations, including removing declarations
no longer present. Development-mode route reload also removes definitions from deleted or renamed files
before parsing replacements. This replacement does not make the surrounding route reload transactional.
Existing expressions resolve the current definition on their next evaluation.
@@ -302,10 +326,10 @@ alive. Constraints can remain until an unused expression is garbage-collected; s
does not necessarily release an expression that is still referenced elsewhere.
Loading declarations does not perform inference or start provider resources for validation.
-For questions using `expert` or `default-expert`, the registry bean is resolved when the
+For evaluations using `expert` or `default-expert`, the registry bean is resolved when the
expression is compiled. Rebinding that name does not update already compiled expressions.
After replacing the bean, recreate the consuming routes or expressions, or reload their
-question declarations so the expressions compile against the replacement bean.
+evaluation declarations so the expressions compile against the replacement bean.
=== YAML reload and expert bean preparation
@@ -321,12 +345,12 @@ Use the fluent helper from `camel-semantic` inside an ordinary `RouteBuilder`:
[source,java]
----
-import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
@Override
public void configure() {
- semanticQuestions(this)
- .question("department")
+ semanticEvaluations(this)
+ .evaluation("department")
.type("choice")
.state("${header.myState}")
.instructions("Which department should handle this message?")
@@ -341,21 +365,21 @@ public void configure() {
}
----
-Call `.end().question("anotherName")` to add another question to the group, then `.register()`
+Call `.end().evaluation("anotherName")` to add another evaluation to the group, then `.register()`
once to validate and install the entire group atomically. Register before creating expressions
that refer to these names. When adding builders to a running context, add the builder declaring
-shared questions before builders whose routes reference them.
+shared evaluations before builders whose routes reference them.
Use `.type("boolean")` with optional `.threshold(0.8)`, `.uncertainty(0.1)` and
-`.uncertaintyPolicy("non-match")` for boolean questions. Use `.type("score")` and successive
+`.uncertaintyPolicy("non-match")` for boolean evaluations. Use `.type("score")` and successive
`.level("description")` calls for ordered score levels. Applications can also register immutable
-`SemanticQuestion` definitions using `SemanticQuestions.get(context).replace(source, questions)`.
+`SemanticEvaluation` definitions using `SemanticEvaluations.get(context).replace(source, evaluations)`.
-Each Java resource owns one group. Register all of its questions together; registering again
+Each Java resource owns one group. Register all of its evaluations together; registering again
replaces that resource's previous group. Reloading a Java resource without the helper removes
its declarations. Embedded builders without a resource receive distinct generated source keys.
The helper reserves `"java:" + resource.getLocation()` as the resource source key; passing that
-key and an empty map to `replace` removes the resource's questions.
+key and an empty map to `replace` removes the resource's evaluations.
=== XML declarations
@@ -375,12 +399,12 @@ Both layouts are supported. To keep declarations alongside routes, use `tickets.
----
<routes>
<semantic>
- <question name="department" type="choice" state="${header.myState}">
+ <evaluation name="department" type="choice" state="${header.myState}">
<instructions>Which department should handle this message?</instructions>
<criterion key="billing" value="Invoices, payments, and refunds"/>
<criterion key="technical" value="Bugs, outages, and technical problems"/>
<criterion key="other" value="Everything else"/>
- </question>
+ </evaluation>
</semantic>
<route id="classify-ticket">
<from uri="direct:tickets"/>
@@ -392,25 +416,25 @@ Both layouts are supported. To keep declarations alongside routes, use `tickets.
</routes>
----
-To share declarations across route files, use a standalone `questions.xml`:
+To share declarations across route files, use a standalone `evaluations.xml`:
[source,xml]
----
<semantic>
- <question name="department" type="choice" state="${header.myState}">
+ <evaluation name="department" type="choice" state="${header.myState}">
<instructions>Which department should handle this message?</instructions>
<criterion key="billing" value="Invoices, payments, and refunds"/>
<criterion key="technical" value="Bugs, outages, and technical problems"/>
<criterion key="other" value="Everything else"/>
- </question>
+ </evaluation>
</semantic>
----
Load this together with ordinary `*.xml`, Java or YAML route resources that use `ref:department`.
For example, with Camel Main, set
-`camel.main.routes-include-pattern=classpath:questions.xml,classpath:routes.xml`.
+`camel.main.routes-include-pattern=classpath:evaluations.xml,classpath:routes.xml`.
Declarations are registered before consuming routes are configured. Reload replaces the source's
-questions; removing the `semantic` block, using an empty block, or deleting the resource removes
+evaluations; removing the `semantic` block, using an empty block, or deleting the resource removes
obsolete definitions. XML source keys are the resource locations.
The combined format supports a `routes` root with one optional `semantic` block and ordinary
@@ -418,15 +442,15 @@ The combined format supports a `routes` root with one optional `semantic` block
supported, as are documents consistently using `http://camel.apache.org/schema/semantic`,
`http://camel.apache.org/schema/xml-io`, or `http://camel.apache.org/schema/spring`.
A `semantic` block can also declare `xmlns="http://camel.apache.org/schema/semantic"` inside a
-standard `routes` document; its question elements inherit that namespace. The extension detects
+standard `routes` document; its evaluation elements inherit that namespace. The extension detects
the declaration block or semantic root namespace automatically.
These extensions do not validate against Camel's standard core XSDs. Automatic discovery applies
to Camel's route resource loader; it does not extend Spring's XML application-context parser
or direct JAXB unmarshalling.
-For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question
-attributes. For score questions, replace the named `criterion` elements with ordered `level`
+For boolean evaluations, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional evaluation
+attributes. For score evaluations, replace the named `criterion` elements with ordered `level`
elements, such as `<level>Routine</level><level>Urgent</level><level>Critical</level>`.
The numeric `threshold` and `uncertainty` options accept property placeholders in all three
@@ -435,12 +459,12 @@ DSLs. For example, Java accepts `.threshold("{{semantic.threshold:0.5}}")`, and
are registered.
Java and XML declarations use the same context-wide registry, validation, defaults and adapters
-as YAML. They do not require `camel-yaml-dsl`. Their questions can also be selected together
+as YAML. They do not require `camel-yaml-dsl`. Their evaluations can also be selected together
using `refs:name1,name2`, as described below.
=== Exporting routes
-Question declarations live outside Camel's core route model. Generic model exports and runtime
+Evaluation declarations live outside Camel's core route model. Generic model exports and runtime
route dumps such as `camel.main.dumpRoutes=yaml` contain only routes; keep declarations separately
and load them before reloading an exported route.
@@ -451,7 +475,7 @@ using the generic converter.
=== Selected state
-A question's optional `state` Simple expression overrides
+A evaluation's optional `state` Simple expression overrides
`camel.language.semantic.default-state`, whose default is `$\{body}`. Selectors are compiled
before evaluation; selected strings, maps and lists are passed as data and are never evaluated recursively.
A missing selected header fails instead of falling back to the body. A selector must be nonblank
@@ -470,7 +494,7 @@ in semantic evaluation, and Java applications using `camel-semantic` do not pull
== Results and policy
-Only boolean questions can be predicates. A category string is never implicitly a boolean.
+Only boolean evaluations can be predicates. A category string is never implicitly a boolean.
A direct `semantic` predicate rejects other result types at initialization. Simple's bare or negated
`${semantic('name')}` predicate rejects them before inference when it is matched; comparisons and
membership tests can consume their typed values.
@@ -488,10 +512,10 @@ do not imply equivalent quality or calibration when switching providers. Timeout
answers and unsupported capabilities are errors, distinct from valid negative decisions or
unmatched Choice results.
-== Batching named questions
+== Batching named evaluations
-Use `refs:name1,name2` to evaluate independent questions against the same selected state.
-The expression returns a map of normalized decisions keyed by question name. For example,
+Use `refs:name1,name2` to evaluate independent evaluations against the same selected state.
+The expression returns a map of normalized decisions keyed by evaluation name. For example,
using the declarations above:
[source,java]
@@ -540,13 +564,13 @@ For example, replace the route above with:
----
A result might contain `actionable -> true`, `department -> "billing"` and `urgency -> 1.2`.
-Reading the stored map does not invoke the provider again. Each question retains its own
+Reading the stored map does not invoke the provider again. Each evaluation retains its own
expert-owned threshold and uncertainty policy. There is no implicit AND/OR: batch expressions, including
-a batch containing one boolean question, cannot be used as predicates.
+a batch containing one boolean evaluation, cannot be used as predicates.
`CamelSemanticResults` holds an immutable map of the detailed `SemanticResult` objects,
keyed by the same names. These retain available probabilities, confidence and provider
-metadata. `CamelSemanticResult` remains the single-question diagnostic property for
+metadata. `CamelSemanticResult` remains the single-evaluation diagnostic property for
`ref:name`. Both diagnostic properties are cleared before every semantic evaluation;
a batch publishes its decisions and diagnostics only after every answer passes validation.
Missing, extra or invalid answers, operational failures and an uncertainty policy of `fail`
@@ -560,17 +584,17 @@ duplicate and unknown references are rejected. There is no quoting or escaping i
syntax: names containing commas or leading/trailing whitespace require a single `ref:name`
evaluation. Existing single references are unchanged.
-All selected questions must use the same effective Simple state selector, after applying
+All selected evaluations must use the same effective Simple state selector, after applying
`camel.language.semantic.default-state` and resolving property placeholders. Selectors
must match exactly; different expressions that happen to return equal values are not
interchangeable. The selector is evaluated once per batch. Missing or unsupported state
-fails before inference. One snapshot of the question definitions is used throughout each
+fails before inference. One snapshot of the evaluation definitions is used throughout each
batch, including validation and decision policies; the next evaluation sees replacements
-from resource reload. Questions that need a previous answer require separate evaluations.
+from resource reload. Evaluations that need a previous answer require separate evaluations.
-`SemanticAdapter.evaluateBatch(Map<String, SemanticQuestion>, Object)` has a default
-implementation that invokes the existing single-question method sequentially, preserving
-experts that do not implement a provider-specific batch optimisation. Questions are grouped by the resolved expert instance,
+`SemanticAdapter.evaluateBatch(Map<String, SemanticEvaluation>, Object)` has a default
+implementation that invokes the existing single-evaluation method sequentially, preserving
+experts that do not implement a provider-specific batch optimisation. Evaluations are grouped by the resolved expert instance,
and each group is passed to that instance's batch method. Two instances of the same class remain
separate groups. Each group must return exactly its assigned names. Camel validates the complete
combined result before publishing decisions or diagnostics, preserving the original reference order.
@@ -587,15 +611,15 @@ Use `language("semantic", "ref:name")` wherever an expression or boolean predica
|===
|EIP / integration point |Usage
|Choice |Store a category with Set Property, then compare that property in ordinary when predicates.
-|Filter |Use a boolean question as the filter predicate.
-|Validate |Use a boolean question; false follows normal validation failure handling.
+|Filter |Use a boolean evaluation as the filter predicate.
+|Validate |Use a boolean evaluation; false follows normal validation failure handling.
|Set Header / Set Property |Store a category or score for explicit reuse in later steps.
|Aggregate correlation |Use a category as a correlation expression, retaining tenant or case identifiers where needed.
-|Aggregate completion |Evaluate a boolean question against the accumulated body, with a size or timeout limit. `eagerCheckCompletion` instead sees the incoming exchange.
+|Aggregate completion |Evaluate a boolean evaluation against the accumulated body, with a size or timeout limit. `eagerCheckCompletion` instead sees the incoming exchange.
|Recipient List |Map a category to a configured list of recipient URIs.
|Routing Slip |Map a category to a predefined processing sequence.
|Enrich |Map a category to a configured resource URI and use an ordinary aggregation strategy.
-|Loop |Reevaluate a boolean question on updated state, with an explicit iteration or time budget.
+|Loop |Reevaluate a boolean evaluation on updated state, with an explicit iteration or time budget.
|Sort |Score each item once, store the scores, then sort using a deterministic comparator.
|On Exception / retryWhile |Evaluate whether another attempt is worthwhile, with an explicit retry budget checked before inference.
|Contextual action validation |Use Validate after ordinary permission checks and before executing the action.
@@ -630,7 +654,7 @@ Storing the category before xref:eips:choice-eip.adoc[Choice] performs one seman
reaches that Set Header or Set Property step. The branches compare the stored result without
calling the provider again. Place that step inside a loop when the decision must be refreshed
on each iteration. Nested decisions can use separate properties to retain their own results.
-Boolean questions can also be used directly as ordinary when predicates. These patterns use
+Boolean evaluations can also be used directly as ordinary when predicates. These patterns use
the existing EIP model and work with the Java, XML and YAML DSLs.
=== Aggregation and destinations
@@ -687,7 +711,7 @@ and in an implementation-dependent order.
=== Bounded semantic retry
-A boolean question can supply the xref:manual::exception-advanced.adoc[retryWhile predicate]
+A boolean evaluation can supply the xref:manual::exception-advanced.adoc[retryWhile predicate]
for an exception clause. Ask whether another attempt is worthwhile using the current failure
and selected request context. Restrict this to operations that are safe to retry.
@@ -695,10 +719,10 @@ IMPORTANT: `retryWhile` replaces the `maximumRedeliveries` decision. Check the r
inside the predicate, before calling the provider. A provider that always returns true must
not cause unlimited retries or evaluations.
-Declare a boolean question named `retryable` with `state: $\{exchangeProperty.retryState}`.
+Declare a boolean evaluation named `retryable` with `state: $\{exchangeProperty.retryState}`.
In this example, the request body is a string. `onExceptionOccurred` prepares the state before
the retry predicate runs; `onRedelivery` runs later and is too late for this purpose.
-Include only the failure details needed by the question, excluding credentials and sensitive data.
+Include only the failure details needed by the evaluation, excluding credentials and sensitive data.
[source,java]
----
@@ -735,16 +759,16 @@ negative decision in the escalation route, check `getSuppressed()` of the
=== Contextual action validation
-Use a semantic question for an additional check such as "Does this proposed action serve
+Use a semantic evaluation for an additional check such as "Does this proposed action serve
the approved task?" after normal identity, permission and tenant checks have succeeded.
The semantic decision must not grant permissions that those checks denied.
-For example, declare this question alongside routes:
+For example, declare this evaluation alongside routes:
[source,yaml]
----
- semantic:
- question:
+ evaluation:
withinScope:
type: boolean
instructions: Does the proposed action serve the approved task?
@@ -770,7 +794,7 @@ from("direct:action")
.to("direct:performAction");
----
-A negative decision raises normal validation failure. With this question's `fail` policy,
+A negative decision raises normal validation failure. With this evaluation's `fail` policy,
an uncertain decision raises an evaluation error. Timeouts and malformed responses also
fail, and none of these outcomes should execute the protected action. Failure handling may
reject the request or send it for human review; do not use `continued(true)` to resume at
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/switch-eip.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/switch-eip.adoc
index 4fab8169deb6..3c51ccd67dae 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/switch-eip.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/switch-eip.adoc
@@ -169,7 +169,7 @@ handling routes shown above.
[source,yaml]
----
- semantic:
- question:
+ evaluation:
department:
type: choice
instructions: Which department should handle this message?
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/typesafe-ai-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/typesafe-ai-component.adoc
index 9504107aebe2..4b7591a3a0e6 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/typesafe-ai-component.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/typesafe-ai-component.adoc
@@ -27,7 +27,7 @@ For Boolean evaluations it applies the requested `threshold`, `uncertainty` and
once and returns both the typed verdict and the provider probability. Omitted policy parameters
use this adapter's defaults: threshold `0.5`, no uncertainty band, and failure for uncertain results.
The semantic runtime validates the result and does not apply a second policy.
-Named boolean, choice and score questions use the common contract and inherit this component's
+Named boolean, choice and score evaluations use the common contract and inherit this component's
credentials, model and transport settings without requiring a TypeSafe AI endpoint in the route.
[source,xml]
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json
index 8d6da5d4eefc..0f9c9da87883 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json
@@ -3,7 +3,7 @@
"kind": "language",
"name": "semantic",
"title": "Semantic Evaluation",
- "description": "Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters",
+ "description": "Invoke named evaluations of message content to produce boolean decisions, categories, scores and label sets through provider adapters",
"deprecated": false,
"firstVersion": "4.23.0",
"label": "language,ai",
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
index 6a67d3001303..4fe171054c8c 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
@@ -20525,34 +20525,6 @@
},
"semantic" : {
"isListItem" : true,
- "children" : [ {
- "name" : "expert",
- "type" : "string"
- }, {
- "name" : "question",
- "type" : "map",
- "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
- "required" : true
- }, {
- "name" : "state",
- "type" : "string"
- }, {
- "name" : "evaluation",
- "type" : "map",
- "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
- "required" : true
- } ]
- },
- "semanticDefinitionDeserializer$BlockSchema" : {
- "children" : [ {
- "name" : "expert",
- "type" : "string"
- }, {
- "name" : "state",
- "type" : "string"
- } ]
- },
- "semanticDefinitionDeserializer$EvaluationBlockSchema" : {
"children" : [ {
"name" : "evaluation",
"type" : "map",
@@ -20665,20 +20637,6 @@
"name" : "uncertaintyPolicy",
"type" : "string"
} ]
- },
- "semanticDefinitionDeserializer$QuestionBlockSchema" : {
- "children" : [ {
- "name" : "expert",
- "type" : "string"
- }, {
- "name" : "question",
- "type" : "map",
- "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
- "required" : true
- }, {
- "name" : "state",
- "type" : "string"
- } ]
}
}
}
\ No newline at end of file
diff --git a/components/camel-ai/camel-semantic/pom.xml b/components/camel-ai/camel-semantic/pom.xml
index 67549b19c64b..c68b662e30b7 100644
--- a/components/camel-ai/camel-semantic/pom.xml
+++ b/components/camel-ai/camel-semantic/pom.xml
@@ -27,7 +27,7 @@
</parent>
<artifactId>camel-semantic</artifactId>
<name>Camel :: AI :: Semantic Evaluation</name>
- <description>Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters</description>
+ <description>Invoke named evaluations of message content to produce boolean decisions, categories, scores and label sets through provider adapters</description>
<dependencies>
<dependency>
<groupId>org.apache.camel</groupId>
diff --git a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json
index 8d6da5d4eefc..0f9c9da87883 100644
--- a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json
+++ b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json
@@ -3,7 +3,7 @@
"kind": "language",
"name": "semantic",
"title": "Semantic Evaluation",
- "description": "Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters",
+ "description": "Invoke named evaluations of message content to produce boolean decisions, categories, scores and label sets through provider adapters",
"deprecated": false,
"firstVersion": "4.23.0",
"label": "language,ai",
diff --git a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties
index 09dbb24f7d38..618df189e08a 100644
--- a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties
+++ b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties
@@ -4,4 +4,4 @@ groupId=org.apache.camel
artifactId=camel-semantic
version=4.23.0-SNAPSHOT
projectName=Camel :: AI :: Semantic Evaluation
-projectDescription=Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters
+projectDescription=Invoke named evaluations of message content to produce boolean decisions, categories, scores and label sets through provider adapters
diff --git a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
index 0598a8714749..5083db4bc638 100644
--- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
+++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
@@ -2,7 +2,7 @@
:doctitle: Semantic Evaluation
:shortname: semantic
:artifactid: camel-semantic
-:description: Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters
+:description: Invoke named evaluations of message content to produce boolean decisions, categories, scores and label sets through provider adapters
:since: 4.23
:supportlevel: Preview
:tabs-sync-option:
@@ -24,7 +24,7 @@ concurrency limits and transport resources. TypeSafe AI uses
`camel.component.typesafe-ai.*` settings even when a route contains no TypeSafe AI endpoint.
An *expert* is a configured semantic evaluator, implemented by `SemanticAdapter`. It is not
-an autonomous agent. Each named question can select a registry bean using `expert`.
+an autonomous agent. Each named evaluation can select a registry bean using `expert`.
Without that field, `camel.language.semantic.default-expert=myExpert` selects the default bean.
The existing `camel.language.semantic.adapter=myAdapter` setting remains supported as the
default when `default-expert` is absent; it also accepts a fully qualified implementation class name.
@@ -35,7 +35,7 @@ implementation class, including advertised superclasses of registered subclasses
configured instances remain distinct candidates.
No eligible expert, or multiple candidates, is an initialization error listing the candidates.
Unknown explicit names and invalid defaults fail without fallback. Selection never uses the
-question's result type: two boolean experts can evaluate entirely different properties.
+evaluation's result type: two boolean experts can evaluate entirely different properties.
Use plain names without `#bean:` or `#class:` prefixes. Registry lookup takes precedence over
class resolution. Class selection uses Camel's class resolver and injector.
Created adapters are registered as `camelSemanticAdapter` and managed by the context.
@@ -99,19 +99,42 @@ Declaring an evaluation does not execute it.
`parameters` can contain strings, booleans, numbers, nested maps and lists. Camel copies them
into immutable declarations. Java, YAML and XML builders resolve property placeholders in string
parameter values, including nested list and map values; map keys stay literal. Resolved strings
-remain strings: generic numeric parameters require numbers, while the `threshold` and `uncertainty`
-shorthand options also accept numeric placeholders. Direct `SemanticQuestion` construction takes
+remain strings. For numeric parameters, including values inside nested maps or lists, use an
+explicit numeric value after placeholder resolution. YAML accepts `!number "{{limit:0.8}}"`,
+XML accepts `<number>{{limit:0.8}}</number>`, and Java accepts
+`Double.valueOf(getContext().resolvePropertyPlaceholders("{{limit:0.8}}"))` as a parameter value.
+These forms work for any parameter name and expert. The `threshold` and `uncertainty` shorthand
+options also accept numeric placeholders. Direct `SemanticEvaluation` construction takes
already resolved parameter values and requires no Camel context. Omitted parameters stay absent: their behaviour belongs to the
expert, and Camel does not insert threshold or confidence defaults. Parameter contracts use
`String`, `Boolean`, `Number`, `Map` and `List` data shapes. Use `Number` with `integer = true`
for whole-number parameters; do not use JVM numeric wrapper classes such as `Integer` in a
contract. A provider consumes numeric values through `Number` across all DSLs.
+For example, a nested expert policy can externalise its cutoff without converting string labels:
+
+[source,yaml]
+----
+- semantic:
+ expert: moderation
+ evaluation:
+ contentPolicy:
+ operation: categories
+ parameters:
+ policy:
+ cutoff: !number "{{content.cutoff:0.8}}"
+ label: "{{content.label:privacy}}"
+----
+
+String parameters, including prompts, use Camel property placeholder escaping for literal braces.
+For example, YAML `instructions: 'Explain \{{example\}}'` gives the expert `Explain {{example}}`.
+In Java, use `.parameter("instructions", "Explain \\{{example\\}}")` for the same literal text.
+
Camel validates recognised parameter names, required values, types and declared constraints.
The expert checks operation-specific combinations and compatibility with its bean configuration.
Startup validates all declarations, including unused evaluations, without inference. Route
resource replacements also validate before publication, retaining previous declarations on failure.
-The low-level `SemanticQuestions.replace(String, Map)` API also validates new and changed
+The low-level `SemanticEvaluations.replace(String, Map)` API also validates new and changed
unused declarations before publishing them on a started context.
=== Calling an evaluation
@@ -191,9 +214,9 @@ Java uses the same declaration model and validation path:
[source,java]
----
-import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
-semanticQuestions(this).expert("moderation").state("${body}")
+semanticEvaluations(this).expert("moderation").state("${body}")
.evaluation("responseSafety").operation("response_safety").end()
.evaluation("safetyCategories").operation("categories")
.parameter("policy", Map.of("labels", List.of("privacy", "unsafe"), "threshold", 0.8))
@@ -224,19 +247,20 @@ parameter to request the expert's omission behaviour.
== Instruction-driven shorthand
-The `question` syntax and Java convenience methods below target instruction-driven operations
+The shorthand fields and Java convenience methods below target instruction-driven operations
named `boolean`, `choice` and `score`. Their fields become expert parameters and are validated by
the selected expert. TypeSafe AI implements these operations and requires nonblank instructions.
-Specialised operations normally use `evaluation` and `parameters` as shown above.
+All operations use the same `evaluation` declaration syntax; specialised operations supply their
+expert-owned `parameters` as shown above.
-== Named questions
+== Named evaluations
The xref:others:yaml-dsl.adoc[YAML DSL] supports declarations alongside routes, including declarations after their use:
[source,yaml]
----
- semantic:
- question:
+ evaluation:
department:
type: choice
instructions: Which department should handle this message?
@@ -287,7 +311,7 @@ The xref:others:yaml-dsl.adoc[YAML DSL] supports declarations alongside routes,
----
Names are context-wide. Duplicate declarations across resources and unknown references fail.
-Reloading a resource replaces its complete set of questions, including removing declarations
+Reloading a resource replaces its complete set of evaluations, including removing declarations
no longer present. Development-mode route reload also removes definitions from deleted or renamed files
before parsing replacements. This replacement does not make the surrounding route reload transactional.
Existing expressions resolve the current definition on their next evaluation.
@@ -302,10 +326,10 @@ alive. Constraints can remain until an unused expression is garbage-collected; s
does not necessarily release an expression that is still referenced elsewhere.
Loading declarations does not perform inference or start provider resources for validation.
-For questions using `expert` or `default-expert`, the registry bean is resolved when the
+For evaluations using `expert` or `default-expert`, the registry bean is resolved when the
expression is compiled. Rebinding that name does not update already compiled expressions.
After replacing the bean, recreate the consuming routes or expressions, or reload their
-question declarations so the expressions compile against the replacement bean.
+evaluation declarations so the expressions compile against the replacement bean.
=== YAML reload and expert bean preparation
@@ -321,12 +345,12 @@ Use the fluent helper from `camel-semantic` inside an ordinary `RouteBuilder`:
[source,java]
----
-import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
@Override
public void configure() {
- semanticQuestions(this)
- .question("department")
+ semanticEvaluations(this)
+ .evaluation("department")
.type("choice")
.state("${header.myState}")
.instructions("Which department should handle this message?")
@@ -341,21 +365,21 @@ public void configure() {
}
----
-Call `.end().question("anotherName")` to add another question to the group, then `.register()`
+Call `.end().evaluation("anotherName")` to add another evaluation to the group, then `.register()`
once to validate and install the entire group atomically. Register before creating expressions
that refer to these names. When adding builders to a running context, add the builder declaring
-shared questions before builders whose routes reference them.
+shared evaluations before builders whose routes reference them.
Use `.type("boolean")` with optional `.threshold(0.8)`, `.uncertainty(0.1)` and
-`.uncertaintyPolicy("non-match")` for boolean questions. Use `.type("score")` and successive
+`.uncertaintyPolicy("non-match")` for boolean evaluations. Use `.type("score")` and successive
`.level("description")` calls for ordered score levels. Applications can also register immutable
-`SemanticQuestion` definitions using `SemanticQuestions.get(context).replace(source, questions)`.
+`SemanticEvaluation` definitions using `SemanticEvaluations.get(context).replace(source, evaluations)`.
-Each Java resource owns one group. Register all of its questions together; registering again
+Each Java resource owns one group. Register all of its evaluations together; registering again
replaces that resource's previous group. Reloading a Java resource without the helper removes
its declarations. Embedded builders without a resource receive distinct generated source keys.
The helper reserves `"java:" + resource.getLocation()` as the resource source key; passing that
-key and an empty map to `replace` removes the resource's questions.
+key and an empty map to `replace` removes the resource's evaluations.
=== XML declarations
@@ -375,12 +399,12 @@ Both layouts are supported. To keep declarations alongside routes, use `tickets.
----
<routes>
<semantic>
- <question name="department" type="choice" state="${header.myState}">
+ <evaluation name="department" type="choice" state="${header.myState}">
<instructions>Which department should handle this message?</instructions>
<criterion key="billing" value="Invoices, payments, and refunds"/>
<criterion key="technical" value="Bugs, outages, and technical problems"/>
<criterion key="other" value="Everything else"/>
- </question>
+ </evaluation>
</semantic>
<route id="classify-ticket">
<from uri="direct:tickets"/>
@@ -392,25 +416,25 @@ Both layouts are supported. To keep declarations alongside routes, use `tickets.
</routes>
----
-To share declarations across route files, use a standalone `questions.xml`:
+To share declarations across route files, use a standalone `evaluations.xml`:
[source,xml]
----
<semantic>
- <question name="department" type="choice" state="${header.myState}">
+ <evaluation name="department" type="choice" state="${header.myState}">
<instructions>Which department should handle this message?</instructions>
<criterion key="billing" value="Invoices, payments, and refunds"/>
<criterion key="technical" value="Bugs, outages, and technical problems"/>
<criterion key="other" value="Everything else"/>
- </question>
+ </evaluation>
</semantic>
----
Load this together with ordinary `*.xml`, Java or YAML route resources that use `ref:department`.
For example, with Camel Main, set
-`camel.main.routes-include-pattern=classpath:questions.xml,classpath:routes.xml`.
+`camel.main.routes-include-pattern=classpath:evaluations.xml,classpath:routes.xml`.
Declarations are registered before consuming routes are configured. Reload replaces the source's
-questions; removing the `semantic` block, using an empty block, or deleting the resource removes
+evaluations; removing the `semantic` block, using an empty block, or deleting the resource removes
obsolete definitions. XML source keys are the resource locations.
The combined format supports a `routes` root with one optional `semantic` block and ordinary
@@ -418,15 +442,15 @@ The combined format supports a `routes` root with one optional `semantic` block
supported, as are documents consistently using `http://camel.apache.org/schema/semantic`,
`http://camel.apache.org/schema/xml-io`, or `http://camel.apache.org/schema/spring`.
A `semantic` block can also declare `xmlns="http://camel.apache.org/schema/semantic"` inside a
-standard `routes` document; its question elements inherit that namespace. The extension detects
+standard `routes` document; its evaluation elements inherit that namespace. The extension detects
the declaration block or semantic root namespace automatically.
These extensions do not validate against Camel's standard core XSDs. Automatic discovery applies
to Camel's route resource loader; it does not extend Spring's XML application-context parser
or direct JAXB unmarshalling.
-For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question
-attributes. For score questions, replace the named `criterion` elements with ordered `level`
+For boolean evaluations, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional evaluation
+attributes. For score evaluations, replace the named `criterion` elements with ordered `level`
elements, such as `<level>Routine</level><level>Urgent</level><level>Critical</level>`.
The numeric `threshold` and `uncertainty` options accept property placeholders in all three
@@ -435,12 +459,12 @@ DSLs. For example, Java accepts `.threshold("{{semantic.threshold:0.5}}")`, and
are registered.
Java and XML declarations use the same context-wide registry, validation, defaults and adapters
-as YAML. They do not require `camel-yaml-dsl`. Their questions can also be selected together
+as YAML. They do not require `camel-yaml-dsl`. Their evaluations can also be selected together
using `refs:name1,name2`, as described below.
=== Exporting routes
-Question declarations live outside Camel's core route model. Generic model exports and runtime
+Evaluation declarations live outside Camel's core route model. Generic model exports and runtime
route dumps such as `camel.main.dumpRoutes=yaml` contain only routes; keep declarations separately
and load them before reloading an exported route.
@@ -451,7 +475,7 @@ using the generic converter.
=== Selected state
-A question's optional `state` Simple expression overrides
+A evaluation's optional `state` Simple expression overrides
`camel.language.semantic.default-state`, whose default is `$\{body}`. Selectors are compiled
before evaluation; selected strings, maps and lists are passed as data and are never evaluated recursively.
A missing selected header fails instead of falling back to the body. A selector must be nonblank
@@ -470,7 +494,7 @@ in semantic evaluation, and Java applications using `camel-semantic` do not pull
== Results and policy
-Only boolean questions can be predicates. A category string is never implicitly a boolean.
+Only boolean evaluations can be predicates. A category string is never implicitly a boolean.
A direct `semantic` predicate rejects other result types at initialization. Simple's bare or negated
`${semantic('name')}` predicate rejects them before inference when it is matched; comparisons and
membership tests can consume their typed values.
@@ -488,10 +512,10 @@ do not imply equivalent quality or calibration when switching providers. Timeout
answers and unsupported capabilities are errors, distinct from valid negative decisions or
unmatched Choice results.
-== Batching named questions
+== Batching named evaluations
-Use `refs:name1,name2` to evaluate independent questions against the same selected state.
-The expression returns a map of normalized decisions keyed by question name. For example,
+Use `refs:name1,name2` to evaluate independent evaluations against the same selected state.
+The expression returns a map of normalized decisions keyed by evaluation name. For example,
using the declarations above:
[source,java]
@@ -540,13 +564,13 @@ For example, replace the route above with:
----
A result might contain `actionable -> true`, `department -> "billing"` and `urgency -> 1.2`.
-Reading the stored map does not invoke the provider again. Each question retains its own
+Reading the stored map does not invoke the provider again. Each evaluation retains its own
expert-owned threshold and uncertainty policy. There is no implicit AND/OR: batch expressions, including
-a batch containing one boolean question, cannot be used as predicates.
+a batch containing one boolean evaluation, cannot be used as predicates.
`CamelSemanticResults` holds an immutable map of the detailed `SemanticResult` objects,
keyed by the same names. These retain available probabilities, confidence and provider
-metadata. `CamelSemanticResult` remains the single-question diagnostic property for
+metadata. `CamelSemanticResult` remains the single-evaluation diagnostic property for
`ref:name`. Both diagnostic properties are cleared before every semantic evaluation;
a batch publishes its decisions and diagnostics only after every answer passes validation.
Missing, extra or invalid answers, operational failures and an uncertainty policy of `fail`
@@ -560,17 +584,17 @@ duplicate and unknown references are rejected. There is no quoting or escaping i
syntax: names containing commas or leading/trailing whitespace require a single `ref:name`
evaluation. Existing single references are unchanged.
-All selected questions must use the same effective Simple state selector, after applying
+All selected evaluations must use the same effective Simple state selector, after applying
`camel.language.semantic.default-state` and resolving property placeholders. Selectors
must match exactly; different expressions that happen to return equal values are not
interchangeable. The selector is evaluated once per batch. Missing or unsupported state
-fails before inference. One snapshot of the question definitions is used throughout each
+fails before inference. One snapshot of the evaluation definitions is used throughout each
batch, including validation and decision policies; the next evaluation sees replacements
-from resource reload. Questions that need a previous answer require separate evaluations.
+from resource reload. Evaluations that need a previous answer require separate evaluations.
-`SemanticAdapter.evaluateBatch(Map<String, SemanticQuestion>, Object)` has a default
-implementation that invokes the existing single-question method sequentially, preserving
-experts that do not implement a provider-specific batch optimisation. Questions are grouped by the resolved expert instance,
+`SemanticAdapter.evaluateBatch(Map<String, SemanticEvaluation>, Object)` has a default
+implementation that invokes the existing single-evaluation method sequentially, preserving
+experts that do not implement a provider-specific batch optimisation. Evaluations are grouped by the resolved expert instance,
and each group is passed to that instance's batch method. Two instances of the same class remain
separate groups. Each group must return exactly its assigned names. Camel validates the complete
combined result before publishing decisions or diagnostics, preserving the original reference order.
@@ -587,15 +611,15 @@ Use `language("semantic", "ref:name")` wherever an expression or boolean predica
|===
|EIP / integration point |Usage
|Choice |Store a category with Set Property, then compare that property in ordinary when predicates.
-|Filter |Use a boolean question as the filter predicate.
-|Validate |Use a boolean question; false follows normal validation failure handling.
+|Filter |Use a boolean evaluation as the filter predicate.
+|Validate |Use a boolean evaluation; false follows normal validation failure handling.
|Set Header / Set Property |Store a category or score for explicit reuse in later steps.
|Aggregate correlation |Use a category as a correlation expression, retaining tenant or case identifiers where needed.
-|Aggregate completion |Evaluate a boolean question against the accumulated body, with a size or timeout limit. `eagerCheckCompletion` instead sees the incoming exchange.
+|Aggregate completion |Evaluate a boolean evaluation against the accumulated body, with a size or timeout limit. `eagerCheckCompletion` instead sees the incoming exchange.
|Recipient List |Map a category to a configured list of recipient URIs.
|Routing Slip |Map a category to a predefined processing sequence.
|Enrich |Map a category to a configured resource URI and use an ordinary aggregation strategy.
-|Loop |Reevaluate a boolean question on updated state, with an explicit iteration or time budget.
+|Loop |Reevaluate a boolean evaluation on updated state, with an explicit iteration or time budget.
|Sort |Score each item once, store the scores, then sort using a deterministic comparator.
|On Exception / retryWhile |Evaluate whether another attempt is worthwhile, with an explicit retry budget checked before inference.
|Contextual action validation |Use Validate after ordinary permission checks and before executing the action.
@@ -630,7 +654,7 @@ Storing the category before xref:eips:choice-eip.adoc[Choice] performs one seman
reaches that Set Header or Set Property step. The branches compare the stored result without
calling the provider again. Place that step inside a loop when the decision must be refreshed
on each iteration. Nested decisions can use separate properties to retain their own results.
-Boolean questions can also be used directly as ordinary when predicates. These patterns use
+Boolean evaluations can also be used directly as ordinary when predicates. These patterns use
the existing EIP model and work with the Java, XML and YAML DSLs.
=== Aggregation and destinations
@@ -687,7 +711,7 @@ and in an implementation-dependent order.
=== Bounded semantic retry
-A boolean question can supply the xref:manual::exception-advanced.adoc[retryWhile predicate]
+A boolean evaluation can supply the xref:manual::exception-advanced.adoc[retryWhile predicate]
for an exception clause. Ask whether another attempt is worthwhile using the current failure
and selected request context. Restrict this to operations that are safe to retry.
@@ -695,10 +719,10 @@ IMPORTANT: `retryWhile` replaces the `maximumRedeliveries` decision. Check the r
inside the predicate, before calling the provider. A provider that always returns true must
not cause unlimited retries or evaluations.
-Declare a boolean question named `retryable` with `state: $\{exchangeProperty.retryState}`.
+Declare a boolean evaluation named `retryable` with `state: $\{exchangeProperty.retryState}`.
In this example, the request body is a string. `onExceptionOccurred` prepares the state before
the retry predicate runs; `onRedelivery` runs later and is too late for this purpose.
-Include only the failure details needed by the question, excluding credentials and sensitive data.
+Include only the failure details needed by the evaluation, excluding credentials and sensitive data.
[source,java]
----
@@ -735,16 +759,16 @@ negative decision in the escalation route, check `getSuppressed()` of the
=== Contextual action validation
-Use a semantic question for an additional check such as "Does this proposed action serve
+Use a semantic evaluation for an additional check such as "Does this proposed action serve
the approved task?" after normal identity, permission and tenant checks have succeeded.
The semantic decision must not grant permissions that those checks denied.
-For example, declare this question alongside routes:
+For example, declare this evaluation alongside routes:
[source,yaml]
----
- semantic:
- question:
+ evaluation:
withinScope:
type: boolean
instructions: Does the proposed action serve the approved task?
@@ -770,7 +794,7 @@ from("direct:action")
.to("direct:performAction");
----
-A negative decision raises normal validation failure. With this question's `fail` policy,
+A negative decision raises normal validation failure. With this evaluation's `fail` policy,
an uncertain decision raises an evaluation error. Timeouts and malformed responses also
fail, and none of these outcomes should execute the protected action. Failure handling may
reject the request or send it for human review; do not use `continued(true)` to resume at
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
index 7e932683a409..f2eebc7c2c88 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
@@ -43,9 +43,9 @@ import org.apache.camel.RuntimeCamelException;
import org.apache.camel.semantic.SemanticAdapter;
import org.apache.camel.semantic.SemanticCapabilities;
import org.apache.camel.semantic.SemanticCapabilities.Operation;
+import org.apache.camel.semantic.SemanticEvaluation;
+import org.apache.camel.semantic.SemanticEvaluations;
import org.apache.camel.semantic.SemanticExpert.ResultType;
-import org.apache.camel.semantic.SemanticQuestion;
-import org.apache.camel.semantic.SemanticQuestions;
import org.apache.camel.semantic.SemanticResult;
import org.apache.camel.spi.FactoryFinder;
import org.apache.camel.spi.Metadata;
@@ -56,10 +56,10 @@ import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.support.service.ServiceSupport;
import org.apache.camel.util.IOHelper;
-/** Evaluates a named, provider-independent question against selected message state. */
+/** Invokes a named, provider-independent evaluation against selected message state. */
@Language(value = "semantic", modelName = "language")
@Metadata(title = "Semantic Evaluation",
- description = "Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters",
+ description = "Invoke named evaluations of message content to produce boolean decisions, categories, scores and label sets through provider adapters",
label = "language,ai", firstVersion = "4.23.0")
public class SemanticLanguage extends LanguageSupport {
public static final String RESULT = "CamelSemanticResult";
@@ -101,7 +101,7 @@ public class SemanticLanguage extends LanguageSupport {
return defaultState;
}
- /** Default Simple state selector for questions without their own selector. Defaults to the body. */
+ /** Default Simple state selector for evaluations without their own selector. Defaults to the body. */
public void setDefaultState(String defaultState) {
this.defaultState = defaultState;
}
@@ -133,10 +133,10 @@ public class SemanticLanguage extends LanguageSupport {
if (expression != null && expression.startsWith("refs:")) {
List<String> names = Arrays.stream(expression.substring(5).split(",", -1)).map(String::strip).toList();
if (names.stream().anyMatch(String::isEmpty)) {
- throw new IllegalArgumentException("Semantic batch requires nonblank question names separated by commas");
+ throw new IllegalArgumentException("Semantic batch requires nonblank evaluation names separated by commas");
}
if (new HashSet<>(names).size() != names.size()) {
- throw new IllegalArgumentException("Duplicate semantic question reference in batch");
+ throw new IllegalArgumentException("Duplicate semantic evaluation reference in batch");
}
return names;
}
@@ -298,11 +298,11 @@ public class SemanticLanguage extends LanguageSupport {
}
/** Validate declarations, including unused evaluations, without starting managed expert resources. */
- public void validateDeclarations(Map<String, SemanticQuestion> declarations) {
+ public void validateDeclarations(Map<String, SemanticEvaluation> declarations) {
validationOnly(() -> {
- declarations.forEach((name, question) -> {
- expert(name, question);
- String selector = question.getState() != null ? question.getState() : defaultState;
+ declarations.forEach((name, evaluation) -> {
+ expert(name, evaluation);
+ String selector = evaluation.getState() != null ? evaluation.getState() : defaultState;
if (selector == null || selector.isBlank()) {
throw new IllegalArgumentException("Semantic evaluation '" + name + "': state must not be blank");
}
@@ -328,8 +328,8 @@ public class SemanticLanguage extends LanguageSupport {
}
}
- private ResolvedExpert expert(String name, SemanticQuestion question) {
- String reference = question.getExpert() != null ? question.getExpert() : defaultExpert;
+ private ResolvedExpert expert(String name, SemanticEvaluation evaluation) {
+ String reference = evaluation.getExpert() != null ? evaluation.getExpert() : defaultExpert;
String label = reference != null ? reference : adapter != null ? adapter : "automatic";
try {
SemanticAdapter provider;
@@ -350,8 +350,8 @@ public class SemanticLanguage extends LanguageSupport {
}
}
SemanticCapabilities capabilities = SemanticCapabilities.from(provider.getClass());
- capabilities.validate(question);
- provider.validate(question);
+ capabilities.validate(evaluation);
+ provider.validate(evaluation);
return new ResolvedExpert(provider, capabilities, label);
} catch (RuntimeException e) {
if (e instanceof IllegalArgumentException) {
@@ -431,9 +431,9 @@ public class SemanticLanguage extends LanguageSupport {
private final List<String> names;
private final boolean batch;
private final boolean predicate;
- private final Consumer<Map<String, SemanticQuestion>> validator = this::prepare;
+ private final Consumer<Map<String, SemanticEvaluation>> validator = this::prepare;
private volatile Compiled compiled;
- private volatile SemanticQuestions questions;
+ private volatile SemanticEvaluations evaluations;
private Evaluation(List<String> names, boolean batch, boolean predicate) {
this.names = names;
@@ -444,26 +444,26 @@ public class SemanticLanguage extends LanguageSupport {
@Override
public void init(CamelContext context) {
super.init(context);
- questions = SemanticQuestions.get(context);
- Map<String, SemanticQuestion> selected = questions.get(names);
+ evaluations = SemanticEvaluations.get(context);
+ Map<String, SemanticEvaluation> selected = evaluations.get(names);
compile(selected);
if (!Boolean.TRUE.equals(validating.get())) {
- questions.setValidator(names, validator);
+ evaluations.setValidator(names, validator);
}
}
private void requireBoolean(Operation operation) {
if (operation.getResultType() != ResultType.BOOLEAN) {
- throw new IllegalArgumentException("Semantic predicate requires a boolean question: " + names.get(0));
+ throw new IllegalArgumentException("Semantic predicate requires a boolean evaluation: " + names.get(0));
}
}
- private synchronized Compiled compile(Map<String, SemanticQuestion> selected) {
+ private synchronized Compiled compile(Map<String, SemanticEvaluation> selected) {
if (Boolean.TRUE.equals(validating.get())) {
// Revisit cached nested selectors against the candidate snapshot without publishing compilation state.
return prepare(selected);
}
- if (compiled == null || !compiled.questions.equals(selected)) {
+ if (compiled == null || !compiled.evaluations.equals(selected)) {
Compiled candidate = prepare(selected);
startAdapter(candidate.groups);
compiled = candidate;
@@ -471,19 +471,19 @@ public class SemanticLanguage extends LanguageSupport {
return compiled;
}
- private Compiled prepare(Map<String, SemanticQuestion> selected) {
+ private Compiled prepare(Map<String, SemanticEvaluation> selected) {
return validationOnly(() -> prepareDeclarations(selected));
}
- private Compiled prepareDeclarations(Map<String, SemanticQuestion> selected) {
+ private Compiled prepareDeclarations(Map<String, SemanticEvaluation> selected) {
Map<String, Operation> operations = new LinkedHashMap<>();
String selector = null;
Map<SemanticAdapter, Group> byInstance = new IdentityHashMap<>();
List<Group> groups = new ArrayList<>();
for (var entry : selected.entrySet()) {
- SemanticQuestion question = entry.getValue();
- ResolvedExpert resolved = expert(entry.getKey(), question);
- Operation operation = resolved.capabilities.operation(question.getOperation());
+ SemanticEvaluation evaluation = entry.getValue();
+ ResolvedExpert resolved = expert(entry.getKey(), evaluation);
+ Operation operation = resolved.capabilities.operation(evaluation.getOperation());
operations.put(entry.getKey(), operation);
if (predicate) {
requireBoolean(operation);
@@ -494,16 +494,16 @@ public class SemanticLanguage extends LanguageSupport {
byInstance.put(resolved.provider, group);
groups.add(group);
}
- group.questions.put(entry.getKey(), question);
- String effective = question.getState() != null ? question.getState() : defaultState;
+ group.evaluations.put(entry.getKey(), evaluation);
+ String effective = evaluation.getState() != null ? evaluation.getState() : defaultState;
if (effective == null || effective.isBlank()) {
throw new IllegalArgumentException(
- "Semantic state selector must not be blank for question: " + entry.getKey());
+ "Semantic state selector must not be blank for evaluation: " + entry.getKey());
}
effective = getCamelContext().resolvePropertyPlaceholders(effective);
if (selector != null && !selector.equals(effective)) {
throw new IllegalArgumentException(
- "Semantic batch questions must use the same effective state selector");
+ "Semantic batch evaluations must use the same effective state selector");
}
selector = effective;
}
@@ -511,7 +511,7 @@ public class SemanticLanguage extends LanguageSupport {
state.init(getCamelContext());
return new Compiled(
selected, Map.copyOf(operations), state, groups.stream()
- .map(group -> new Group(group.expert, Collections.unmodifiableMap(group.questions))).toList());
+ .map(group -> new Group(group.expert, Collections.unmodifiableMap(group.evaluations))).toList());
}
@Override
@@ -522,55 +522,54 @@ public class SemanticLanguage extends LanguageSupport {
private Object evaluate(Exchange exchange, boolean asPredicate) {
exchange.removeProperty(RESULT);
exchange.removeProperty(RESULTS);
- SemanticQuestion single = null;
+ SemanticEvaluation single = null;
if (asPredicate) {
if (batch) {
throw new IllegalArgumentException("Semantic batch expressions cannot be predicates");
}
- single = questions.get(names.get(0));
+ single = evaluations.get(names.get(0));
}
try {
Compiled current = compiled;
if (batch) {
- Map<String, SemanticQuestion> selected = questions.get(names);
- if (current == null || !current.questions.equals(selected)) {
+ Map<String, SemanticEvaluation> selected = evaluations.get(names);
+ if (current == null || !current.evaluations.equals(selected)) {
current = compile(selected);
}
} else {
String name = names.get(0);
if (single == null) {
- single = questions.get(name);
+ single = evaluations.get(name);
}
- if (current == null || current.questions.get(name) != single) {
+ if (current == null || current.evaluations.get(name) != single) {
current = compile(Map.of(name, single));
}
}
if (asPredicate) {
requireBoolean(current.operations.get(names.get(0)));
}
- startAdapter(current.groups);
Object state = current.state.evaluate(exchange, Object.class);
// A state selector can itself evaluate another semantic expression.
exchange.removeProperty(RESULT);
exchange.removeProperty(RESULTS);
if (state == null) {
- throw new IllegalArgumentException("Missing selected state for semantic questions: " + names);
+ throw new IllegalArgumentException("Missing selected state for semantic evaluations: " + names);
}
if (!(state instanceof String || state instanceof Map<?, ?> || state instanceof List<?>)) {
throw new IllegalArgumentException(
- "Unsupported state type for semantic questions: " + names
+ "Unsupported state type for semantic evaluations: " + names
+ ". Select strings, maps or lists explicitly");
}
// Check every group's input before invoking any provider.
for (Group group : current.groups) {
try {
- for (var entry : group.questions.entrySet()) {
+ for (var entry : group.evaluations.entrySet()) {
current.operations.get(entry.getKey()).validateInput(state);
group.expert.provider.validateInput(entry.getValue(), state);
}
} catch (IllegalArgumentException invalid) {
throw new IllegalArgumentException(
- "Semantic evaluations " + group.questions.keySet() + ", expert '" + group.expert.reference
+ "Semantic evaluations " + group.evaluations.keySet() + ", expert '" + group.expert.reference
+ "': " + invalid.getMessage(),
invalid);
}
@@ -582,11 +581,11 @@ public class SemanticLanguage extends LanguageSupport {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException("Semantic batch evaluation interrupted");
}
- Map<String, SemanticResult> answers = group.expert.provider.evaluateBatch(group.questions, state);
- if (answers == null || !answers.keySet().equals(group.questions.keySet())) {
- throw new IllegalArgumentException("Semantic batch result names must match question names");
+ Map<String, SemanticResult> answers = group.expert.provider.evaluateBatch(group.evaluations, state);
+ if (answers == null || !answers.keySet().equals(group.evaluations.keySet())) {
+ throw new IllegalArgumentException("Semantic batch result names must match evaluation names");
}
- for (String name : group.questions.keySet()) {
+ for (String name : group.evaluations.keySet()) {
decisions.put(name,
validateResult(name, group.expert, current.operations.get(name), answers.get(name)));
}
@@ -594,15 +593,15 @@ public class SemanticLanguage extends LanguageSupport {
}
Map<String, SemanticResult> details = new LinkedHashMap<>();
Map<String, Object> ordered = new LinkedHashMap<>();
- for (String name : current.questions.keySet()) {
+ for (String name : current.evaluations.keySet()) {
details.put(name, results.get(name));
ordered.put(name, decisions.get(name));
}
exchange.setProperty(RESULTS, Collections.unmodifiableMap(details));
return Collections.unmodifiableMap(ordered);
}
- SemanticQuestion question = current.questions.get(names.get(0));
- SemanticResult result = current.groups.get(0).expert.provider.evaluate(question, state);
+ SemanticEvaluation evaluation = current.evaluations.get(names.get(0));
+ SemanticResult result = current.groups.get(0).expert.provider.evaluate(evaluation, state);
Object decision = validateResult(names.get(0), current.groups.get(0).expert,
current.operations.get(names.get(0)), result);
exchange.setProperty(RESULT, result);
@@ -647,10 +646,10 @@ public class SemanticLanguage extends LanguageSupport {
private record ResolvedExpert(SemanticAdapter provider, SemanticCapabilities capabilities, String reference) {
}
- private record Group(ResolvedExpert expert, Map<String, SemanticQuestion> questions) {
+ private record Group(ResolvedExpert expert, Map<String, SemanticEvaluation> evaluations) {
}
- private record Compiled(Map<String, SemanticQuestion> questions, Map<String, Operation> operations,
+ private record Compiled(Map<String, SemanticEvaluation> evaluations, Map<String, Operation> operations,
Expression state, List<Group> groups) {
}
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAdapter.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAdapter.java
index b91eb86a6baa..1e4527e5bc89 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAdapter.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAdapter.java
@@ -29,25 +29,26 @@ import java.util.Map;
* Referenced registry beans retain their existing lifecycle owner.
*/
public interface SemanticAdapter {
- /** Reject unsupported question kinds, criteria, decision policies or input selectors before traffic starts. */
- void validate(SemanticQuestion question);
+ /** Reject unsupported evaluation kinds, criteria, decision policies or input selectors before traffic starts. */
+ void validate(SemanticEvaluation evaluation);
/** Validate message-dependent requirements before any operation in a batch performs inference. */
- default void validateInput(SemanticQuestion evaluation, Object state) {
+ default void validateInput(SemanticEvaluation evaluation, Object state) {
}
/** Synchronous, potentially blocking evaluation. Operational errors must be thrown, never returned as decisions. */
- SemanticResult evaluate(SemanticQuestion question, Object state) throws Exception;
+ SemanticResult evaluate(SemanticEvaluation evaluation, Object state) throws Exception;
/**
- * Evaluate named questions against the same selected state, returning exactly one result per name. The default
- * implementation calls the single-question method sequentially; providers may override it to use one request.
+ * Evaluate named evaluations against the same selected state, returning exactly one result per name. The default
+ * implementation calls the single-evaluation method sequentially; providers may override it to use one request.
* Operational errors must be thrown, never returned as partial results. The expert applies any requested decision
* policy exactly once.
*/
- default Map<String, SemanticResult> evaluateBatch(Map<String, SemanticQuestion> questions, Object state) throws Exception {
+ default Map<String, SemanticResult> evaluateBatch(Map<String, SemanticEvaluation> evaluations, Object state)
+ throws Exception {
Map<String, SemanticResult> results = new LinkedHashMap<>();
- for (var entry : questions.entrySet()) {
+ for (var entry : evaluations.entrySet()) {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException("Semantic batch evaluation interrupted");
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java
index 4c10a75b6a2b..05203405ab79 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java
@@ -99,7 +99,7 @@ public final class SemanticCapabilities {
return result;
}
- public void validate(SemanticQuestion evaluation) {
+ public void validate(SemanticEvaluation evaluation) {
operation(evaluation.getOperation()).validate(evaluation.getParameters());
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java
similarity index 50%
rename from components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java
rename to components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java
index d1baa30076d5..3125cfee27c3 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java
@@ -22,33 +22,20 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Set;
/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
-public final class SemanticQuestion {
+public final class SemanticEvaluation {
private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class, BigInteger.class, BigDecimal.class);
- public enum Type {
- BOOLEAN,
- CHOICE,
- SCORE,
- CLASSIFICATION
- }
-
- public enum UncertaintyPolicy {
- FAIL,
- NON_MATCH
- }
-
private final String operation;
private final String expert;
private final String state;
private final Map<String, Object> parameters;
- public SemanticQuestion(String operation, String expert, String state, Map<String, ?> parameters) {
+ public SemanticEvaluation(String operation, String expert, String state, Map<String, ?> parameters) {
if (operation == null || operation.isBlank()) {
throw new IllegalArgumentException("Evaluation operation is required");
}
@@ -64,41 +51,6 @@ public final class SemanticQuestion {
this.parameters = immutableMap(parameters == null ? Map.of() : parameters);
}
- /** Convenience declaration for instruction-driven operations named boolean, choice or score. */
- public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy) {
- this(type, instructions, state, criteria, levels, threshold, uncertainty, policy, null);
- }
-
- public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy,
- String expert) {
- this(type.name().toLowerCase(Locale.ROOT), expert, state,
- instructionParameters(type, instructions, criteria, levels, threshold, uncertainty, policy));
- }
-
- private static Map<String, Object> instructionParameters(
- Type type, String instructions, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty,
- UncertaintyPolicy policy) {
- Map<String, Object> values = new LinkedHashMap<>();
- if (instructions != null) {
- values.put("instructions", instructions);
- }
- if (criteria != null && !criteria.isEmpty()) {
- values.put("criteria", criteria);
- }
- if (levels != null && !levels.isEmpty()) {
- values.put("criteria", levels);
- }
- if (type == Type.BOOLEAN) {
- values.put("threshold", threshold);
- values.put("uncertainty", uncertainty);
- values.put("uncertaintyPolicy", policy == UncertaintyPolicy.NON_MATCH ? "non-match" : "fail");
- }
- return values;
- }
-
static Map<String, Object> immutableMap(Map<String, ?> values) {
Map<String, Object> copy = new LinkedHashMap<>();
values.forEach((name, value) -> {
@@ -149,45 +101,4 @@ public final class SemanticQuestion {
return parameters;
}
- /**
- * Instruction-driven operation kind; custom operations have their result type in the expert contract.
- *
- * @throws IllegalArgumentException if the operation is not a built-in instruction-driven kind
- */
- public Type getType() {
- return Type.valueOf(operation.toUpperCase(Locale.ROOT));
- }
-
- public String getInstructions() {
- return (String) parameters.get("instructions");
- }
-
- @SuppressWarnings("unchecked")
- public Map<String, String> getCriteria() {
- return parameters.get("criteria") instanceof Map<?, ?> map ? (Map<String, String>) map : Map.of();
- }
-
- @SuppressWarnings("unchecked")
- public List<String> getLevels() {
- return parameters.get("criteria") instanceof List<?> list ? (List<String>) list : List.of();
- }
-
- /** Requested threshold, or null when omitted. Defaults belong to the expert. */
- public Double getThreshold() {
- return parameters.get("threshold") instanceof Number number ? number.doubleValue() : null;
- }
-
- /** Requested uncertainty band, or null when omitted. Defaults belong to the expert. */
- public Double getUncertainty() {
- return parameters.get("uncertainty") instanceof Number number ? number.doubleValue() : null;
- }
-
- /** Requested instruction-driven policy, or null when omitted. Custom policies remain available in parameters. */
- public UncertaintyPolicy getUncertaintyPolicy() {
- Object policy = parameters.get("uncertaintyPolicy");
- if (policy == null) {
- return null;
- }
- return UncertaintyPolicy.valueOf(policy.toString().toUpperCase(Locale.ROOT).replace('-', '_'));
- }
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java
similarity index 81%
rename from components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java
rename to components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java
index dc9db101c94a..382eb7c20a3a 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java
@@ -26,8 +26,8 @@ import org.apache.camel.CamelContext;
import org.apache.camel.util.StringHelper;
/** Fluent definition of an expert-owned evaluation. All DSLs build the same immutable declaration. */
-public final class SemanticQuestionBuilder {
- private final SemanticQuestionsBuilder parent;
+public final class SemanticEvaluationBuilder {
+ private final SemanticEvaluationsBuilder parent;
private final Map<String, Object> parameters = new LinkedHashMap<>();
private final Map<String, String> criteria = new LinkedHashMap<>();
private final List<String> levels = new ArrayList<>();
@@ -39,25 +39,25 @@ public final class SemanticQuestionBuilder {
private String state;
/** Create a standalone declaration, completed with {@link #build(CamelContext)}. */
- public SemanticQuestionBuilder() {
+ public SemanticEvaluationBuilder() {
this(null);
}
- SemanticQuestionBuilder(SemanticQuestionsBuilder parent) {
+ SemanticEvaluationBuilder(SemanticEvaluationsBuilder parent) {
this.parent = parent;
}
- public SemanticQuestionBuilder operation(String operation) {
+ public SemanticEvaluationBuilder operation(String operation) {
this.operation = operation;
return this;
}
/** Select an instruction-driven operation by type, such as boolean, choice or score. */
- public SemanticQuestionBuilder type(String type) {
+ public SemanticEvaluationBuilder type(String type) {
return operation(type.toLowerCase(Locale.ROOT));
}
- public SemanticQuestionBuilder expert(String expert) {
+ public SemanticEvaluationBuilder expert(String expert) {
this.expert = expert;
return this;
}
@@ -66,12 +66,12 @@ public final class SemanticQuestionBuilder {
return expert != null ? expert : parent != null ? parent.getExpert() : null;
}
- public SemanticQuestionBuilder state(String state) {
+ public SemanticEvaluationBuilder state(String state) {
this.state = state;
return this;
}
- public SemanticQuestionBuilder parameter(String name, Object value) {
+ public SemanticEvaluationBuilder parameter(String name, Object value) {
if (parameters.containsKey(name)) {
throw new IllegalArgumentException("Duplicate semantic parameter: " + name);
}
@@ -79,16 +79,16 @@ public final class SemanticQuestionBuilder {
return this;
}
- public SemanticQuestionBuilder parameters(Map<String, ?> values) {
+ public SemanticEvaluationBuilder parameters(Map<String, ?> values) {
values.forEach(this::parameter);
return this;
}
- public SemanticQuestionBuilder instructions(String instructions) {
+ public SemanticEvaluationBuilder instructions(String instructions) {
return parameter("instructions", instructions);
}
- public SemanticQuestionBuilder criterion(String name, String description) {
+ public SemanticEvaluationBuilder criterion(String name, String description) {
if (criteria.containsKey(name)) {
throw new IllegalArgumentException("Duplicate semantic criterion: " + name);
}
@@ -96,36 +96,36 @@ public final class SemanticQuestionBuilder {
return this;
}
- public SemanticQuestionBuilder level(String level) {
+ public SemanticEvaluationBuilder level(String level) {
levels.add(level);
return this;
}
- public SemanticQuestionBuilder threshold(double threshold) {
+ public SemanticEvaluationBuilder threshold(double threshold) {
return parameter("threshold", threshold);
}
- public SemanticQuestionBuilder threshold(String threshold) {
+ public SemanticEvaluationBuilder threshold(String threshold) {
this.threshold = threshold;
return this;
}
- public SemanticQuestionBuilder uncertainty(double uncertainty) {
+ public SemanticEvaluationBuilder uncertainty(double uncertainty) {
return parameter("uncertainty", uncertainty);
}
- public SemanticQuestionBuilder uncertainty(String uncertainty) {
+ public SemanticEvaluationBuilder uncertainty(String uncertainty) {
this.uncertainty = uncertainty;
return this;
}
- public SemanticQuestionBuilder uncertaintyPolicy(String policy) {
+ public SemanticEvaluationBuilder uncertaintyPolicy(String policy) {
parameter("uncertaintyPolicy", policy);
normalizeUncertaintyPolicy = true;
return this;
}
- public SemanticQuestionsBuilder end() {
+ public SemanticEvaluationsBuilder end() {
if (parent == null) {
throw new IllegalStateException("Complete a standalone declaration with build(context)");
}
@@ -137,7 +137,7 @@ public final class SemanticQuestionBuilder {
}
/** Build an immutable declaration, resolving placeholders in parameter values while retaining their types. */
- public SemanticQuestion build(CamelContext context) {
+ public SemanticEvaluation build(CamelContext context) {
Map<String, Object> values = new LinkedHashMap<>(parameters);
if (!criteria.isEmpty() || !levels.isEmpty()) {
if (values.containsKey("criteria") || !criteria.isEmpty() && !levels.isEmpty()) {
@@ -163,7 +163,7 @@ public final class SemanticQuestionBuilder {
values.put("uncertaintyPolicy",
StringHelper.asEnumConstantValue(policy).toLowerCase(Locale.ROOT).replace('_', '-'));
}
- return new SemanticQuestion(
+ return new SemanticEvaluation(
operation, getExpert(), state != null ? state : parent != null ? parent.getState() : null, values);
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
similarity index 66%
rename from components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java
rename to components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
index e92c910cacaa..a7fb706f27ea 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
@@ -28,40 +28,40 @@ import org.apache.camel.CamelContext;
import org.apache.camel.language.semantic.SemanticLanguage;
import org.apache.camel.spi.Resource;
-/** Context-local named questions, replaced atomically per source when a route resource is reloaded. */
-public final class SemanticQuestions {
+/** Context-local named evaluations, replaced atomically per source when a route resource is reloaded. */
+public final class SemanticEvaluations {
private static final Object CREATION_LOCK = new Object();
private final CamelContext context;
- private final Map<String, Map<String, SemanticQuestion>> sources = new HashMap<>();
+ private final Map<String, Map<String, SemanticEvaluation>> sources = new HashMap<>();
private final Map<String, Resource> resources = new HashMap<>();
- private volatile Map<String, SemanticQuestion> questions = Map.of();
- private final ThreadLocal<Map<String, SemanticQuestion>> candidate = new ThreadLocal<>();
- private final Map<Consumer<Map<String, SemanticQuestion>>, List<String>> validators = new WeakHashMap<>();
+ private volatile Map<String, SemanticEvaluation> evaluations = Map.of();
+ private final ThreadLocal<Map<String, SemanticEvaluation>> candidate = new ThreadLocal<>();
+ private final Map<Consumer<Map<String, SemanticEvaluation>>, List<String>> validators = new WeakHashMap<>();
- private SemanticQuestions(CamelContext context) {
+ private SemanticEvaluations(CamelContext context) {
this.context = context;
}
/** Validate every declaration, including evaluations not referenced by a route. */
public void validate() {
- if (!questions.isEmpty()) {
- ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(questions);
+ if (!evaluations.isEmpty()) {
+ ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(evaluations);
}
}
/** Validate current declarations and weakly track a callback owned by its initialized expression. */
- public synchronized void setValidator(List<String> names, Consumer<Map<String, SemanticQuestion>> validator) {
+ public synchronized void setValidator(List<String> names, Consumer<Map<String, SemanticEvaluation>> validator) {
// A replacement may have occurred between the expression's initial compilation and registration.
validator.accept(get(names));
validators.put(validator, List.copyOf(names));
}
- public static SemanticQuestions get(CamelContext context) {
+ public static SemanticEvaluations get(CamelContext context) {
synchronized (CREATION_LOCK) {
- SemanticQuestions answer = context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class);
+ SemanticEvaluations answer = context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class);
if (answer == null) {
- answer = new SemanticQuestions(context);
- context.getCamelContextExtension().addContextPlugin(SemanticQuestions.class, answer);
+ answer = new SemanticEvaluations(context);
+ context.getCamelContextExtension().addContextPlugin(SemanticEvaluations.class, answer);
}
return answer;
}
@@ -74,22 +74,22 @@ public final class SemanticQuestions {
* are validated before publication, including unused evaluations. A validation failure preserves the previous
* source.
*/
- public synchronized void replace(String source, Map<String, SemanticQuestion> definitions) {
- Map<String, SemanticQuestion> replacement = new HashMap<>();
+ public synchronized void replace(String source, Map<String, SemanticEvaluation> definitions) {
+ Map<String, SemanticEvaluation> replacement = new HashMap<>();
sources.forEach((location, entries) -> {
if (!location.equals(source)) {
replacement.putAll(entries);
}
});
- definitions.forEach((name, question) -> {
- if (name == null || name.isBlank() || question == null) {
- throw new IllegalArgumentException("Semantic question requires a name and definition");
+ definitions.forEach((name, evaluation) -> {
+ if (name == null || name.isBlank() || evaluation == null) {
+ throw new IllegalArgumentException("Semantic evaluation requires a name and definition");
}
- if (replacement.putIfAbsent(name, question) != null) {
- throw new IllegalArgumentException("Duplicate semantic question: " + name);
+ if (replacement.putIfAbsent(name, evaluation) != null) {
+ throw new IllegalArgumentException("Duplicate semantic evaluation: " + name);
}
});
- Map<String, SemanticQuestion> previous = candidate.get();
+ Map<String, SemanticEvaluation> previous = candidate.get();
candidate.set(Map.copyOf(replacement));
try {
if (context.isStarted() && !definitions.isEmpty()) {
@@ -97,12 +97,12 @@ public final class SemanticQuestions {
}
validators.forEach((validator, names) -> {
if (!Collections.disjoint(names, definitions.keySet())) {
- Map<String, SemanticQuestion> selected = new LinkedHashMap<>();
+ Map<String, SemanticEvaluation> selected = new LinkedHashMap<>();
names.forEach(name -> {
- SemanticQuestion question = replacement.get(name);
+ SemanticEvaluation evaluation = replacement.get(name);
// Removed declarations remain removable; their existing expressions fail if evaluated again.
- if (question != null) {
- selected.put(name, question);
+ if (evaluation != null) {
+ selected.put(name, evaluation);
}
});
validator.accept(Collections.unmodifiableMap(selected));
@@ -121,15 +121,15 @@ public final class SemanticQuestions {
sources.put(source, Map.copyOf(definitions));
}
resources.remove(source);
- questions = Map.copyOf(replacement);
+ evaluations = Map.copyOf(replacement);
}
/** Track a route resource so deleted files can be discarded before development-mode reload. */
- public synchronized void replace(Resource source, Map<String, SemanticQuestion> definitions) {
+ public synchronized void replace(Resource source, Map<String, SemanticEvaluation> definitions) {
replace(source.getLocation(), source, definitions);
}
- synchronized void replace(String location, Resource source, Map<String, SemanticQuestion> definitions) {
+ synchronized void replace(String location, Resource source, Map<String, SemanticEvaluation> definitions) {
removeDeletedResources();
replace(location, definitions);
if (source != null && !definitions.isEmpty() && "file".equals(source.getScheme())) {
@@ -148,36 +148,36 @@ public final class SemanticQuestions {
}
}
- /** Whether any named questions have been registered. */
+ /** Whether any named evaluations have been registered. */
public boolean isEmpty() {
- return questions.isEmpty();
+ return evaluations.isEmpty();
}
- public SemanticQuestion get(String name) {
- SemanticQuestion question = snapshot().get(name);
- if (question == null) {
- throw new IllegalArgumentException("Unknown semantic question: " + name);
+ public SemanticEvaluation get(String name) {
+ SemanticEvaluation evaluation = snapshot().get(name);
+ if (evaluation == null) {
+ throw new IllegalArgumentException("Unknown semantic evaluation: " + name);
}
- return question;
+ return evaluation;
}
/** Resolve all requested names from one immutable snapshot, preserving reference order. */
- public Map<String, SemanticQuestion> get(List<String> names) {
- Map<String, SemanticQuestion> snapshot = snapshot();
- Map<String, SemanticQuestion> selected = new LinkedHashMap<>();
+ public Map<String, SemanticEvaluation> get(List<String> names) {
+ Map<String, SemanticEvaluation> snapshot = snapshot();
+ Map<String, SemanticEvaluation> selected = new LinkedHashMap<>();
for (String name : names) {
- SemanticQuestion question = snapshot.get(name);
- if (question == null) {
- throw new IllegalArgumentException("Unknown semantic question: " + name);
+ SemanticEvaluation evaluation = snapshot.get(name);
+ if (evaluation == null) {
+ throw new IllegalArgumentException("Unknown semantic evaluation: " + name);
}
- selected.put(name, question);
+ selected.put(name, evaluation);
}
return Collections.unmodifiableMap(selected);
}
- private Map<String, SemanticQuestion> snapshot() {
- Map<String, SemanticQuestion> validation = candidate.get();
- return validation != null ? validation : questions;
+ private Map<String, SemanticEvaluation> snapshot() {
+ Map<String, SemanticEvaluation> validation = candidate.get();
+ return validation != null ? validation : evaluations;
}
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationsBuilder.java
similarity index 64%
rename from components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java
rename to components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationsBuilder.java
index a9f2ce5f979b..f6168f8ccb20 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationsBuilder.java
@@ -28,24 +28,24 @@ import org.apache.camel.builder.RouteBuilderLifecycleStrategy;
import org.apache.camel.spi.Resource;
/** Fluent declarations for use inside an ordinary {@link RouteBuilder#configure()}. */
-public final class SemanticQuestionsBuilder {
- private static final String LIFECYCLE = SemanticQuestionsBuilder.class.getName();
+public final class SemanticEvaluationsBuilder {
+ private static final String LIFECYCLE = SemanticEvaluationsBuilder.class.getName();
private String expert;
private String state;
private final CamelContext context;
private final RouteBuilder builder;
private final Resource resource;
private final String source;
- private final Map<String, SemanticQuestionBuilder> questions = new LinkedHashMap<>();
+ private final Map<String, SemanticEvaluationBuilder> evaluations = new LinkedHashMap<>();
- private SemanticQuestionsBuilder(RouteBuilder builder) {
+ private SemanticEvaluationsBuilder(RouteBuilder builder) {
this(builder.getContext(), builder.getResource(), builder.getResource() == null
? "java:" + builder.getContext().getUuidGenerator().generateUuid()
: "java:" + builder.getResource().getLocation(),
builder);
}
- SemanticQuestionsBuilder(CamelContext context, Resource resource, String source, RouteBuilder builder) {
+ SemanticEvaluationsBuilder(CamelContext context, Resource resource, String source, RouteBuilder builder) {
this.context = context;
this.resource = resource;
this.source = source;
@@ -53,16 +53,16 @@ public final class SemanticQuestionsBuilder {
}
/** Start one group of declarations, then call {@link #register()} before using its references. */
- public static SemanticQuestionsBuilder semanticQuestions(RouteBuilder builder) {
- return new SemanticQuestionsBuilder(builder);
+ public static SemanticEvaluationsBuilder semanticEvaluations(RouteBuilder builder) {
+ return new SemanticEvaluationsBuilder(builder);
}
- public SemanticQuestionsBuilder expert(String expert) {
+ public SemanticEvaluationsBuilder expert(String expert) {
this.expert = expert;
return this;
}
- public SemanticQuestionsBuilder state(String state) {
+ public SemanticEvaluationsBuilder state(String state) {
this.state = state;
return this;
}
@@ -75,35 +75,31 @@ public final class SemanticQuestionsBuilder {
return state;
}
- public SemanticQuestionBuilder evaluation(String name) {
- return question(name);
- }
-
- /** Add a named question. Names must be unique across the context. */
- public SemanticQuestionBuilder question(String name) {
+ /** Add a named evaluation. Names must be unique across the context. */
+ public SemanticEvaluationBuilder evaluation(String name) {
if (name == null || name.isBlank()) {
- throw new IllegalArgumentException("Semantic question requires a nonblank name");
+ throw new IllegalArgumentException("Semantic evaluation requires a nonblank name");
}
- SemanticQuestionBuilder question = new SemanticQuestionBuilder(this);
- if (questions.putIfAbsent(name, question) != null) {
- throw new IllegalArgumentException("Duplicate semantic question: " + name);
+ SemanticEvaluationBuilder evaluation = new SemanticEvaluationBuilder(this);
+ if (evaluations.putIfAbsent(name, evaluation) != null) {
+ throw new IllegalArgumentException("Duplicate semantic evaluation: " + name);
}
- return question;
+ return evaluation;
}
- /** Validate all declarations and atomically replace this source's questions. An empty group removes them. */
+ /** Validate all declarations and atomically replace this source's evaluations. An empty group removes them. */
public void register() {
- Map<String, SemanticQuestion> definitions = new LinkedHashMap<>();
- questions.forEach((name, question) -> {
+ Map<String, SemanticEvaluation> definitions = new LinkedHashMap<>();
+ evaluations.forEach((name, evaluation) -> {
try {
- definitions.put(name, question.build(context));
+ definitions.put(name, evaluation.build(context));
} catch (IllegalArgumentException e) {
- String expert = question.getExpert() != null ? question.getExpert() : "default/automatic";
+ String expert = evaluation.getExpert() != null ? evaluation.getExpert() : "default/automatic";
throw new IllegalArgumentException(
- "Invalid semantic question '" + name + "': " + e.getMessage() + " (expert '" + expert + "')", e);
+ "Invalid semantic evaluation '" + name + "': " + e.getMessage() + " (expert '" + expert + "')", e);
}
});
- SemanticQuestions registry = SemanticQuestions.get(context);
+ SemanticEvaluations registry = SemanticEvaluations.get(context);
synchronized (registry) {
registry.replace(source, resource, definitions);
DeclarationsLifecycle lifecycle = context.getRegistry().lookupByNameAndType(LIFECYCLE, DeclarationsLifecycle.class);
@@ -119,19 +115,19 @@ public final class SemanticQuestionsBuilder {
// A resource may remove the helper entirely on reload. The existing builder lifecycle detects that case.
private static final class DeclarationsLifecycle implements RouteBuilderLifecycleStrategy {
- private final SemanticQuestions questions;
+ private final SemanticEvaluations evaluations;
private final Set<RouteBuilder> registered = Collections.newSetFromMap(new WeakHashMap<>());
- private DeclarationsLifecycle(SemanticQuestions questions) {
- this.questions = questions;
+ private DeclarationsLifecycle(SemanticEvaluations evaluations) {
+ this.evaluations = evaluations;
}
@Override
public void afterConfigure(RouteBuilder builder) {
- synchronized (questions) {
- questions.removeDeletedResources();
+ synchronized (evaluations) {
+ evaluations.removeDeletedResources();
if (!registered.remove(builder) && builder.getResource() != null) {
- questions.remove("java:" + builder.getResource().getLocation());
+ evaluations.remove("java:" + builder.getResource().getLocation());
}
}
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java
index 658018d0fbfb..de0f7cd71750 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java
@@ -35,9 +35,10 @@ public class SemanticReloadPlugin implements ContextServicePlugin {
lifecycle = new LifecycleStrategySupport() {
@Override
public void onContextStarting(CamelContext camelContext) {
- SemanticQuestions questions = camelContext.getCamelContextExtension().getContextPlugin(SemanticQuestions.class);
- if (questions != null) {
- questions.validate();
+ SemanticEvaluations evaluations
+ = camelContext.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class);
+ if (evaluations != null) {
+ evaluations.validate();
}
}
@@ -96,9 +97,9 @@ public class SemanticReloadPlugin implements ContextServicePlugin {
if (xmlLoader != null) {
xmlLoader.resetLoaderDiscovery();
}
- SemanticQuestions questions = context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class);
- if (questions != null) {
- questions.removeDeletedResources();
+ SemanticEvaluations evaluations = context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class);
+ if (evaluations != null) {
+ evaluations.removeDeletedResources();
}
}
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java
index 6e2c4a87d67d..6bfdb281c567 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java
@@ -91,10 +91,10 @@ final class SemanticXmlLoader extends RoutesBuilderLoaderSupport {
builder.addLifecycleInterceptor(new RouteBuilderLifecycleStrategy() {
@Override
public void afterConfigure(RouteBuilder routeBuilder) {
- SemanticQuestions questions
- = getCamelContext().getCamelContextExtension().getContextPlugin(SemanticQuestions.class);
- if (questions != null) {
- questions.remove(resource.getLocation());
+ SemanticEvaluations evaluations
+ = getCamelContext().getCamelContextExtension().getContextPlugin(SemanticEvaluations.class);
+ if (evaluations != null) {
+ evaluations.remove(resource.getLocation());
}
}
});
@@ -161,7 +161,7 @@ final class SemanticXmlLoader extends RoutesBuilderLoaderSupport {
if ("semantic".equals(reader.getLocalName())) {
return true;
}
- // Only direct children can declare questions. Consume nested route content without
+ // Only direct children can declare evaluations. Consume nested route content without
// inspecting names or namespaces, then continue looking for declarations after routes.
skipSubtree(reader);
depth--;
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java
index e5d7fae3d65b..d6553ac0804d 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java
@@ -85,11 +85,11 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
if (!NAMESPACES.contains(namespace)) {
throw new IllegalArgumentException("Unsupported semantic XML namespace: " + namespace);
}
- SemanticQuestionsBuilder questions
- = new SemanticQuestionsBuilder(getCamelContext(), resource, resource.getLocation(), null);
+ SemanticEvaluationsBuilder evaluations
+ = new SemanticEvaluationsBuilder(getCamelContext(), resource, resource.getLocation(), null);
RoutesDefinition routes = new RoutesDefinition();
if ("semantic".equals(root.getLocalName())) {
- declarations(root, questions);
+ declarations(root, evaluations);
} else if ("routes".equals(root.getLocalName())) {
boolean found = false;
for (Element child : children(root)) {
@@ -98,7 +98,7 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
throw new IllegalArgumentException("Only one semantic declaration block is allowed");
}
found = true;
- declarations(child, questions);
+ declarations(child, evaluations);
} else if (!"route".equals(child.getLocalName())) {
throw new IllegalArgumentException("Unexpected element in routes: " + child.getTagName());
}
@@ -110,8 +110,8 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
} else {
throw new IllegalArgumentException("Expected semantic or routes root element");
}
- // Parse the complete document and validate every question before publishing any definitions.
- questions.register();
+ // Parse the complete document and validate every evaluation before publishing any definitions.
+ evaluations.register();
return routes;
}
@@ -139,58 +139,58 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
}
}
- private static void declarations(Element semantic, SemanticQuestionsBuilder questions) {
+ private void declarations(Element semantic, SemanticEvaluationsBuilder evaluations) {
attributes(semantic, Set.of("expert", "state"));
if (semantic.hasAttribute("expert")) {
- questions.expert(semantic.getAttribute("expert"));
+ evaluations.expert(semantic.getAttribute("expert"));
}
if (semantic.hasAttribute("state")) {
- questions.state(semantic.getAttribute("state"));
+ evaluations.state(semantic.getAttribute("state"));
}
for (Element element : children(semantic)) {
- if (!"question".equals(element.getLocalName()) && !"evaluation".equals(element.getLocalName())) {
+ if (!"evaluation".equals(element.getLocalName())) {
throw new IllegalArgumentException("Unexpected semantic element: " + element.getTagName());
}
try {
- question(element, questions);
+ evaluation(element, evaluations);
} catch (IllegalArgumentException e) {
String expert = element.hasAttribute("expert") ? element.getAttribute("expert")
- : questions.getExpert() != null ? questions.getExpert() : "default/automatic";
+ : evaluations.getExpert() != null ? evaluations.getExpert() : "default/automatic";
throw new IllegalArgumentException(
- "Invalid semantic question '" + element.getAttribute("name") + "': " + e.getMessage()
+ "Invalid semantic evaluation '" + element.getAttribute("name") + "': " + e.getMessage()
+ " (expert '" + expert + "')",
e);
}
}
}
- private static void question(Element element, SemanticQuestionsBuilder questions) {
+ private void evaluation(Element element, SemanticEvaluationsBuilder evaluations) {
attributes(element,
Set.of("name", "type", "operation", "expert", "state", "threshold", "uncertainty", "uncertaintyPolicy"));
- SemanticQuestionBuilder question = questions.question(element.getAttribute("name"));
+ SemanticEvaluationBuilder evaluation = evaluations.evaluation(element.getAttribute("name"));
if (element.hasAttribute("type") && element.hasAttribute("operation")) {
throw new IllegalArgumentException("Specify exactly one operation or type");
}
if (element.hasAttribute("operation")) {
- question.operation(element.getAttribute("operation"));
+ evaluation.operation(element.getAttribute("operation"));
}
if (element.hasAttribute("type")) {
- question.type(element.getAttribute("type"));
+ evaluation.type(element.getAttribute("type"));
}
if (element.hasAttribute("expert")) {
- question.expert(element.getAttribute("expert"));
+ evaluation.expert(element.getAttribute("expert"));
}
if (element.hasAttribute("state")) {
- question.state(element.getAttribute("state"));
+ evaluation.state(element.getAttribute("state"));
}
if (element.hasAttribute("threshold")) {
- question.threshold(element.getAttribute("threshold"));
+ evaluation.threshold(element.getAttribute("threshold"));
}
if (element.hasAttribute("uncertainty")) {
- question.uncertainty(element.getAttribute("uncertainty"));
+ evaluation.uncertainty(element.getAttribute("uncertainty"));
}
if (element.hasAttribute("uncertaintyPolicy")) {
- question.uncertaintyPolicy(element.getAttribute("uncertaintyPolicy"));
+ evaluation.uncertaintyPolicy(element.getAttribute("uncertaintyPolicy"));
}
boolean instructions = false;
for (Element child : children(element)) {
@@ -202,34 +202,34 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
throw new IllegalArgumentException("Expected parameter element");
}
attributes(parameter, Set.of("name"));
- question.parameter(parameter.getAttribute("name"), parameterValue(parameter));
+ evaluation.parameter(parameter.getAttribute("name"), parameterValue(parameter));
}
}
case "instructions" -> {
if (instructions) {
- throw new IllegalArgumentException("Duplicate instructions for semantic question");
+ throw new IllegalArgumentException("Duplicate instructions for semantic evaluation");
}
instructions = true;
attributes(child, Set.of());
- question.instructions(text(child));
+ evaluation.instructions(text(child));
}
case "criterion" -> {
attributes(child, Set.of("key", "value"));
if (!children(child).isEmpty()) {
throw new IllegalArgumentException("Semantic criterion must not contain elements");
}
- question.criterion(child.getAttribute("key"), child.getAttribute("value"));
+ evaluation.criterion(child.getAttribute("key"), child.getAttribute("value"));
}
case "level" -> {
attributes(child, Set.of());
- question.level(text(child));
+ evaluation.level(text(child));
}
- default -> throw new IllegalArgumentException("Unexpected question element: " + child.getTagName());
+ default -> throw new IllegalArgumentException("Unexpected evaluation element: " + child.getTagName());
}
}
}
- private static Object parameterValue(Element parent) {
+ private Object parameterValue(Element parent) {
List<Element> values = children(parent);
if (values.size() != 1) {
throw new IllegalArgumentException("Parameter or map entry requires exactly one typed value");
@@ -237,7 +237,7 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
return value(values.get(0));
}
- private static Object value(Element element) {
+ private Object value(Element element) {
attributes(element, Set.of());
return switch (element.getLocalName()) {
case "string" -> text(element);
@@ -249,7 +249,7 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
}
case "number" -> {
try {
- yield new BigDecimal(text(element).strip());
+ yield new BigDecimal(getCamelContext().resolvePropertyPlaceholders(text(element)).strip());
} catch (NumberFormatException invalid) {
throw new IllegalArgumentException("Invalid numeric parameter");
}
@@ -261,7 +261,7 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
}
yield Boolean.valueOf(text);
}
- case "list" -> children(element).stream().map(SemanticXmlRoutesBuilderLoader::value).toList();
+ case "list" -> children(element).stream().map(this::value).toList();
case "map" -> {
Map<String, Object> map = new LinkedHashMap<>();
for (Element entry : children(element)) {
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java
index cef45e978038..c54bd48f6224 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java
@@ -30,9 +30,9 @@ import org.apache.camel.dsl.yaml.common.YamlDeserializerResolver;
import org.apache.camel.dsl.yaml.common.YamlDeserializerSupport;
import org.apache.camel.dsl.yaml.common.exception.InvalidNodeTypeException;
import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
-import org.apache.camel.semantic.SemanticQuestion;
-import org.apache.camel.semantic.SemanticQuestionBuilder;
-import org.apache.camel.semantic.SemanticQuestions;
+import org.apache.camel.semantic.SemanticEvaluation;
+import org.apache.camel.semantic.SemanticEvaluationBuilder;
+import org.apache.camel.semantic.SemanticEvaluations;
import org.apache.camel.spi.CamelContextCustomizer;
import org.apache.camel.spi.annotations.YamlIn;
import org.apache.camel.spi.annotations.YamlProperty;
@@ -49,14 +49,14 @@ import org.snakeyaml.engine.v2.nodes.Tag;
/** Named semantic declarations are installed in a resource-wide pass before route references are resolved. */
@YamlIn
@YamlType(nodes = "semantic", properties = {
- @YamlProperty(name = "__oneOf",
- type = "object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionBlockSchema",
- oneOf = "declarations", required = true),
- @YamlProperty(name = "__oneOf",
- type = "object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationBlockSchema",
- oneOf = "declarations", required = true)
+ @YamlProperty(name = "expert", type = "string"),
+ @YamlProperty(name = "state", type = "string"),
+ @YamlProperty(name = "evaluation",
+ type = "map:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema",
+ required = true)
})
public class SemanticDefinitionDeserializer extends YamlDeserializerSupport implements ConstructNode, YamlDeserializerResolver {
+ private static final Tag NUMBER = new Tag("!number");
private static final Set<String> FIELDS
= Set.of("type", "operation", "expert", "parameters", "instructions", "state", "criteria", "threshold",
"uncertainty", "uncertaintyPolicy");
@@ -79,54 +79,53 @@ public class SemanticDefinitionDeserializer extends YamlDeserializerSupport impl
if (!(root instanceof SequenceNode sequence)) {
return;
}
- Map<String, SemanticQuestion> definitions = new LinkedHashMap<>();
+ Map<String, SemanticEvaluation> definitions = new LinkedHashMap<>();
for (Node node : sequence.getValue()) {
if (!(node instanceof MappingNode mapping)) {
- // Leave malformed entries to the route loader without replacing the resource's questions.
+ // Leave malformed entries to the route loader without replacing the resource's evaluations.
return;
}
for (NodeTuple tuple : mapping.getValue()) {
if ("semantic".equals(asText(tuple.getKeyNode()))) {
- read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, question) -> {
- if (definitions.putIfAbsent(name, question) != null) {
+ read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, evaluation) -> {
+ if (definitions.putIfAbsent(name, evaluation) != null) {
throw new YamlDeserializationException(
- tuple.getValueNode(), "Duplicate semantic question: " + name);
+ tuple.getValueNode(), "Duplicate semantic evaluation: " + name);
}
});
}
}
}
CamelContext context = dc.getCamelContext();
- SemanticQuestions questions = definitions.isEmpty()
- ? context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)
- : SemanticQuestions.get(context);
- if (questions != null) {
+ SemanticEvaluations evaluations = definitions.isEmpty()
+ ? context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class)
+ : SemanticEvaluations.get(context);
+ if (evaluations != null) {
try {
- questions.replace(dc.getResource(), definitions);
+ evaluations.replace(dc.getResource(), definitions);
} catch (IllegalArgumentException e) {
throw new YamlDeserializationException(root, e.getMessage(), e);
}
}
}
- private static Map<String, SemanticQuestion> read(CamelContext context, Node node) {
+ private static Map<String, SemanticEvaluation> read(CamelContext context, Node node) {
Map<String, Node> semantic = fields(node, "semantic declaration");
for (String field : semantic.keySet()) {
- if (!Set.of("question", "evaluation", "expert", "state").contains(field)) {
+ if (!Set.of("evaluation", "expert", "state").contains(field)) {
throw new YamlDeserializationException(
semantic.get(field), "Unknown property '" + field + "' in semantic declaration");
}
}
- if (semantic.containsKey("question") == semantic.containsKey("evaluation")) {
- throw new YamlDeserializationException(node, "Semantic declaration requires exactly one of question or evaluation");
+ if (!semantic.containsKey("evaluation")) {
+ throw new YamlDeserializationException(node, "Semantic declaration requires evaluation");
}
- String declaration = semantic.containsKey("evaluation") ? "evaluation" : "question";
- Map<String, SemanticQuestion> result = new LinkedHashMap<>();
- fields(semantic.get(declaration), "semantic evaluations").forEach((name, definition) -> {
+ Map<String, SemanticEvaluation> result = new LinkedHashMap<>();
+ fields(semantic.get("evaluation"), "semantic evaluations").forEach((name, definition) -> {
if (name.isBlank()) {
- throw new YamlDeserializationException(definition, "Semantic question requires a nonblank name");
+ throw new YamlDeserializationException(definition, "Semantic evaluation requires a nonblank name");
}
- Map<String, Node> values = fields(definition, "semantic question '" + name + "'");
+ Map<String, Node> values = fields(definition, "semantic evaluation '" + name + "'");
for (String common : List.of("expert", "state")) {
if (!values.containsKey(common) && semantic.containsKey(common)) {
values.put(common, semantic.get(common));
@@ -138,10 +137,10 @@ public class SemanticDefinitionDeserializer extends YamlDeserializerSupport impl
expert = "invalid expert reference";
expert = asText(values.get("expert"));
}
- result.put(name, readQuestion(context, name, expert, definition, values));
+ result.put(name, readEvaluation(context, name, expert, definition, values));
} catch (IllegalArgumentException | InvalidNodeTypeException e) {
throw new YamlDeserializationException(
- definition, "Invalid semantic question '" + name + "': " + e.getMessage()
+ definition, "Invalid semantic evaluation '" + name + "': " + e.getMessage()
+ " (expert '" + expert + "')",
e);
}
@@ -149,9 +148,9 @@ public class SemanticDefinitionDeserializer extends YamlDeserializerSupport impl
return result;
}
- private static SemanticQuestion readQuestion(
+ private static SemanticEvaluation readEvaluation(
CamelContext context, String name, String expert, Node definition, Map<String, Node> values) {
- String description = "semantic question '" + name + "'";
+ String description = "semantic evaluation '" + name + "'";
String expertContext = " (expert '" + expert + "')";
values.forEach((field, value) -> {
if (!FIELDS.contains(field)) {
@@ -166,11 +165,11 @@ public class SemanticDefinitionDeserializer extends YamlDeserializerSupport impl
String operation = values.containsKey("operation")
? asText(values.get("operation"))
: asText(values.get("type")).toLowerCase(Locale.ROOT);
- SemanticQuestionBuilder builder = new SemanticQuestionBuilder().operation(operation)
+ SemanticEvaluationBuilder builder = new SemanticEvaluationBuilder().operation(operation)
.expert(asText(values.get("expert"))).state(asText(values.get("state")));
if (values.containsKey("parameters")) {
fields(values.get("parameters"), "evaluation parameters")
- .forEach((key, value) -> builder.parameter(key, value(value)));
+ .forEach((key, value) -> builder.parameter(key, value(context, value)));
}
for (String field : List.of("instructions", "criteria", "threshold", "uncertainty", "uncertaintyPolicy")) {
Node node = values.get(field);
@@ -198,7 +197,7 @@ public class SemanticDefinitionDeserializer extends YamlDeserializerSupport impl
builder.parameter(field,
sequence.getValue().stream().map(SemanticDefinitionDeserializer::asText).toList());
} else {
- builder.parameter(field, value(node));
+ builder.parameter(field, value(context, node));
}
}
default -> throw new IllegalStateException(field);
@@ -207,18 +206,18 @@ public class SemanticDefinitionDeserializer extends YamlDeserializerSupport impl
return builder.build(context);
}
- private static Object value(Node node) {
+ private static Object value(CamelContext context, Node node) {
if (node instanceof MappingNode) {
Map<String, Object> result = new LinkedHashMap<>();
- fields(node, "parameter map").forEach((key, child) -> result.put(key, value(child)));
+ fields(node, "parameter map").forEach((key, child) -> result.put(key, value(context, child)));
return result;
}
if (node instanceof SequenceNode sequence) {
- return sequence.getValue().stream().map(SemanticDefinitionDeserializer::value).toList();
+ return sequence.getValue().stream().map(child -> value(context, child)).toList();
}
- if (Tag.FLOAT.equals(node.getTag())) {
+ if (Tag.FLOAT.equals(node.getTag()) || NUMBER.equals(node.getTag())) {
try {
- return new BigDecimal(asText(node));
+ return new BigDecimal(context.resolvePropertyPlaceholders(asText(node)));
} catch (NumberFormatException invalid) {
throw new YamlDeserializationException(node, "Invalid numeric parameter");
}
@@ -237,29 +236,6 @@ public class SemanticDefinitionDeserializer extends YamlDeserializerSupport impl
return result;
}
- @YamlType(properties = {
- @YamlProperty(name = "expert", type = "string"),
- @YamlProperty(name = "state", type = "string")
- })
- public static class BlockSchema {
- }
-
- @YamlType(properties = {
- @YamlProperty(name = "question",
- type = "map:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema",
- required = true)
- })
- public static class QuestionBlockSchema extends BlockSchema {
- }
-
- @YamlType(properties = {
- @YamlProperty(name = "evaluation",
- type = "map:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema",
- required = true)
- })
- public static class EvaluationBlockSchema extends BlockSchema {
- }
-
@YamlType(properties = {
@YamlProperty(name = "__oneOf",
type = "object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$MapCriteriaSchema",
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/FixedSemanticExpert.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/FixedSemanticExpert.java
index f44ee11a45d2..435574d5ce98 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/FixedSemanticExpert.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/FixedSemanticExpert.java
@@ -50,26 +50,26 @@ class FixedSemanticExpert implements SemanticAdapter {
final List<Object> states = new ArrayList<>();
@Override
- public void validate(SemanticQuestion question) {
- SemanticCapabilities.from(getClass()).validate(question);
+ public void validate(SemanticEvaluation evaluation) {
+ SemanticCapabilities.from(getClass()).validate(evaluation);
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) {
calls++;
states.add(state);
if (fail) {
throw new IllegalStateException("provider unavailable");
}
- return TestSemanticAdapter.applyPolicy(question,
+ return TestSemanticAdapter.applyPolicy(evaluation,
new SemanticResult(null, probability, null, null, Map.of("provider", "fixed")));
}
@Override
- public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticQuestion> questions, Object state)
+ public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticEvaluation> evaluations, Object state)
throws Exception {
- batches.add(List.copyOf(questions.keySet()));
- return SemanticAdapter.super.evaluateBatch(questions, state);
+ batches.add(List.copyOf(evaluations.keySet()));
+ return SemanticAdapter.super.evaluateBatch(evaluations, state);
}
@Override
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticBatchTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticBatchTest.java
index efb6e8412b7f..d66003f1e18c 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticBatchTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticBatchTest.java
@@ -55,10 +55,10 @@ class SemanticBatchTest {
context.start();
exchange = new DefaultExchange(context);
exchange.getMessage().setBody("original");
- SemanticQuestions.get(context).replace("test", Map.of(
- "urgent", question(SemanticQuestion.Type.BOOLEAN, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL),
- "department", question(SemanticQuestion.Type.CHOICE, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL),
- "priority", question(SemanticQuestion.Type.SCORE, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL)));
+ SemanticEvaluations.get(context).replace("test", Map.of(
+ "urgent", evaluation("boolean", null, 0.5, 0, "fail"),
+ "department", evaluation("choice", null, 0.5, 0, "fail"),
+ "priority", evaluation("score", null, 0.5, 0, "fail")));
}
@AfterEach
@@ -66,27 +66,29 @@ class SemanticBatchTest {
context.stop();
}
- private static SemanticQuestion question(
- SemanticQuestion.Type type, String state, double threshold,
- double uncertainty, SemanticQuestion.UncertaintyPolicy policy) {
- return new SemanticQuestion(
- type, "Classify", state,
- type == SemanticQuestion.Type.CHOICE ? Map.of("billing", "Payments", "technical", "Bugs") : Map.of(),
- type == SemanticQuestion.Type.SCORE ? List.of("low", "medium", "high") : List.of(),
- threshold, uncertainty, policy);
+ private static SemanticEvaluation evaluation(
+ String type, String state, double threshold,
+ double uncertainty, String policy) {
+ return new SemanticEvaluation(type, null, state, switch (type) {
+ case "boolean" -> Map.of("instructions", "Classify", "threshold", threshold,
+ "uncertainty", uncertainty, "uncertaintyPolicy", policy);
+ case "choice" -> Map.of("instructions", "Classify", "criteria", Map.of("billing", "Payments", "technical", "Bugs"));
+ case "score" -> Map.of("instructions", "Classify", "criteria", List.of("low", "medium", "high"));
+ default -> throw new IllegalArgumentException("Unknown fixture operation");
+ });
}
@Test
- void existingAdapterEvaluatesMixedQuestionsSequentiallyAndRetainsDetails() {
+ void existingAdapterEvaluatesMixedEvaluationsSequentiallyAndRetainsDetails() {
Expression expression = language.createExpression("refs: urgent, department, priority ");
assertThat(adapter.calls).isEmpty();
assertThat(expression.evaluate(exchange, Map.class))
.containsAllEntriesOf(Map.of("urgent", true, "department", "billing", "priority", 1.2));
Map<String, Object> decisions = expression.evaluate(exchange, Map.class);
assertThat(new ArrayList<>(decisions.keySet())).containsExactly("urgent", "department", "priority");
- assertThat(adapter.calls).containsExactly(SemanticQuestion.Type.BOOLEAN, SemanticQuestion.Type.CHOICE,
- SemanticQuestion.Type.SCORE, SemanticQuestion.Type.BOOLEAN, SemanticQuestion.Type.CHOICE,
- SemanticQuestion.Type.SCORE);
+ assertThat(adapter.calls).containsExactly("boolean", "choice",
+ "score", "boolean", "choice",
+ "score");
assertThat(adapter.states).containsOnly("original");
assertThat(exchange.getMessage().getBody()).isEqualTo("original");
Map<?, ?> details = exchange.getProperty(SemanticLanguage.RESULTS, Map.class);
@@ -111,13 +113,13 @@ class SemanticBatchTest {
@Test
void singleReferenceStillAcceptsNamesContainingCommas() {
- SemanticQuestions.get(context).replace("comma", Map.of("a,b", SemanticQuestions.get(context).get("urgent")));
+ SemanticEvaluations.get(context).replace("comma", Map.of("a,b", SemanticEvaluations.get(context).get("urgent")));
assertThat(language.createExpression("ref:a,b").evaluate(exchange, Boolean.class)).isTrue();
assertThatThrownBy(() -> language.createExpression("refs:a,b")).hasMessageContaining("Unknown");
}
@Test
- void batchesCannotBePredicatesEvenWithOneBooleanQuestion() {
+ void batchesCannotBePredicatesEvenWithOneBooleanEvaluation() {
assertThatThrownBy(() -> language.validatePredicate("refs:urgent")).hasMessageContaining("cannot be predicates");
assertThatThrownBy(() -> language.createPredicate("refs:urgent")).hasMessageContaining("cannot be predicates");
Expression expression = language.createExpression("refs:urgent");
@@ -152,9 +154,9 @@ class SemanticBatchTest {
properties.setProperty("selected", "${header.selected}");
context.getPropertiesComponent().setInitialProperties(properties);
language.setDefaultState("{{selected}}");
- SemanticQuestions.get(context).replace("explicit", Map.of("other",
- question(SemanticQuestion.Type.BOOLEAN, "${header.selected}", 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL)));
+ SemanticEvaluations.get(context).replace("explicit", Map.of("other",
+ evaluation("boolean", "${header.selected}", 0.5, 0,
+ "fail")));
exchange.getMessage().setHeader("selected", Map.of("text", "invoice"));
Expression expression = language.createExpression("refs:urgent,other");
expression.evaluate(exchange, Map.class);
@@ -166,7 +168,7 @@ class SemanticBatchTest {
}
@Test
- void batchUsesTheSameEmptyPlaceholderStateAsSingleQuestions() {
+ void batchUsesTheSameEmptyPlaceholderStateAsSingleEvaluations() {
Properties properties = new Properties();
properties.setProperty("selected", "");
context.getPropertiesComponent().setInitialProperties(properties);
@@ -178,15 +180,15 @@ class SemanticBatchTest {
@Test
void incompatibleSelectorsAndUnsupportedCapabilitiesFailBeforeInference() {
- SemanticQuestions.get(context).replace("other", Map.of("other",
- question(SemanticQuestion.Type.BOOLEAN, "${header.other}", 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL)));
+ SemanticEvaluations.get(context).replace("other", Map.of("other",
+ evaluation("boolean", "${header.other}", 0.5, 0, "fail")));
assertThatThrownBy(() -> language.createExpression("refs:urgent,other"))
.hasMessageContaining("same effective state selector");
context.getRegistry().unbind("adapter");
context.getRegistry().bind("adapter", new RecordingAdapter() {
@Override
- public void validate(SemanticQuestion question) {
- if (question.getType() == SemanticQuestion.Type.SCORE) {
+ public void validate(SemanticEvaluation evaluation) {
+ if ("score".equals(evaluation.getOperation())) {
throw new IllegalArgumentException("Unsupported score");
}
}
@@ -200,17 +202,17 @@ class SemanticBatchTest {
}
@Test
- void eachQuestionKeepsItsDecisionPolicyAndFailureClearsAllDiagnostics() {
- SemanticQuestion negative
- = question(SemanticQuestion.Type.BOOLEAN, null, 0.95, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
- SemanticQuestion uncertain
- = question(SemanticQuestion.Type.BOOLEAN, null, 0.9, 0.05, SemanticQuestion.UncertaintyPolicy.NON_MATCH);
- SemanticQuestions.get(context).replace("policy", Map.of("negative", negative, "uncertain", uncertain));
+ void eachEvaluationKeepsItsDecisionPolicyAndFailureClearsAllDiagnostics() {
+ SemanticEvaluation negative
+ = evaluation("boolean", null, 0.95, 0, "fail");
+ SemanticEvaluation uncertain
+ = evaluation("boolean", null, 0.9, 0.05, "non-match");
+ SemanticEvaluations.get(context).replace("policy", Map.of("negative", negative, "uncertain", uncertain));
Expression expression = language.createExpression("refs:urgent,negative,uncertain");
assertThat(expression.evaluate(exchange, Map.class)).containsEntry("urgent", true)
.containsEntry("negative", false).containsEntry("uncertain", false);
- SemanticQuestions.get(context).replace("policy", Map.of("negative", negative, "uncertain",
- question(SemanticQuestion.Type.BOOLEAN, null, 0.9, 0.05, SemanticQuestion.UncertaintyPolicy.FAIL)));
+ SemanticEvaluations.get(context).replace("policy", Map.of("negative", negative, "uncertain",
+ evaluation("boolean", null, 0.9, 0.05, "fail")));
exchange.setProperty(SemanticLanguage.RESULT, "stale");
assertThatThrownBy(() -> expression.evaluate(exchange, Map.class)).hasMessageContaining("uncertain");
assertThat(exchange.getProperty(SemanticLanguage.RESULTS)).isNull();
@@ -223,19 +225,19 @@ class SemanticBatchTest {
context.getRegistry().unbind("adapter");
context.getRegistry().bind("adapter", new RecordingAdapter() {
@Override
- public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticQuestion> questions, Object state) {
+ public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticEvaluation> evaluations, Object state) {
if (failure.equals("failure")) {
throw new IllegalStateException("provider failed");
}
Map<String, SemanticResult> results = new LinkedHashMap<>();
- results.put("urgent", result(SemanticQuestion.Type.BOOLEAN));
+ results.put("urgent", result("boolean"));
if (!failure.equals("missing")) {
results.put("department", failure.equals("null") ? null
: failure.equals("invalid") ? new SemanticResult("undeclared", null, null, null, null)
- : result(SemanticQuestion.Type.CHOICE));
+ : result("choice"));
}
if (failure.equals("extra")) {
- results.put("extra", result(SemanticQuestion.Type.BOOLEAN));
+ results.put("extra", result("boolean"));
}
return results;
}
@@ -249,54 +251,54 @@ class SemanticBatchTest {
}
@Test
- void reloadDuringValidationOrEvaluationCannotMixQuestionDefinitions() {
- SemanticQuestion old = SemanticQuestions.get(context).get("urgent");
- SemanticQuestion updated
- = question(SemanticQuestion.Type.BOOLEAN, null, 0.95, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
- SemanticQuestions questions = SemanticQuestions.get(context);
- questions.replace("test", Map.of("first", old, "second", old));
+ void reloadDuringValidationOrEvaluationCannotMixEvaluationDefinitions() {
+ SemanticEvaluation old = SemanticEvaluations.get(context).get("urgent");
+ SemanticEvaluation updated
+ = evaluation("boolean", null, 0.95, 0, "fail");
+ SemanticEvaluations evaluations = SemanticEvaluations.get(context);
+ evaluations.replace("test", Map.of("first", old, "second", old));
AtomicInteger validations = new AtomicInteger();
context.getRegistry().unbind("adapter");
context.getRegistry().bind("adapter", new RecordingAdapter() {
@Override
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
if (validations.incrementAndGet() == 1) {
- questions.replace("test", Map.of("first", updated, "second", updated));
+ evaluations.replace("test", Map.of("first", updated, "second", updated));
}
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) throws Exception {
- questions.replace("test", Map.of("first", old, "second", old));
- return super.evaluate(question, state);
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) throws Exception {
+ evaluations.replace("test", Map.of("first", old, "second", old));
+ return super.evaluate(evaluation, state);
}
});
Expression expression = language.createExpression("refs:first,second");
assertThat(expression.evaluate(exchange, Map.class)).containsEntry("first", false).containsEntry("second", false);
assertThat(expression.evaluate(exchange, Map.class)).containsEntry("first", true).containsEntry("second", true);
- questions.replace("test", Map.of("first", old));
+ evaluations.replace("test", Map.of("first", old));
assertThatThrownBy(() -> expression.evaluate(exchange, Map.class))
- .hasMessageContaining("Unknown semantic question: second");
+ .hasMessageContaining("Unknown semantic evaluation: second");
assertThat(exchange.getProperty(SemanticLanguage.RESULTS)).isNull();
- assertThatThrownBy(() -> questions.replace("test", Map.of("first", old, "second",
- question(SemanticQuestion.Type.BOOLEAN, "${header.changed}", 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL))))
+ assertThatThrownBy(() -> evaluations.replace("test", Map.of("first", old, "second",
+ evaluation("boolean", "${header.changed}", 0.5, 0, "fail"))))
.hasMessageContaining("same effective state selector");
- assertThat(questions.get("first")).isSameAs(old);
- assertThatThrownBy(() -> questions.get("second")).hasMessageContaining("Unknown semantic question");
- assertThatThrownBy(() -> expression.evaluate(exchange, Map.class)).hasMessageContaining("Unknown semantic question");
+ assertThat(evaluations.get("first")).isSameAs(old);
+ assertThatThrownBy(() -> evaluations.get("second")).hasMessageContaining("Unknown semantic evaluation");
+ assertThatThrownBy(() -> expression.evaluate(exchange, Map.class)).hasMessageContaining("Unknown semantic evaluation");
}
@Test
void reloadChecksBatchSelectorsAcrossResourcesAndRetainsThePreviousSnapshot() {
- SemanticQuestions questions = SemanticQuestions.get(context);
- SemanticQuestion original = questions.get("urgent");
- questions.replace("other", Map.of("other", original));
+ SemanticEvaluations evaluations = SemanticEvaluations.get(context);
+ SemanticEvaluation original = evaluations.get("urgent");
+ evaluations.replace("other", Map.of("other", original));
Expression expression = language.createExpression("refs:urgent,other");
- assertThatThrownBy(() -> questions.replace("other", Map.of("other",
- question(SemanticQuestion.Type.BOOLEAN, "${header.changed}", 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL))))
+ assertThatThrownBy(() -> evaluations.replace("other", Map.of("other",
+ evaluation("boolean", "${header.changed}", 0.5, 0, "fail"))))
.hasMessageContaining("same effective state selector");
assertThat(adapter.calls).isEmpty();
- assertThat(questions.get("other")).isSameAs(original);
+ assertThat(evaluations.get("other")).isSameAs(original);
assertThat(expression.evaluate(exchange, Map.class)).containsEntry("urgent", true).containsEntry("other", true);
}
@@ -306,7 +308,7 @@ class SemanticBatchTest {
context.getRegistry().unbind("adapter");
context.getRegistry().bind("adapter", new RecordingAdapter() {
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) throws Exception {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) throws Exception {
invocations.incrementAndGet();
throw new InterruptedException("interrupted");
}
@@ -322,29 +324,29 @@ class SemanticBatchTest {
}
}
- private static SemanticResult result(SemanticQuestion.Type type) {
+ private static SemanticResult result(String type) {
return switch (type) {
- case BOOLEAN -> new SemanticResult(null, 0.9, null, null, Map.of("provider", "fixture"));
- case CHOICE -> new SemanticResult(
+ case "boolean" -> new SemanticResult(null, 0.9, null, null, Map.of("provider", "fixture"));
+ case "choice" -> new SemanticResult(
"billing", null, Map.of("billing", 0.9, "technical", 0.1), 0.8, Map.of("provider", "fixture"));
- case SCORE -> new SemanticResult(1.2, null, null, 0.7, Map.of("provider", "fixture"));
+ case "score" -> new SemanticResult(1.2, null, null, 0.7, Map.of("provider", "fixture"));
default -> throw new IllegalArgumentException("Unsupported fixture operation");
};
}
private static class RecordingAdapter extends TestSemanticAdapter {
- final List<SemanticQuestion.Type> calls = new ArrayList<>();
+ final List<String> calls = new ArrayList<>();
final List<Object> states = new ArrayList<>();
@Override
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) throws Exception {
- calls.add(question.getType());
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) throws Exception {
+ calls.add(evaluation.getOperation());
states.add(state);
- return applyPolicy(question, result(question.getType()));
+ return applyPolicy(evaluation, result(evaluation.getOperation()));
}
}
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java
index 6e00e44c369e..8cfba855aca3 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java
@@ -71,7 +71,7 @@ class SemanticCapabilitiesTest {
Map<String, Object> policy = new LinkedHashMap<>();
List<String> labels = new ArrayList<>(List.of("privacy"));
policy.put("labels", labels);
- SemanticQuestion evaluation = new SemanticQuestion(
+ SemanticEvaluation evaluation = new SemanticEvaluation(
"classify", "expert", null,
Map.of("criterion", "safety", "policy", policy));
labels.add("later");
@@ -104,12 +104,11 @@ class SemanticCapabilitiesTest {
@Test
void explicitPolicyValuesArePreservedAndMutableNumbersAreRejected() {
- var evaluation = new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, null, null, null, null,
- 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
+ var evaluation = new SemanticEvaluation(
+ "boolean", null, null, Map.of("threshold", 0.5, "uncertainty", 0.0, "uncertaintyPolicy", "fail"));
assertThat(evaluation.getParameters()).containsEntry("threshold", 0.5).containsEntry("uncertainty", 0.0)
.containsEntry("uncertaintyPolicy", "fail");
- assertThatThrownBy(() -> new SemanticQuestion("classify", "expert", null, Map.of("threshold", new AtomicInteger(1))))
+ assertThatThrownBy(() -> new SemanticEvaluation("classify", "expert", null, Map.of("threshold", new AtomicInteger(1))))
.hasMessageContaining("immutable scalar");
var operation = SemanticCapabilities.from(StaticExpert.class).operation("classify");
assertThatCode(() -> operation.validate(Map.of("criterion", "safety", "limit", 2.0))).doesNotThrowAnyException();
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
index 1b29c3c1ce90..868bc6188692 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
@@ -40,7 +40,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.snakeyaml.engine.v2.api.LoadSettings;
import org.snakeyaml.engine.v2.api.lowlevel.Compose;
-import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -53,13 +53,15 @@ class SemanticContractTest {
context.getRegistry().bind("content", expert);
declarations(context, dsl);
context.start();
- var evaluation = SemanticQuestions.get(context).get("categories");
+ var evaluation = SemanticEvaluations.get(context).get("categories");
assertThat(evaluation.getExpert()).isEqualTo("content");
assertThat(evaluation.getState()).isEqualTo("${body}");
assertThat(evaluation.getParameters()).containsOnlyKeys("policy", "limit");
+ assertThat(evaluation.getParameters().get("limit")).isInstanceOf(Number.class);
assertThat(evaluation.getParameters().get("policy")).isInstanceOfSatisfying(Map.class, policy -> {
assertThat(policy.get("allowed-tags")).isEqualTo(List.of("privacy", "unsafe"));
assertThat(policy.get("enabled")).isEqualTo(true);
+ assertThat(policy.get("cutoff")).isInstanceOf(Number.class);
});
assertThat(new BigDecimal(((Map<?, ?>) evaluation.getParameters().get("policy")).get("cutoff").toString()))
.isEqualByComparingTo("0.7");
@@ -100,8 +102,8 @@ class SemanticContractTest {
try (var context = new DefaultCamelContext()) {
var expert = new ContentExpert();
context.getRegistry().bind("content", expert);
- SemanticQuestions.get(context).replace("test",
- Map.of("q", new SemanticQuestion(operation, "content", null, Map.of())));
+ SemanticEvaluations.get(context).replace("test",
+ Map.of("q", new SemanticEvaluation(operation, "content", null, Map.of())));
context.start();
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody(Map.of("response", "answer"));
@@ -118,14 +120,14 @@ class SemanticContractTest {
void cachedNestedSelectorsValidateTheCompleteReplacement(boolean transitive) throws Exception {
try (var context = new DefaultCamelContext()) {
context.getRegistry().bind("content", new ContentExpert());
- var registry = SemanticQuestions.get(context);
- Map<String, SemanticQuestion> declarations = new LinkedHashMap<>();
- declarations.put("outer", new SemanticQuestion("detect", "content", "${semantic('inner')} ready", Map.of()));
- declarations.put("inner", new SemanticQuestion(
+ var registry = SemanticEvaluations.get(context);
+ Map<String, SemanticEvaluation> declarations = new LinkedHashMap<>();
+ declarations.put("outer", new SemanticEvaluation("detect", "content", "${semantic('inner')} ready", Map.of()));
+ declarations.put("inner", new SemanticEvaluation(
"detect", "content",
transitive ? "${semantic('leaf')} ready" : null, Map.of()));
if (transitive) {
- declarations.put("leaf", new SemanticQuestion("detect", "content", null, Map.of()));
+ declarations.put("leaf", new SemanticEvaluation("detect", "content", null, Map.of()));
}
registry.replace("test", declarations);
context.start();
@@ -133,11 +135,11 @@ class SemanticContractTest {
exchange.getMessage().setBody("content");
var expression = context.resolveLanguage("semantic").createExpression("ref:outer");
assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
- Map<String, SemanticQuestion> replacement = new LinkedHashMap<>(declarations);
+ Map<String, SemanticEvaluation> replacement = new LinkedHashMap<>(declarations);
String removed = transitive ? "leaf" : "inner";
replacement.remove(removed);
assertThatThrownBy(() -> registry.replace("test", replacement))
- .hasMessageContaining("Unknown semantic question: " + removed);
+ .hasMessageContaining("Unknown semantic evaluation: " + removed);
assertThat(registry.get(removed)).isSameAs(declarations.get(removed));
assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
}
@@ -149,13 +151,13 @@ class SemanticContractTest {
var entered = new CountDownLatch(1);
var release = new CountDownLatch(1);
try (var context = new DefaultCamelContext()) {
- var original = new SemanticQuestion("detect", "content", null, Map.of());
- var candidate = new SemanticQuestion("detect", "content", "${header.candidate}", Map.of());
- var registry = SemanticQuestions.get(context);
+ var original = new SemanticEvaluation("detect", "content", null, Map.of());
+ var candidate = new SemanticEvaluation("detect", "content", "${header.candidate}", Map.of());
+ var registry = SemanticEvaluations.get(context);
context.getRegistry().bind("content", new ContentExpert() {
@Override
- public void validate(SemanticQuestion question) {
- if (question == candidate) {
+ public void validate(SemanticEvaluation evaluation) {
+ if (evaluation == candidate) {
entered.countDown();
try {
assertThat(release.await(10, TimeUnit.SECONDS)).isTrue();
@@ -187,17 +189,17 @@ class SemanticContractTest {
void invalidProgrammaticReplacementRetainsUnusedDeclarations() throws Exception {
try (var context = new DefaultCamelContext()) {
context.getRegistry().bind("content", new ContentExpert());
- var registry = SemanticQuestions.get(context);
- var previous = new SemanticQuestion("detect", "content", null, Map.of());
+ var registry = SemanticEvaluations.get(context);
+ var previous = new SemanticEvaluation("detect", "content", null, Map.of());
registry.replace("test", Map.of("q", previous));
context.start();
assertThatThrownBy(() -> registry.replace("test", Map.of("q",
- new SemanticQuestion("detect", "content", null, Map.of("threshold", "wrong")))))
+ new SemanticEvaluation("detect", "content", null, Map.of("threshold", "wrong")))))
.hasMessageContaining("q").hasMessageContaining("content").hasMessageContaining("threshold");
assertThat(registry.get("q")).isSameAs(previous);
- assertThat(previous.getThreshold()).isNull();
- assertThat(previous.getUncertainty()).isNull();
- assertThat(previous.getUncertaintyPolicy()).isNull();
+ assertThat(previous.getParameters().get("threshold")).isNull();
+ assertThat(previous.getParameters().get("uncertainty")).isNull();
+ assertThat(previous.getParameters().get("uncertaintyPolicy")).isNull();
}
}
@@ -208,7 +210,7 @@ class SemanticContractTest {
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- semanticQuestions(this).expert("content")
+ semanticEvaluations(this).expert("content")
.evaluation("categories").operation("classify").end()
.evaluation("safety").operation("safety").register();
from("direct:check").choice()
@@ -240,7 +242,7 @@ class SemanticContractTest {
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- semanticQuestions(this).expert("content")
+ semanticEvaluations(this).expert("content")
.evaluation("injection").operation("detect").parameter("threshold", 0.9).end()
.evaluation("defaultPolicy").operation("detect").register();
}
@@ -273,8 +275,8 @@ class SemanticContractTest {
try (var context = new DefaultCamelContext()) {
var expert = new ContentExpert();
context.getRegistry().bind("content", expert);
- SemanticQuestions.get(context).replace("test",
- Map.of("categories", new SemanticQuestion("classify", "content", null, Map.of())));
+ SemanticEvaluations.get(context).replace("test",
+ Map.of("categories", new SemanticEvaluation("classify", "content", null, Map.of())));
context.start();
var expression = context.resolveLanguage("simple").createExpression("${semantic('categories')}");
var exchange = new DefaultExchange(context);
@@ -301,8 +303,8 @@ class SemanticContractTest {
throw new AssertionError("Runtime must read the class contract directly");
}
});
- SemanticQuestions.get(context).replace("test",
- Map.of("detect", new SemanticQuestion("detect", "content", null, Map.of())));
+ SemanticEvaluations.get(context).replace("test",
+ Map.of("detect", new SemanticEvaluation("detect", "content", null, Map.of())));
context.start();
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody("content");
@@ -322,9 +324,9 @@ class SemanticContractTest {
if (started) {
context.start();
}
- SemanticQuestions.get(context).replace("test", Map.of(
- "inner", new SemanticQuestion("boolean", null, null, Map.of()),
- "outer", new SemanticQuestion("detect", "content", "${semantic('inner')} ready", Map.of())));
+ SemanticEvaluations.get(context).replace("test", Map.of(
+ "inner", new SemanticEvaluation("boolean", null, null, Map.of()),
+ "outer", new SemanticEvaluation("detect", "content", "${semantic('inner')} ready", Map.of())));
context.start();
assertThat(SemanticExpertTest.ManagedExpert.initialized).isZero();
assertThat(SemanticExpertTest.ManagedExpert.started).isZero();
@@ -341,7 +343,7 @@ class SemanticContractTest {
try (var context = new DefaultCamelContext()) {
var expert = new ContentExpert();
context.getRegistry().bind("content", expert);
- SemanticQuestions.get(context).replace("test", Map.of("bad", new SemanticQuestion(
+ SemanticEvaluations.get(context).replace("test", Map.of("bad", new SemanticEvaluation(
"detect", "content", null,
Map.of("threshold", "private-value"))));
assertThatThrownBy(context::start).hasMessageContaining("bad").hasMessageContaining("content")
@@ -354,9 +356,9 @@ class SemanticContractTest {
void nestedStateEvaluationCannotLeaveResultsWhenOuterEvaluationFails() throws Exception {
try (var context = new DefaultCamelContext()) {
context.getRegistry().bind("content", new ContentExpert());
- SemanticQuestions.get(context).replace("test", Map.of(
- "inner", new SemanticQuestion("safety", "content", null, Map.of()),
- "outer", new SemanticQuestion("classify", "content", "${semantic('inner')}", Map.of())));
+ SemanticEvaluations.get(context).replace("test", Map.of(
+ "inner", new SemanticEvaluation("safety", "content", null, Map.of()),
+ "outer", new SemanticEvaluation("classify", "content", "${semantic('inner')}", Map.of())));
context.start();
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody(Map.of("response", "answer"));
@@ -374,7 +376,7 @@ class SemanticContractTest {
context.getRegistry().bind("content", new ContentExpert());
numericDeclaration(context, dsl, "9007199254740993");
context.start();
- Object value = SemanticQuestions.get(context).get("numeric").getParameters().get("limit");
+ Object value = SemanticEvaluations.get(context).get("numeric").getParameters().get("limit");
assertThat(new BigDecimal(value.toString())).isEqualByComparingTo("9007199254740993");
}
try (var context = new DefaultCamelContext()) {
@@ -386,8 +388,8 @@ class SemanticContractTest {
private static void numericDeclaration(DefaultCamelContext context, String dsl, String value) throws Exception {
if (dsl.equals("java")) {
- SemanticQuestions.get(context).replace("test", Map.of("numeric",
- new SemanticQuestion("classify", "content", null, Map.of("limit", new BigDecimal(value)))));
+ SemanticEvaluations.get(context).replace("test", Map.of("numeric",
+ new SemanticEvaluation("classify", "content", null, Map.of("limit", new BigDecimal(value)))));
} else if (dsl.equals("yaml")) {
yamlDeclaration(context, """
- semantic:
@@ -424,11 +426,12 @@ class SemanticContractTest {
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- semanticQuestions(this).expert("content").state("${body}")
+ semanticEvaluations(this).expert("content").state("${body}")
.evaluation("categories").operation("classify")
- .parameter("limit", 2).parameter("policy",
+ .parameter("limit", Double.valueOf(getContext().resolvePropertyPlaceholders("{{limit:2}}")))
+ .parameter("policy",
Map.of("allowed-tags", List.of("{{label:privacy}}", "unsafe"), "enabled", true, "cutoff",
- 0.7))
+ Double.valueOf(getContext().resolvePropertyPlaceholders("{{cutoff:0.7}}"))))
.register();
}
});
@@ -441,11 +444,11 @@ class SemanticContractTest {
categories:
operation: classify
parameters:
- limit: 2
+ limit: !number "{{limit:2}}"
policy:
allowed-tags: ["{{label:privacy}}", unsafe]
enabled: true
- cutoff: 0.7
+ cutoff: !number "{{cutoff:0.7}}"
""";
yamlDeclaration(context, yaml);
} else {
@@ -454,11 +457,11 @@ class SemanticContractTest {
<semantic expert="content" state="${body}">
<evaluation name="categories" operation="classify">
<parameters>
- <parameter name="limit"><number>2</number></parameter>
+ <parameter name="limit"><number>{{limit:2}}</number></parameter>
<parameter name="policy"><map>
<entry key="allowed-tags"><list><string>{{label:privacy}}</string><string>unsafe</string></list></entry>
<entry key="enabled"><boolean>true</boolean></entry>
- <entry key="cutoff"><number>0.7</number></entry>
+ <entry key="cutoff"><number>{{cutoff:0.7}}</number></entry>
</map></parameter>
</parameters>
</evaluation>
@@ -500,11 +503,11 @@ class SemanticContractTest {
boolean malformed;
@Override
- public void validate(SemanticQuestion evaluation) {
+ public void validate(SemanticEvaluation evaluation) {
}
@Override
- public void validateInput(SemanticQuestion evaluation, Object selected) {
+ public void validateInput(SemanticEvaluation evaluation, Object selected) {
if (!evaluation.getOperation().equals("detect")
&& (!(selected instanceof Map<?, ?> map) || !map.containsKey("response"))) {
throw new IllegalArgumentException("Selected state requires a response");
@@ -512,7 +515,7 @@ class SemanticContractTest {
}
@Override
- public SemanticResult evaluate(SemanticQuestion evaluation, Object selected) {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object selected) {
calls++;
state = selected;
parameters = evaluation.getParameters();
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java
index 0dace6f1cda9..79288d8efb1c 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java
@@ -32,6 +32,7 @@ import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.ResourceHelper;
import org.apache.camel.support.RouteWatcherReloadStrategy;
+import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -41,7 +42,7 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
-import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -56,16 +57,16 @@ class SemanticDeclarationDslTest {
context = new DefaultCamelContext();
context.getRegistry().bind("adapter", new TestSemanticAdapter() {
@Override
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) {
states.add(state);
- return applyPolicy(question, switch (question.getType()) {
- case BOOLEAN -> new SemanticResult(null, 0.82, null, null, null);
- case CHOICE -> new SemanticResult("billing", null, null, null, null);
- case SCORE -> new SemanticResult(1.2, null, null, null, null);
+ return applyPolicy(evaluation, switch (evaluation.getOperation()) {
+ case "boolean" -> new SemanticResult(null, 0.82, null, null, null);
+ case "choice" -> new SemanticResult("billing", null, null, null, null);
+ case "score" -> new SemanticResult(1.2, null, null, null, null);
default -> throw new IllegalArgumentException("Unsupported fixture operation");
});
}
@@ -79,33 +80,33 @@ class SemanticDeclarationDslTest {
context.close();
}
- private static void questions(SemanticQuestionsBuilder semantic) {
- semantic.question("urgent").type("boolean").instructions("Is this urgent?")
+ private static void evaluations(SemanticEvaluationsBuilder semantic) {
+ semantic.evaluation("urgent").type("boolean").instructions("Is this urgent?")
.state("${header.myState}").threshold(0.8).uncertainty(0.1).uncertaintyPolicy("non-match");
- semantic.question("department").type("choice").instructions("Which department?")
+ semantic.evaluation("department").type("choice").instructions("Which department?")
.state("${header.myState}").criterion("billing", "Invoices and refunds")
.criterion("technical", "Bugs and outages");
- semantic.question("priority").type("score").instructions("How urgent?")
+ semantic.evaluation("priority").type("score").instructions("How urgent?")
.state("${header.myState}").level("Routine").level("Urgent").level("Critical");
semantic.register();
}
- private static String xmlQuestions() {
+ private static String xmlEvaluations() {
return """
<semantic>
- <question name="urgent" type="boolean" state="${header.myState}"
+ <evaluation name="urgent" type="boolean" state="${header.myState}"
threshold="0.8" uncertainty="0.1" uncertaintyPolicy="non-match">
<instructions>Is this urgent?</instructions>
- </question>
- <question name="department" type="choice" state="${header.myState}">
+ </evaluation>
+ <evaluation name="department" type="choice" state="${header.myState}">
<instructions>Which department?</instructions>
<criterion key="billing" value="Invoices and refunds"/>
<criterion key="technical" value="Bugs and outages"/>
- </question>
- <question name="priority" type="score" state="${header.myState}">
+ </evaluation>
+ <evaluation name="priority" type="score" state="${header.myState}">
<instructions>How urgent?</instructions>
<level>Routine</level><level>Urgent</level><level>Critical</level>
- </question>
+ </evaluation>
</semantic>
""";
}
@@ -117,7 +118,7 @@ class SemanticDeclarationDslTest {
@ParameterizedTest
@ValueSource(strings = {
"java", "routes", "standalone", "no-namespace", "component-namespace", "xml-io-namespace", "semantic-extension" })
- void componentDeclarationsEvaluateSingleQuestionsAndMixedBatches(String dsl) throws Exception {
+ void componentDeclarationsEvaluateSingleEvaluationsAndMixedBatches(String dsl) throws Exception {
if (dsl.equals("java")) {
context.addRoutes(new RouteBuilder() {
@Override
@@ -125,7 +126,7 @@ class SemanticDeclarationDslTest {
from("direct:batch").setProperty("decision").language("semantic", "refs:urgent,department,priority")
.setHeader("department", simple("${exchangeProperty.decision[department]}"));
// Declarations are collected before route initialization, including those after the route.
- questions(semanticQuestions(this));
+ evaluations(semanticEvaluations(this));
}
});
} else {
@@ -141,9 +142,9 @@ class SemanticDeclarationDslTest {
if (dsl.equals("standalone")) {
PluginHelper.getRoutesLoader(context).loadRoutes(List.of(
ResourceHelper.fromString("use.xml", document("routes", route)),
- ResourceHelper.fromString("questions.xml", xmlQuestions())));
+ ResourceHelper.fromString("evaluations.xml", xmlEvaluations())));
} else {
- String xml = document("routes", route + xmlQuestions());
+ String xml = document("routes", route + xmlEvaluations());
if (dsl.equals("no-namespace")) {
xml = xml.replace(" xmlns=\"http://camel.apache.org/schema/spring\"", "");
} else if (dsl.equals("component-namespace")) {
@@ -151,14 +152,15 @@ class SemanticDeclarationDslTest {
} else if (dsl.equals("xml-io-namespace")) {
xml = xml.replace("schema/spring", "schema/xml-io");
}
- load(dsl.equals("semantic-extension") ? "questions.semantic.xml" : "questions.xml", xml);
+ load(dsl.equals("semantic-extension") ? "evaluations.semantic.xml" : "evaluations.xml", xml);
}
}
assertThat(states).isEmpty();
- SemanticQuestion urgent = SemanticQuestions.get(context).get("urgent");
- assertThat(urgent.getThreshold()).isEqualTo(0.8);
- assertThat(urgent.getUncertaintyPolicy()).isEqualTo(SemanticQuestion.UncertaintyPolicy.NON_MATCH);
- assertThat(SemanticQuestions.get(context).get("priority").getLevels()).containsExactly("Routine", "Urgent", "Critical");
+ SemanticEvaluation urgent = SemanticEvaluations.get(context).get("urgent");
+ assertThat(urgent.getParameters().get("threshold")).isEqualTo(0.8);
+ assertThat(urgent.getParameters().get("uncertaintyPolicy")).isEqualTo("non-match");
+ assertThat(SemanticEvaluations.get(context).get("priority").getParameters().get("criteria"))
+ .isEqualTo(List.of("Routine", "Urgent", "Critical"));
try (var template = context.createProducerTemplate()) {
Exchange exchange = template.request("direct:batch", e -> {
e.getMessage().setBody("original");
@@ -182,91 +184,93 @@ class SemanticDeclarationDslTest {
@ParameterizedTest
@MethodSource("invalidXml")
void invalidDeclarationsLeaveThePreviousDefinitionsIntact(String declaration, String message) throws Exception {
- load("questions.xml", document("routes", xmlQuestions()));
- SemanticQuestion previous = SemanticQuestions.get(context).get("department");
+ load("evaluations.xml", document("routes", xmlEvaluations()));
+ SemanticEvaluation previous = SemanticEvaluations.get(context).get("department");
assertThatThrownBy(() -> PluginHelper.getRoutesLoader(context).updateRoutes(
- ResourceHelper.fromString("questions.xml",
+ ResourceHelper.fromString("evaluations.xml",
document("routes", "<semantic>" + declaration + "</semantic>"))))
.hasMessageContaining(message);
- assertThat(SemanticQuestions.get(context).get("department")).isSameAs(previous);
+ assertThat(SemanticEvaluations.get(context).get("department")).isSameAs(previous);
assertThat(states).isEmpty();
- PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.xml",
- document("routes", xmlQuestions().replace("header.myState", "header.corrected"))));
- assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.corrected}");
+ PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("evaluations.xml",
+ document("routes", xmlEvaluations().replace("header.myState", "header.corrected"))));
+ assertThat(SemanticEvaluations.get(context).get("department").getState()).isEqualTo("${header.corrected}");
}
static Stream<Arguments> invalidXml() {
- String question = "<question name=\"q\" type=\"boolean\"><instructions>Valid?</instructions></question>";
+ String evaluation = "<evaluation name=\"q\" type=\"boolean\"><instructions>Valid?</instructions></evaluation>";
return Stream.of(
- Arguments.of(question + question, "Duplicate semantic question"),
- Arguments.of(question.replace("name=\"q\"", "name=\" \""), "nonblank name"),
- Arguments.of(question.replace("type=\"boolean\"", ""), "operation is required"),
- Arguments.of(question.replace("boolean", "unknown"), "Unknown operation"),
- Arguments.of(question.replace("Valid?", " "), "Parameter 'instructions' is outside its size constraints"),
- Arguments.of(question.replace("type=\"boolean\"", "type=\"choice\" threshold=\"0.8\""),
+ Arguments.of(evaluation + evaluation, "Duplicate semantic evaluation"),
+ Arguments.of(evaluation.replace("name=\"q\"", "name=\" \""), "nonblank name"),
+ Arguments.of(evaluation.replace("type=\"boolean\"", ""), "operation is required"),
+ Arguments.of(evaluation.replace("boolean", "unknown"), "Unknown operation"),
+ Arguments.of(evaluation.replace("Valid?", " "), "Parameter 'instructions' is outside its size constraints"),
+ Arguments.of(evaluation.replace("type=\"boolean\"", "type=\"choice\" threshold=\"0.8\""),
"Unknown parameter 'threshold'"),
Arguments.of(
- question.replace("</question>",
- "<criterion key=\"true\" value=\"A\"/><criterion key=\"true\" value=\"B\"/></question>"),
+ evaluation.replace("</evaluation>",
+ "<criterion key=\"true\" value=\"A\"/><criterion key=\"true\" value=\"B\"/></evaluation>"),
"Duplicate semantic criterion"),
- Arguments.of(question.replace("type=\"boolean\"", "type=\"score\""), "Parameter 'criteria' is required"),
- Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" threshold=\"NaN\""), "numeric constraints"),
- Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" threshold=\"abc\""),
- "Invalid semantic question 'q': Parameter 'threshold' must be a valid number"),
- Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" uncertainty=\"abc\""),
- "Invalid semantic question 'q': Parameter 'uncertainty' must be a valid number"),
- Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" state=\" \""),
+ Arguments.of(evaluation.replace("type=\"boolean\"", "type=\"score\""), "Parameter 'criteria' is required"),
+ Arguments.of(evaluation.replace("type=\"boolean\"", "type=\"boolean\" threshold=\"NaN\""),
+ "numeric constraints"),
+ Arguments.of(evaluation.replace("type=\"boolean\"", "type=\"boolean\" threshold=\"abc\""),
+ "Invalid semantic evaluation 'q': Parameter 'threshold' must be a valid number"),
+ Arguments.of(evaluation.replace("type=\"boolean\"", "type=\"boolean\" uncertainty=\"abc\""),
+ "Invalid semantic evaluation 'q': Parameter 'uncertainty' must be a valid number"),
+ Arguments.of(evaluation.replace("type=\"boolean\"", "type=\"boolean\" state=\" \""),
"state selector must not be blank"),
- Arguments.of(question.replace("name=\"q\"", "unknown=\"q\""), "Unexpected attribute"));
+ Arguments.of(evaluation.replace("name=\"q\"", "unknown=\"q\""), "Unexpected attribute"));
}
@Test
void javaResourceReloadReplacesAndRemovesDeclarations() throws Exception {
- class Questions extends RouteBuilder {
+ class Evaluations extends RouteBuilder {
private final String name;
- Questions(String name) {
+ Evaluations(String name) {
this.name = name;
- setResource(ResourceHelper.fromString("Questions.java", ""));
+ setResource(ResourceHelper.fromString("Evaluations.java", ""));
}
@Override
public void configure() {
if (name != null) {
- semanticQuestions(this).question(name).type("boolean").instructions("Valid?").register();
+ semanticEvaluations(this).evaluation(name).type("boolean").instructions("Valid?").register();
}
}
}
- context.addRoutes(new Questions("first"));
- context.addRoutes(new Questions("second"));
- assertThatThrownBy(() -> SemanticQuestions.get(context).get("first")).hasMessageContaining("Unknown");
- assertThat(SemanticQuestions.get(context).get("second")).isNotNull();
- context.addRoutes(new Questions(null));
- assertThatThrownBy(() -> SemanticQuestions.get(context).get("second")).hasMessageContaining("Unknown");
+ context.addRoutes(new Evaluations("first"));
+ context.addRoutes(new Evaluations("second"));
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).get("first")).hasMessageContaining("Unknown");
+ assertThat(SemanticEvaluations.get(context).get("second")).isNotNull();
+ context.addRoutes(new Evaluations(null));
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).get("second")).hasMessageContaining("Unknown");
assertThat(states).isEmpty();
}
@Test
void embeddedBuilderInstancesOwnSeparateDeclarations() throws Exception {
- class Questions extends RouteBuilder {
+ class Evaluations extends RouteBuilder {
private final String name;
- Questions(String name) {
+ Evaluations(String name) {
this.name = name;
}
@Override
public void configure() {
if (name != null) {
- semanticQuestions(this).question(name).type("boolean").instructions("Valid?").register();
+ semanticEvaluations(this).evaluation(name).type("boolean").instructions("Valid?").register();
}
}
}
- context.addRoutes(new Questions("first"));
- context.addRoutes(new Questions("second"));
- context.addRoutes(new Questions(null));
- assertThat(SemanticQuestions.get(context).get(List.of("first", "second"))).hasSize(2);
- assertThatThrownBy(() -> context.addRoutes(new Questions("first"))).hasMessageContaining("Duplicate semantic question");
+ context.addRoutes(new Evaluations("first"));
+ context.addRoutes(new Evaluations("second"));
+ context.addRoutes(new Evaluations(null));
+ assertThat(SemanticEvaluations.get(context).get(List.of("first", "second"))).hasSize(2);
+ assertThatThrownBy(() -> context.addRoutes(new Evaluations("first")))
+ .hasMessageContaining("Duplicate semantic evaluation");
assertThat(states).isEmpty();
}
@@ -276,12 +280,12 @@ class SemanticDeclarationDslTest {
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- semanticQuestions(this).question("urgent").type("BOOLEAN").instructions("Urgent?").uncertaintyPolicy(policy)
+ semanticEvaluations(this).evaluation("urgent").type("BOOLEAN").instructions("Urgent?").uncertaintyPolicy(policy)
.register();
}
});
- assertThat(SemanticQuestions.get(context).get("urgent").getUncertaintyPolicy())
- .isEqualTo(SemanticQuestion.UncertaintyPolicy.NON_MATCH);
+ assertThat(SemanticEvaluations.get(context).get("urgent").getParameters().get("uncertaintyPolicy"))
+ .isEqualTo("non-match");
}
@Test
@@ -289,11 +293,11 @@ class SemanticDeclarationDslTest {
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- questions(semanticQuestions(this));
+ evaluations(semanticEvaluations(this));
}
});
- assertThatThrownBy(() -> load("duplicate.xml", document("routes", xmlQuestions())))
- .hasMessageContaining("Duplicate semantic question");
+ assertThatThrownBy(() -> load("duplicate.xml", document("routes", xmlEvaluations())))
+ .hasMessageContaining("Duplicate semantic evaluation");
assertThat(states).isEmpty();
}
@@ -304,41 +308,41 @@ class SemanticDeclarationDslTest {
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- semanticQuestions(this).question("urgent").type("boolean").instructions("Urgent?")
+ semanticEvaluations(this).evaluation("urgent").type("boolean").instructions("Urgent?")
.threshold("{{threshold:0.8}}").uncertainty("{{uncertainty:0.1}}").register();
}
});
} else {
- load("questions.xml", document("routes", xmlQuestions()
+ load("evaluations.xml", document("routes", xmlEvaluations()
.replace("threshold=\"0.8\"", "threshold=\"{{threshold:0.8}}\"")
.replace("uncertainty=\"0.1\"", "uncertainty=\"{{uncertainty:0.1}}\"")));
}
- assertThat(SemanticQuestions.get(context).get("urgent").getThreshold()).isEqualTo(0.8);
- assertThat(SemanticQuestions.get(context).get("urgent").getUncertainty()).isEqualTo(0.1);
+ assertThat(SemanticEvaluations.get(context).get("urgent").getParameters().get("threshold")).isEqualTo(0.8);
+ assertThat(SemanticEvaluations.get(context).get("urgent").getParameters().get("uncertainty")).isEqualTo(0.1);
assertThatThrownBy(() -> load("invalid.xml", document("routes", """
- <semantic><question name="invalid" type="boolean" threshold="{{threshold:abc}}">
+ <semantic><evaluation name="invalid" type="boolean" threshold="{{threshold:abc}}">
<instructions>Urgent?</instructions>
- </question></semantic>
+ </evaluation></semantic>
""")))
- .hasMessageContaining("Invalid semantic question 'invalid': Parameter 'threshold' must be a valid number")
+ .hasMessageContaining("Invalid semantic evaluation 'invalid': Parameter 'threshold' must be a valid number")
.hasRootCauseInstanceOf(IllegalArgumentException.class);
}
@Test
void failedResourceBatchDoesNotReuseEarlierCachedDeclarationsOnRetry() throws Exception {
- String second = "<semantic><question name=\"second\" type=\"boolean\">"
- + "<instructions>Valid?</instructions></question></semantic>";
+ String second = "<semantic><evaluation name=\"second\" type=\"boolean\">"
+ + "<instructions>Valid?</instructions></evaluation></semantic>";
var loader = PluginHelper.getRoutesLoader(context);
assertThatThrownBy(() -> loader.loadRoutes(List.of(
- ResourceHelper.fromString("first.xml", document("routes", xmlQuestions())),
+ ResourceHelper.fromString("first.xml", document("routes", xmlEvaluations())),
ResourceHelper.fromString("second.xml", document("routes", second.replace("boolean", "unknown"))))))
.hasMessageContaining("Unknown operation");
loader.loadRoutes(List.of(
ResourceHelper.fromString("first.xml",
- document("routes", xmlQuestions().replace("header.myState", "header.new"))),
+ document("routes", xmlEvaluations().replace("header.myState", "header.new"))),
ResourceHelper.fromString("second.xml", document("routes", second))));
- assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.new}");
- assertThat(SemanticQuestions.get(context).get("second")).isNotNull();
+ assertThat(SemanticEvaluations.get(context).get("department").getState()).isEqualTo("${header.new}");
+ assertThat(SemanticEvaluations.get(context).get("second")).isNotNull();
assertThat(states).isEmpty();
}
@@ -350,7 +354,7 @@ class SemanticDeclarationDslTest {
<setBody><language language="semantic">ref:department</language></setBody>
</route>
""")),
- ResourceHelper.fromString("definitions.xml", document("routes", xmlQuestions()))));
+ ResourceHelper.fromString("definitions.xml", document("routes", xmlEvaluations()))));
assertThat(states).isEmpty();
try (var template = context.createProducerTemplate()) {
assertThat(template.requestBodyAndHeader("direct:use", "original", "myState", "invoice")).isEqualTo("billing");
@@ -363,106 +367,108 @@ class SemanticDeclarationDslTest {
<route><from uri="direct:unknown"/>
<setBody><language language="semantic">ref:missing</language></setBody>
</route>
- """))).hasRootCauseMessage("Unknown semantic question: missing");
+ """))).hasRootCauseMessage("Unknown semantic evaluation: missing");
assertThat(states).isEmpty();
}
@Test
void reloadingReplacesAndRemovesDeclarationsUsedByExistingExpressions() throws Exception {
- load("questions.xml", document("routes", xmlQuestions()));
+ load("evaluations.xml", document("routes", xmlEvaluations()));
var expression = context.resolveLanguage("semantic").createExpression("ref:department");
var exchange = new DefaultExchange(context);
exchange.getMessage().setHeader("myState", "old");
assertThat(expression.evaluate(exchange, String.class)).isEqualTo("billing");
- PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.xml",
- document("routes", xmlQuestions().replace("header.myState", "header.updated"))));
+ PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("evaluations.xml",
+ document("routes", xmlEvaluations().replace("header.myState", "header.updated"))));
exchange.getMessage().setHeader("updated", "new");
assertThat(expression.evaluate(exchange, String.class)).isEqualTo("billing");
assertThat(states).containsExactly("old", "new");
PluginHelper.getRoutesLoader(context)
- .updateRoutes(ResourceHelper.fromString("questions.xml", document("routes", "")));
- assertThatThrownBy(() -> expression.evaluate(exchange, Object.class)).hasMessageContaining("Unknown semantic question");
+ .updateRoutes(ResourceHelper.fromString("evaluations.xml", document("routes", "")));
+ assertThatThrownBy(() -> expression.evaluate(exchange, Object.class))
+ .hasMessageContaining("Unknown semantic evaluation");
}
@Test
void watcherRemovesDeletedAndRenamedDeclarationResources() throws Exception {
- Path original = directory.resolve("questions.xml");
- Files.writeString(original, document("routes", xmlQuestions()));
+ Path original = directory.resolve("evaluations.xml");
+ Files.writeString(original, document("routes", xmlEvaluations()));
Resource source = ResourceHelper.resolveResource(context, original.toUri().toString());
PluginHelper.getRoutesLoader(context).loadRoutes(source);
TestWatcher watcher = new TestWatcher();
watcher.setCamelContext(context);
Path renamed = Files.move(original, directory.resolve("renamed.xml"));
watcher.reload(source);
- assertThatThrownBy(() -> SemanticQuestions.get(context).get("department")).hasMessageContaining("Unknown");
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).get("department")).hasMessageContaining("Unknown");
Resource replacement = ResourceHelper.resolveResource(context, renamed.toUri().toString());
watcher.reload(replacement);
assertThat(watcher.getLastError()).isNull();
- assertThat(SemanticQuestions.get(context).get("department")).isNotNull();
+ assertThat(SemanticEvaluations.get(context).get("department")).isNotNull();
Files.delete(renamed);
watcher.reload(replacement);
- assertThatThrownBy(() -> SemanticQuestions.get(context).get("department")).hasMessageContaining("Unknown");
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).get("department")).hasMessageContaining("Unknown");
}
@ParameterizedTest
@ValueSource(strings = { "", "http://camel.apache.org/schema/semantic", "http://camel.apache.org/schema/xml-io" })
void standaloneXmlSupportsOptionalNamespace(String namespace) throws Exception {
- load("questions.xml", xmlQuestions().replace("<semantic>", "<semantic xmlns=\"" + namespace + "\">"));
- assertThat(SemanticQuestions.get(context).get("department").getCriteria()).containsKey("billing");
- load("questions.xml", "<semantic/>");
- assertThat(SemanticQuestions.get(context).isEmpty()).isTrue();
+ load("evaluations.xml", xmlEvaluations().replace("<semantic>", "<semantic xmlns=\"" + namespace + "\">"));
+ assertThat(SemanticEvaluations.get(context).get("department").getParameters().get("criteria"))
+ .asInstanceOf(InstanceOfAssertFactories.MAP).containsKey("billing");
+ load("evaluations.xml", "<semantic/>");
+ assertThat(SemanticEvaluations.get(context).isEmpty()).isTrue();
}
@Test
void failedJavaReplacementKeepsPreviousDefinitions() throws Exception {
- class Questions extends RouteBuilder {
+ class Evaluations extends RouteBuilder {
private final String instructions;
- Questions(String instructions) {
+ Evaluations(String instructions) {
this.instructions = instructions;
- setResource(ResourceHelper.fromString("Questions.java", ""));
+ setResource(ResourceHelper.fromString("Evaluations.java", ""));
}
@Override
public void configure() {
- semanticQuestions(this).question("urgent").type("boolean").instructions(instructions).end()
- .question("other").type("boolean").instructions("Other?").register();
+ semanticEvaluations(this).evaluation("urgent").type("boolean").instructions(instructions).end()
+ .evaluation("other").type("boolean").instructions("Other?").register();
}
}
- context.addRoutes(new Questions("Urgent?"));
- var previous = SemanticQuestions.get(context).get("urgent");
- assertThatThrownBy(() -> context.addRoutes(new Questions(" ")))
+ context.addRoutes(new Evaluations("Urgent?"));
+ var previous = SemanticEvaluations.get(context).get("urgent");
+ assertThatThrownBy(() -> context.addRoutes(new Evaluations(" ")))
.hasMessageContaining("Parameter 'instructions' is outside its size constraints");
- assertThat(SemanticQuestions.get(context).get("urgent")).isSameAs(previous);
- assertThat(SemanticQuestions.get(context).get("other")).isNotNull();
+ assertThat(SemanticEvaluations.get(context).get("urgent")).isSameAs(previous);
+ assertThat(SemanticEvaluations.get(context).get("other")).isNotNull();
}
@ParameterizedTest
@ValueSource(strings = {
"<routes><semantic/><semantic/></routes>",
"<routes><semantic xmlns='urn:unexpected'/></routes>",
- "<semantic><question name='q' type='boolean'><instructions>Hi</instructions><unexpected/></question></semantic>",
+ "<semantic><evaluation name='q' type='boolean'><instructions>Hi</instructions><unexpected/></evaluation></semantic>",
"<!DOCTYPE semantic [<!ENTITY external SYSTEM 'file:///nonexistent'>]><semantic>&external;</semantic>",
- "<routes><semantic><question name='q' type='boolean'><instructions>Hi</instructions></question></semantic><route><wrong/></route></routes>"
+ "<routes><semantic><evaluation name='q' type='boolean'><instructions>Hi</instructions></evaluation></semantic><route><wrong/></route></routes>"
})
void malformedXmlDoesNotReplacePreviousDefinitions(String xml) throws Exception {
- load("questions.xml", xmlQuestions());
- var previous = SemanticQuestions.get(context).get("department");
- assertThatThrownBy(() -> load("questions.xml", xml)).isInstanceOf(Exception.class);
- assertThat(SemanticQuestions.get(context).get("department")).isSameAs(previous);
+ load("evaluations.xml", xmlEvaluations());
+ var previous = SemanticEvaluations.get(context).get("department");
+ assertThatThrownBy(() -> load("evaluations.xml", xml)).isInstanceOf(Exception.class);
+ assertThat(SemanticEvaluations.get(context).get("department")).isSameAs(previous);
}
@ParameterizedTest
@ValueSource(strings = { "java", "xml" })
void loadingPlainBuilderDiscardsDeletedDeclarationResources(String dsl) throws Exception {
- Path file = directory.resolve(dsl.equals("java") ? "Questions.java" : "questions.xml");
- Files.writeString(file, xmlQuestions());
+ Path file = directory.resolve(dsl.equals("java") ? "Evaluations.java" : "evaluations.xml");
+ Files.writeString(file, xmlEvaluations());
Resource resource = ResourceHelper.resolveResource(context, file.toUri().toString());
if (dsl.equals("java")) {
RouteBuilder builder = new RouteBuilder() {
@Override
public void configure() {
- semanticQuestions(this).question("urgent").type("boolean").instructions("Urgent?").register();
+ semanticEvaluations(this).evaluation("urgent").type("boolean").instructions("Urgent?").register();
}
};
builder.setResource(resource);
@@ -470,30 +476,31 @@ class SemanticDeclarationDslTest {
} else {
PluginHelper.getRoutesLoader(context).loadRoutes(resource);
}
- assertThat(SemanticQuestions.get(context).isEmpty()).isFalse();
+ assertThat(SemanticEvaluations.get(context).isEmpty()).isFalse();
Files.delete(file);
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
}
});
- assertThat(SemanticQuestions.get(context).isEmpty()).isTrue();
+ assertThat(SemanticEvaluations.get(context).isEmpty()).isTrue();
}
@Test
void semanticNamespaceCanBeUsedInsideStandardXmlRoutes() throws Exception {
- load("questions.xml", document("routes", xmlQuestions().replace("<semantic>",
+ load("evaluations.xml", document("routes", xmlEvaluations().replace("<semantic>",
"<semantic xmlns=\"http://camel.apache.org/schema/semantic\">")));
- assertThat(SemanticQuestions.get(context).get("department").getCriteria()).containsKey("billing");
+ assertThat(SemanticEvaluations.get(context).get("department").getParameters().get("criteria"))
+ .asInstanceOf(InstanceOfAssertFactories.MAP).containsKey("billing");
}
@Test
void failedOrdinaryReplacementKeepsPreviousDeclarations() throws Exception {
- load("questions.xml", xmlQuestions());
- var previous = SemanticQuestions.get(context).get("department");
- assertThatThrownBy(() -> load("questions.xml", "<routes><route><wrong/></route></routes>"))
+ load("evaluations.xml", xmlEvaluations());
+ var previous = SemanticEvaluations.get(context).get("department");
+ assertThatThrownBy(() -> load("evaluations.xml", "<routes><route><wrong/></route></routes>"))
.isInstanceOf(Exception.class);
- assertThat(SemanticQuestions.get(context).get("department")).isSameAs(previous);
+ assertThat(SemanticEvaluations.get(context).get("department")).isSameAs(previous);
}
private void load(String location, String xml) throws Exception {
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEipTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEipTest.java
index 9b3693e329fc..29550e4ddb4a 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEipTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEipTest.java
@@ -44,11 +44,11 @@ class SemanticEipTest extends CamelTestSupport {
return new RouteBuilder() {
public void configure() {
context.getRegistry().bind("classifier", new TestSemanticAdapter() {
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
}
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
- String purpose = question.getInstructions();
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) {
+ String purpose = (String) evaluation.getParameters().get("instructions");
calls.computeIfAbsent(purpose, k -> new AtomicInteger()).incrementAndGet();
String text = state.toString();
Object value = switch (purpose) {
@@ -61,18 +61,18 @@ class SemanticEipTest extends CamelTestSupport {
}
case "unfinished" -> Integer.parseInt(text) < 3;
case "urgency" -> text.contains("urgent") ? 2.0 : 0.0;
- default -> throw new IllegalArgumentException("Unknown question");
+ default -> throw new IllegalArgumentException("Unknown evaluation");
};
return new SemanticResult(value, null, null, null, Map.of("provider", "fixture"));
}
});
((SemanticLanguage) context.resolveLanguage("semantic")).setAdapter("classifier");
- SemanticQuestions.get(context).replace("test", Map.of(
- "department", question("department", SemanticQuestion.Type.CHOICE),
- "actionable", question("actionable", SemanticQuestion.Type.BOOLEAN),
- "complete", question("complete", SemanticQuestion.Type.BOOLEAN),
- "unfinished", question("unfinished", SemanticQuestion.Type.BOOLEAN),
- "urgency", question("urgency", SemanticQuestion.Type.SCORE)));
+ SemanticEvaluations.get(context).replace("test", Map.of(
+ "department", evaluation("department", "choice"),
+ "actionable", evaluation("actionable", "boolean"),
+ "complete", evaluation("complete", "boolean"),
+ "unfinished", evaluation("unfinished", "boolean"),
+ "urgency", evaluation("urgency", "score")));
from("direct:batch").setProperty("decision").language("semantic", "refs:actionable,department,urgency")
.setHeader("urgency").simple("${exchangeProperty.decision[urgency]}")
@@ -149,13 +149,15 @@ class SemanticEipTest extends CamelTestSupport {
return new LanguageExpression("semantic", "ref:" + name);
}
- private static SemanticQuestion question(String name, SemanticQuestion.Type type) {
- return new SemanticQuestion(
- type, name, null,
- type == SemanticQuestion.Type.CHOICE
- ? Map.of("billing", "Payments", "technical", "Bugs", "general", "Other requests") : null,
- type == SemanticQuestion.Type.SCORE ? List.of("low", "medium", "high") : null,
- 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
+ private static SemanticEvaluation evaluation(String name, String type) {
+ return new SemanticEvaluation(type, null, null, switch (type) {
+ case "boolean" -> Map.of("instructions", name, "threshold", 0.5,
+ "uncertainty", 0.0, "uncertaintyPolicy", "fail");
+ case "choice" -> Map.of("instructions", name, "criteria",
+ Map.of("billing", "Payments", "technical", "Bugs", "general", "Other requests"));
+ case "score" -> Map.of("instructions", name, "criteria", List.of("low", "medium", "high"));
+ default -> throw new IllegalArgumentException("Unknown fixture operation");
+ });
}
@Test
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticQuestionBuilderTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEvaluationBuilderTest.java
similarity index 75%
rename from components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticQuestionBuilderTest.java
rename to components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEvaluationBuilderTest.java
index 60b9c985c19e..5be99c852b98 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticQuestionBuilderTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEvaluationBuilderTest.java
@@ -30,12 +30,12 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
-import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
-class SemanticQuestionBuilderTest {
- static Stream<Consumer<SemanticQuestionBuilder>> conflicts() {
+class SemanticEvaluationBuilderTest {
+ static Stream<Consumer<SemanticEvaluationBuilder>> conflicts() {
return Stream.of(
b -> b.criterion("a", "A").level("B"),
b -> b.level("B").criterion("a", "A"),
@@ -46,14 +46,14 @@ class SemanticQuestionBuilderTest {
@ParameterizedTest
@MethodSource("conflicts")
- void criteriaConflictsHaveDeclarationContext(Consumer<SemanticQuestionBuilder> configure) throws Exception {
+ void criteriaConflictsHaveDeclarationContext(Consumer<SemanticEvaluationBuilder> configure) throws Exception {
try (var context = new DefaultCamelContext()) {
assertThatThrownBy(() -> context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- var question = semanticQuestions(this).expert("selected").evaluation("q").operation("choice");
- configure.accept(question);
- question.register();
+ var evaluation = semanticEvaluations(this).expert("selected").evaluation("q").operation("choice");
+ configure.accept(evaluation);
+ evaluation.register();
}
})).hasMessageContaining("'q'").hasMessageContaining("expert 'selected'")
.hasMessageContaining("Duplicate parameter 'criteria'");
@@ -64,7 +64,7 @@ class SemanticQuestionBuilderTest {
void callerCriteriaAreNeverMutated() throws Exception {
try (var context = new DefaultCamelContext()) {
Map<String, String> supplied = new LinkedHashMap<>(Map.of("a", "A"));
- var builder = new SemanticQuestionBuilder().operation("choice").parameter("criteria", supplied);
+ var builder = new SemanticEvaluationBuilder().operation("choice").parameter("criteria", supplied);
builder.criterion("b", "B");
assertThatThrownBy(() -> builder.build(context)).hasMessageContaining("Duplicate parameter 'criteria'");
assertThat(supplied).containsExactlyEntriesOf(Map.of("a", "A"));
@@ -93,15 +93,29 @@ class SemanticQuestionBuilderTest {
var properties = new Properties();
properties.setProperty("myPolicy", "NON_MATCH");
context.getPropertiesComponent().setInitialProperties(properties);
- var shorthand = new SemanticQuestionBuilder().operation("boolean").uncertaintyPolicy("{{myPolicy}}")
+ var shorthand = new SemanticEvaluationBuilder().operation("boolean").uncertaintyPolicy("{{myPolicy}}")
.build(context);
- var generic = new SemanticQuestionBuilder().operation("custom").parameter("uncertaintyPolicy", "{{myPolicy}}")
+ var generic = new SemanticEvaluationBuilder().operation("custom").parameter("uncertaintyPolicy", "{{myPolicy}}")
.build(context);
assertThat(shorthand.getParameters()).containsEntry("uncertaintyPolicy", "non-match");
assertThat(generic.getParameters()).containsEntry("uncertaintyPolicy", "NON_MATCH");
}
}
+ @Test
+ void stringParametersRemainStringsAndLiteralPromptPlaceholdersCanBeEscaped() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var declaration = new SemanticEvaluationBuilder().operation("custom")
+ .parameter("numberAsText", "{{limit:0.8}}")
+ .parameter("instructions", "Explain \\{{example\\}} and \\{{")
+ .parameter("policy", Map.of("action", "custom-policy"))
+ .build(context);
+ assertThat(declaration.getParameters()).containsEntry("numberAsText", "0.8")
+ .containsEntry("instructions", "Explain {{example}} and {{")
+ .containsEntry("policy", Map.of("action", "custom-policy"));
+ }
+ }
+
@Test
void nullPolicyIsRejectedWithContractContext() throws Exception {
try (var context = new DefaultCamelContext()) {
@@ -110,7 +124,7 @@ class SemanticQuestionBuilderTest {
assertThatThrownBy(() -> context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- semanticQuestions(this).expert("fixed").evaluation("q").operation("boolean")
+ semanticEvaluations(this).expert("fixed").evaluation("q").operation("boolean")
.uncertaintyPolicy(null).register();
}
})).hasMessageContaining("'q'").hasMessageContaining("'fixed'")
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertTest.java
index 7ce0bae85b0e..12172691ae04 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertTest.java
@@ -38,7 +38,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
-import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -99,8 +99,8 @@ class SemanticExpertTest {
context.getRegistry().bind("a-security", registered);
}
String name = aliases ? "a-security" : "security";
- assertThatThrownBy(() -> SemanticQuestions.get(context).replace("test", Map.of("first",
- question(null, "Unsupported", Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5))))
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).replace("test", Map.of("first",
+ evaluation(null, "Unsupported", Map.of(), "boolean", 0.5))))
.hasMessageContaining("evaluation 'first', expert '" + name + "'")
.hasMessageContaining("Unknown parameter 'instructions'");
define(null, null, 0.5);
@@ -128,11 +128,11 @@ class SemanticExpertTest {
void adaptersMustDeclareTheirStaticContract(boolean automatic) {
SemanticAdapter legacy = new SemanticAdapter() {
@Override
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) {
return new SemanticResult(true, null, null, null, null);
}
};
@@ -143,20 +143,20 @@ class SemanticExpertTest {
context.getRegistry().unbind("other");
}
String reference = automatic ? null : "legacy";
- assertThatThrownBy(() -> SemanticQuestions.get(context).replace("test",
- Map.of("first", question(reference, null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5))))
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).replace("test",
+ Map.of("first", evaluation(reference, null, Map.of(), "boolean", 0.5))))
.hasMessageContaining("first").hasMessageContaining("expert 'legacy'")
.hasMessageContaining("@SemanticExpert");
}
@Test
void fixedExpertRejectsInstructionsCriteriaAndChoiceBeforeInference() {
- List<SemanticQuestion> invalid = List.of(
- question("security", "Is it valid?", Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5),
- question("security", null, Map.of("true", "valid"), SemanticQuestion.Type.BOOLEAN, 0.5),
- question("security", null, Map.of("billing", "invoices"), SemanticQuestion.Type.CHOICE, 0.5));
- for (SemanticQuestion question : invalid) {
- assertThatThrownBy(() -> SemanticQuestions.get(context).replace("test", Map.of("first", question)))
+ List<SemanticEvaluation> invalid = List.of(
+ evaluation("security", "Is it valid?", Map.of(), "boolean", 0.5),
+ evaluation("security", null, Map.of("true", "valid"), "boolean", 0.5),
+ evaluation("security", null, Map.of("billing", "invoices"), "choice", 0.5));
+ for (SemanticEvaluation evaluation : invalid) {
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).replace("test", Map.of("first", evaluation)))
.hasMessageContaining("first").hasMessageContaining("security");
}
assertThat(security.calls).isZero();
@@ -166,8 +166,8 @@ class SemanticExpertTest {
void mixedBatchGroupsByIdentityAndPublishesResultsInReferenceOrder() {
context.getRegistry().bind("alias", security);
define("security", "other", 0.5);
- SemanticQuestions.get(context).replace("alias", Map.of("third", question("alias", null, Map.of(),
- SemanticQuestion.Type.BOOLEAN, 0.5)));
+ SemanticEvaluations.get(context).replace("alias", Map.of("third", evaluation("alias", null, Map.of(),
+ "boolean", 0.5)));
Map<?, ?> decisions = language.createExpression("refs:first,second,third").evaluate(exchange, Map.class);
assertThat(decisions.keySet().toArray()).containsExactly("first", "second", "third");
assertThat(decisions.get("first")).isEqualTo(true);
@@ -202,14 +202,12 @@ class SemanticExpertTest {
assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
security.probability = 0.899;
assertThat(expression.evaluate(exchange, Boolean.class)).isFalse();
- SemanticQuestions.get(context).replace("test", Map.of("first", new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, null, null, null, null, 0.9, 0.05,
- SemanticQuestion.UncertaintyPolicy.FAIL, "security")));
+ SemanticEvaluations.get(context).replace("test", Map.of("first", new SemanticEvaluation(
+ "boolean", "security", null, Map.of("threshold", 0.9, "uncertainty", 0.05, "uncertaintyPolicy", "fail"))));
assertThatThrownBy(() -> expression.evaluate(exchange, Boolean.class)).hasMessageContaining("uncertain");
assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
- SemanticQuestions.get(context).replace("test", Map.of("first", new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, null, null, null, null, 0.9, 0.05,
- SemanticQuestion.UncertaintyPolicy.NON_MATCH, "security")));
+ SemanticEvaluations.get(context).replace("test", Map.of("first", new SemanticEvaluation(
+ "boolean", "security", null, Map.of("threshold", 0.9, "uncertainty", 0.05, "uncertaintyPolicy", "non-match"))));
assertThat(expression.evaluate(exchange, Boolean.class)).isFalse();
}
@@ -239,9 +237,9 @@ class SemanticExpertTest {
void changedDeclarationsAreValidatedBeforePublicationAndReselectExpert() {
define("security", "other", 0.5);
var expression = language.createExpression("ref:first");
- var previous = SemanticQuestions.get(context).get("first");
+ var previous = SemanticEvaluations.get(context).get("first");
assertThatThrownBy(() -> define("missing", "other", 0.5)).hasMessageContaining("missing");
- assertThat(SemanticQuestions.get(context).get("first")).isSameAs(previous);
+ assertThat(SemanticEvaluations.get(context).get("first")).isSameAs(previous);
define("other", "security", 0.5);
assertThat(expression.evaluate(exchange, Boolean.class)).isFalse();
assertThat(security.calls).isZero();
@@ -250,22 +248,22 @@ class SemanticExpertTest {
@Test
void reloadValidatesBothInitializedReferencesAndUnusedDeclarations() {
- Map<String, SemanticQuestion> definitions = new LinkedHashMap<>();
+ Map<String, SemanticEvaluation> definitions = new LinkedHashMap<>();
for (String name : List.of("first", "second", "third")) {
- definitions.put(name, question("security", null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5));
+ definitions.put(name, evaluation("security", null, Map.of(), "boolean", 0.5));
}
- definitions.put("draft", question("security", null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5));
- SemanticQuestions questions = SemanticQuestions.get(context);
- questions.replace("test", definitions);
+ definitions.put("draft", evaluation("security", null, Map.of(), "boolean", 0.5));
+ SemanticEvaluations evaluations = SemanticEvaluations.get(context);
+ evaluations.replace("test", definitions);
var single = language.createExpression("ref:first");
var batch = language.createExpression("refs:second,third");
- questions.replace("test", definitions);
+ evaluations.replace("test", definitions);
for (String name : List.of("first", "second", "third", "draft")) {
- Map<String, SemanticQuestion> invalid = new LinkedHashMap<>(definitions);
- invalid.put(name, question("missing", null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5));
- assertThatThrownBy(() -> questions.replace("test", invalid))
+ Map<String, SemanticEvaluation> invalid = new LinkedHashMap<>(definitions);
+ invalid.put(name, evaluation("missing", null, Map.of(), "boolean", 0.5));
+ assertThatThrownBy(() -> evaluations.replace("test", invalid))
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining(name).hasMessageContaining("missing");
- assertThat(questions.get(name)).isSameAs(definitions.get(name));
+ assertThat(evaluations.get(name)).isSameAs(definitions.get(name));
}
assertThat(single.evaluate(exchange, Boolean.class)).isTrue();
assertThat(batch.evaluate(exchange, Map.class)).containsEntry("second", true).containsEntry("third", true);
@@ -296,7 +294,7 @@ class SemanticExpertTest {
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- semanticQuestions(this).question("injection").type("boolean").expert("{{selected.expert}}")
+ semanticEvaluations(this).evaluation("injection").type("boolean").expert("{{selected.expert}}")
.threshold("{{injection.threshold}}").register();
from("direct:expert").setBody().language("semantic", "ref:injection");
}
@@ -305,13 +303,13 @@ class SemanticExpertTest {
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("expert.xml",
"""
<routes xmlns="http://camel.apache.org/schema/xml-io">
- <semantic><question name="injection" type="boolean" expert="{{selected.expert}}"
+ <semantic><evaluation name="injection" type="boolean" expert="{{selected.expert}}"
threshold="{{injection.threshold}}"/></semantic>
<route><from uri="direct:expert"/><setBody><language language="semantic">ref:injection</language></setBody></route>
</routes>
"""));
}
- assertThat(SemanticQuestions.get(context).get("injection").getExpert()).isEqualTo("{{selected.expert}}");
+ assertThat(SemanticEvaluations.get(context).get("injection").getExpert()).isEqualTo("{{selected.expert}}");
assertThat(security.calls).isZero();
try (var template = context.createProducerTemplate()) {
assertThat(template.requestBody("direct:expert", "text", Boolean.class)).isFalse();
@@ -327,7 +325,7 @@ class SemanticExpertTest {
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- semanticQuestions(this).question("invalid").type("boolean").expert("security")
+ semanticEvaluations(this).evaluation("invalid").type("boolean").expert("security")
.threshold(threshold).register();
}
});
@@ -335,7 +333,7 @@ class SemanticExpertTest {
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("invalid.xml",
"""
<semantic xmlns="http://camel.apache.org/schema/semantic">
- <question name="invalid" type="boolean" expert="security" threshold="%s"/>
+ <evaluation name="invalid" type="boolean" expert="security" threshold="%s"/>
</semantic>
""".formatted(threshold)));
}
@@ -349,7 +347,7 @@ class SemanticExpertTest {
assertThatThrownBy(() -> PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("invalid.xml",
"""
<semantic xmlns="http://camel.apache.org/schema/semantic">
- <question name="invalid" type="boolean" expert="security" thresholdd="0.9"/>
+ <evaluation name="invalid" type="boolean" expert="security" thresholdd="0.9"/>
</semantic>
""")))
.hasStackTraceContaining("'invalid'").hasStackTraceContaining("expert 'security'")
@@ -361,8 +359,8 @@ class SemanticExpertTest {
ManagedExpert.initialized = 0;
ManagedExpert.started = 0;
language.setAdapter(ManagedExpert.class.getName());
- assertThatThrownBy(() -> SemanticQuestions.get(context).replace("test", Map.of("first",
- question(null, "Unsupported", Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5))))
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).replace("test", Map.of("first",
+ evaluation(null, "Unsupported", Map.of(), "boolean", 0.5))))
.hasMessageContaining("Unknown parameter 'instructions'");
assertThat(ManagedExpert.initialized).isZero();
assertThat(ManagedExpert.started).isZero();
@@ -392,11 +390,11 @@ class SemanticExpertTest {
}
@Override
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) {
return new SemanticResult(true, null, null, null, null);
}
}
@@ -441,30 +439,40 @@ class SemanticExpertTest {
@Test
void instructionDrivenAndFixedSemanticExpertsShareOneBatch() {
context.getRegistry().bind("general", new SemanticLanguageTest.LabelAdapter());
- SemanticQuestions.get(context).replace("test", Map.of(
- "injection", question("security", null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5),
- "department", question("general", "Which department?", Map.of("billing", "invoices"),
- SemanticQuestion.Type.CHOICE, 0.5)));
+ SemanticEvaluations.get(context).replace("test", Map.of(
+ "injection", evaluation("security", null, Map.of(), "boolean", 0.5),
+ "department", evaluation("general", "Which department?", Map.of("billing", "invoices"),
+ "choice", 0.5)));
var result = language.createExpression("refs:injection,department").evaluate(exchange, Map.class);
assertThat(result).containsEntry("injection", true).containsEntry("department", "billing");
assertThat(security.batches).containsExactly(List.of("injection"));
}
private void define(String first, String second, double threshold) {
- SemanticQuestions.get(context).replace("test", Map.of(
- "first", question(first, null, Map.of(), SemanticQuestion.Type.BOOLEAN, threshold),
- "second", question(second, null, Map.of(), SemanticQuestion.Type.BOOLEAN, threshold)));
+ SemanticEvaluations.get(context).replace("test", Map.of(
+ "first", evaluation(first, null, Map.of(), "boolean", threshold),
+ "second", evaluation(second, null, Map.of(), "boolean", threshold)));
}
- private static SemanticQuestion question(
+ private static SemanticEvaluation evaluation(
String expert, String instructions, Map<String, String> criteria,
- SemanticQuestion.Type type, double threshold) {
- if (type == SemanticQuestion.Type.BOOLEAN && instructions == null && criteria.isEmpty() && threshold == 0.5) {
- return new SemanticQuestion("boolean", expert, null, Map.of());
+ String type, double threshold) {
+ if ("boolean".equals(type) && instructions == null && criteria.isEmpty() && threshold == 0.5) {
+ return new SemanticEvaluation("boolean", expert, null, Map.of());
}
- return new SemanticQuestion(
- type, instructions, null, criteria, List.of(), threshold, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL, expert);
+ Map<String, Object> parameters = new LinkedHashMap<>();
+ if (instructions != null) {
+ parameters.put("instructions", instructions);
+ }
+ if (!criteria.isEmpty()) {
+ parameters.put("criteria", criteria);
+ }
+ if ("boolean".equals(type)) {
+ parameters.put("threshold", threshold);
+ parameters.put("uncertainty", 0.0);
+ parameters.put("uncertaintyPolicy", "fail");
+ }
+ return new SemanticEvaluation(type, expert, null, parameters);
}
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertYamlTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertYamlTest.java
index 48c2357e0f0a..2a5052e8715e 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertYamlTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertYamlTest.java
@@ -33,7 +33,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
class SemanticExpertYamlTest {
private static final String DECLARATIONS = """
- semantic:
- question:
+ evaluation:
injection:
expert: "{{security.expert:security}}"
type: boolean
@@ -51,9 +51,9 @@ class SemanticExpertYamlTest {
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody("text");
assertThat(expression.evaluate(exchange, Boolean.class)).isFalse();
- var question = SemanticQuestions.get(context).get("injection");
- assertThat(question.getExpert()).isEqualTo("{{security.expert:security}}");
- assertThat(question.getInstructions()).isNull();
+ var evaluation = SemanticEvaluations.get(context).get("injection");
+ assertThat(evaluation.getExpert()).isEqualTo("{{security.expert:security}}");
+ assertThat(evaluation.getParameters().get("instructions")).isNull();
assertThat(expert.calls).isEqualTo(1);
}
}
@@ -66,18 +66,18 @@ class SemanticExpertYamlTest {
preParse(context, DECLARATIONS);
context.start();
var expression = context.resolveLanguage("semantic").createExpression("ref:injection");
- var question = SemanticQuestions.get(context).get("injection");
+ var evaluation = SemanticEvaluations.get(context).get("injection");
assertThatThrownBy(() -> preParse(context,
DECLARATIONS.replace("type: boolean", "type: boolean\n instructions: unsupported")))
.isInstanceOfSatisfying(YamlDeserializationException.class, error -> {
assertThat(error).hasMessageContaining("injection").hasMessageContaining("security")
.hasMessageContaining("Unknown parameter 'instructions'");
assertThat(error.getProblemMark()).hasValueSatisfying(mark -> {
- assertThat(mark.getName()).isEqualTo("questions.yaml");
+ assertThat(mark.getName()).isEqualTo("evaluations.yaml");
assertThat(mark.getLine()).isZero();
});
});
- assertThat(SemanticQuestions.get(context).get("injection")).isSameAs(question);
+ assertThat(SemanticEvaluations.get(context).get("injection")).isSameAs(evaluation);
assertThat(expert.calls).isZero();
preParse(context, DECLARATIONS.replace("threshold: \"{{security.threshold:0.9}}\"", "threshold: 0.7"));
var exchange = new DefaultExchange(context);
@@ -113,11 +113,11 @@ class SemanticExpertYamlTest {
preParse(context, DECLARATIONS);
context.start();
var expression = context.resolveLanguage("semantic").createExpression("ref:injection");
- var previous = SemanticQuestions.get(context).get("injection");
+ var previous = SemanticEvaluations.get(context).get("injection");
String replacement = DECLARATIONS.replace("{{security.expert:security}}", "newSecurity");
assertThatThrownBy(() -> preParse(context, replacement))
.isInstanceOf(YamlDeserializationException.class).hasMessageContaining("newSecurity");
- assertThat(SemanticQuestions.get(context).get("injection")).isSameAs(previous);
+ assertThat(SemanticEvaluations.get(context).get("injection")).isSameAs(previous);
context.getRegistry().bind("newSecurity", new FixedExpert());
preParse(context, replacement);
var exchange = new DefaultExchange(context);
@@ -152,10 +152,10 @@ class SemanticExpertYamlTest {
}
private void preParse(DefaultCamelContext context, String yaml) throws Exception {
- var settings = LoadSettings.builder().setLabel("questions.yaml").build();
+ var settings = LoadSettings.builder().setLabel("evaluations.yaml").build();
try (var deserialization = new YamlDeserializationContext(settings)) {
deserialization.setCamelContext(context);
- deserialization.setResource(ResourceHelper.fromString("questions.yaml", yaml));
+ deserialization.setResource(ResourceHelper.fromString("evaluations.yaml", yaml));
deserialization.start();
deserialization.preParse(new Compose(settings).composeString(yaml).orElseThrow());
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
index 65cb98cfc01a..6599143d05f5 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
@@ -40,8 +40,8 @@ class SemanticInitializationTest {
try (var context = new DefaultCamelContext()) {
context.init();
synchronized (context) {
- var questions = callers.submit(() -> SemanticQuestions.get(context));
- assertThat(questions.get(10, TimeUnit.SECONDS)).isSameAs(SemanticQuestions.get(context));
+ var evaluations = callers.submit(() -> SemanticEvaluations.get(context));
+ assertThat(evaluations.get(10, TimeUnit.SECONDS)).isSameAs(SemanticEvaluations.get(context));
}
var language = language(context, Adapter.class);
synchronized (context.getRegistry()) {
@@ -54,15 +54,15 @@ class SemanticInitializationTest {
}
@Test
- void concurrentQuestionRegistryCreationReturnsOneInstance() throws Exception {
+ void concurrentEvaluationRegistryCreationReturnsOneInstance() throws Exception {
ExecutorService callers = Executors.newFixedThreadPool(4);
CountDownLatch start = new CountDownLatch(1);
try (var context = new DefaultCamelContext()) {
- List<Future<SemanticQuestions>> results = new ArrayList<>();
+ List<Future<SemanticEvaluations>> results = new ArrayList<>();
for (int i = 0; i < 8; i++) {
results.add(callers.submit(() -> {
assertThat(start.await(10, TimeUnit.SECONDS)).isTrue();
- return SemanticQuestions.get(context);
+ return SemanticEvaluations.get(context);
}));
}
start.countDown();
@@ -139,10 +139,10 @@ class SemanticInitializationTest {
}
private static SemanticLanguage language(DefaultCamelContext context, Class<? extends SemanticAdapter> type) {
- SemanticQuestions.get(context).replace("test", Map.of("q",
- new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, "Is this valid?", null, null, null, 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL)));
+ SemanticEvaluations.get(context).replace("test", Map.of("q",
+ new SemanticEvaluation(
+ "boolean", null, null, Map.of("instructions", "Is this valid?", "threshold", 0.5, "uncertainty", 0.0,
+ "uncertaintyPolicy", "fail"))));
SemanticLanguage language = new SemanticLanguage();
language.setCamelContext(context);
language.setAdapter(type.getName());
@@ -151,11 +151,11 @@ class SemanticInitializationTest {
public static class Adapter extends TestSemanticAdapter {
@Override
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) {
return new SemanticResult(true, null, null, null, null);
}
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java
index 88671131668c..66bc5e5174bf 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java
@@ -30,6 +30,8 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.camel.Expression;
@@ -69,7 +71,7 @@ class SemanticLanguageTest {
language.setAdapter(CountingAdapter.class.getName());
context.getRegistry().bind("semantic", language);
context.init();
- questions(question(SemanticQuestion.Type.BOOLEAN, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL));
+ evaluations(evaluation("boolean", null, 0.5, 0, "fail"));
}
@AfterEach
@@ -77,18 +79,44 @@ class SemanticLanguageTest {
context.stop();
}
- static SemanticQuestion question(
- SemanticQuestion.Type type, String state, double threshold, double uncertainty,
- SemanticQuestion.UncertaintyPolicy policy) {
- return new SemanticQuestion(
- type, "Classify this message", state,
- type == SemanticQuestion.Type.CHOICE ? Map.of("billing", "Payment", "technical", "Problem") : Map.of(),
- type == SemanticQuestion.Type.SCORE ? List.of("low", "medium", "high") : List.of(), threshold, uncertainty,
- policy);
+ static SemanticEvaluation evaluation(
+ String type, String state, double threshold, double uncertainty,
+ String policy) {
+ return new SemanticEvaluation(type, null, state, switch (type) {
+ case "boolean" -> Map.of("instructions", "Classify this message", "threshold", threshold,
+ "uncertainty", uncertainty, "uncertaintyPolicy", policy);
+ case "choice" -> Map.of("instructions", "Classify this message", "criteria",
+ Map.of("billing", "Payment", "technical", "Problem"));
+ case "score" -> Map.of("instructions", "Classify this message", "criteria", List.of("low", "medium", "high"));
+ default -> throw new IllegalArgumentException("Unknown fixture operation");
+ });
+ }
+
+ private void evaluations(SemanticEvaluation evaluation) {
+ SemanticEvaluations.get(context).replace("test", Map.of("q", evaluation));
}
- private void questions(SemanticQuestion question) {
- SemanticQuestions.get(context).replace("test", Map.of("q", question));
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void warmEvaluationDoesNotAcquireTheLanguageMonitor(boolean registryOwned) throws Exception {
+ if (registryOwned) {
+ context.getRegistry().bind("registered", new CountingAdapter());
+ language.setAdapter("registered");
+ }
+ context.start();
+ Expression expression = language.createExpression("ref:q");
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("content");
+ assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
+ var executor = Executors.newSingleThreadExecutor();
+ try {
+ synchronized (language) {
+ assertThat(executor.submit(() -> expression.evaluate(exchange, Boolean.class)).get(5, TimeUnit.SECONDS))
+ .isTrue();
+ }
+ } finally {
+ executor.shutdownNow();
+ }
}
@Test
@@ -295,10 +323,10 @@ class SemanticLanguageTest {
}
@Test
- void questionStateOverridesDefaultAndMissingStateNeverFallsBack() {
+ void evaluationStateOverridesDefaultAndMissingStateNeverFallsBack() {
language.setDefaultState("${header.fallback}");
- questions(
- question(SemanticQuestion.Type.BOOLEAN, "${header.selected}", 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL));
+ evaluations(
+ evaluation("boolean", "${header.selected}", 0.5, 0, "fail"));
Expression expression = language.createExpression("ref:q");
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody("body");
@@ -311,7 +339,7 @@ class SemanticLanguageTest {
exchange.getMessage().removeHeader("selected");
assertThatThrownBy(() -> expression.evaluate(exchange, Object.class)).hasMessageContaining("Missing selected state");
assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
- questions(question(SemanticQuestion.Type.BOOLEAN, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL));
+ evaluations(evaluation("boolean", null, 0.5, 0, "fail"));
expression.evaluate(exchange, Boolean.class);
assertThat(adapter.state).isEqualTo("fallback");
}
@@ -320,20 +348,20 @@ class SemanticLanguageTest {
void invalidSelectorsAndUnknownReferencesFailBeforeEvaluation() {
assertThatThrownBy(() -> language.createExpression("q")).hasMessageContaining("ref:name");
assertThatThrownBy(() -> language.createExpression("ref:unknown")).hasMessageContaining("Unknown");
- questions(
- question(SemanticQuestion.Type.BOOLEAN, "${invalidFunction}", 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL));
+ evaluations(
+ evaluation("boolean", "${invalidFunction}", 0.5, 0, "fail"));
assertThatThrownBy(() -> language.createExpression("ref:q")).hasMessageContaining("Unknown function: invalidFunction");
}
@ParameterizedTest
@ValueSource(booleans = { false, true })
- void placeholderResolvingToEmptyStateRetainsSingleQuestionBehavior(boolean questionState) {
+ void placeholderResolvingToEmptyStateRetainsSingleEvaluationBehavior(boolean evaluationState) {
Properties properties = new Properties();
properties.setProperty("selected", "");
context.getPropertiesComponent().setInitialProperties(properties);
- if (questionState) {
- questions(question(SemanticQuestion.Type.BOOLEAN, "{{selected}}", 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL));
+ if (evaluationState) {
+ evaluations(evaluation("boolean", "{{selected}}", 0.5, 0,
+ "fail"));
} else {
language.setDefaultState("{{selected}}");
}
@@ -374,8 +402,8 @@ class SemanticLanguageTest {
assertThatThrownBy(() -> expression.evaluate(exchange, Object.class))
.hasCauseInstanceOf(IllegalArgumentException.class).hasMessageContaining("Unsupported state type");
assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
- questions(question(SemanticQuestion.Type.BOOLEAN, "${bodyAs(String)}", 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL));
+ evaluations(evaluation("boolean", "${bodyAs(String)}", 0.5, 0,
+ "fail"));
assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
CountingAdapter adapter
= context.getRegistry().lookupByNameAndType(SemanticLanguage.ADAPTER_NAME, CountingAdapter.class);
@@ -392,9 +420,9 @@ class SemanticLanguageTest {
= context.getRegistry().lookupByNameAndType(SemanticLanguage.ADAPTER_NAME, CountingAdapter.class);
adapter.answer = new SemanticResult(null, 0.5, null, null, null);
assertThat(predicate.matches(exchange)).isTrue();
- questions(question(SemanticQuestion.Type.BOOLEAN, null, 0.5, 0.1, SemanticQuestion.UncertaintyPolicy.FAIL));
+ evaluations(evaluation("boolean", null, 0.5, 0.1, "fail"));
assertThatThrownBy(() -> predicate.matches(exchange)).hasMessageContaining("uncertain");
- questions(question(SemanticQuestion.Type.BOOLEAN, null, 0.5, 0.1, SemanticQuestion.UncertaintyPolicy.NON_MATCH));
+ evaluations(evaluation("boolean", null, 0.5, 0.1, "non-match"));
assertThat(predicate.matches(exchange)).isFalse();
adapter.failure = new IllegalStateException("provider failed");
assertThatThrownBy(() -> predicate.matches(exchange)).isExactlyInstanceOf(RuntimeCamelException.class)
@@ -407,9 +435,9 @@ class SemanticLanguageTest {
AtomicInteger validations = new AtomicInteger();
context.getRegistry().bind("changing", new CountingAdapter() {
@Override
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
if (validations.incrementAndGet() == 1) {
- questions(question(SemanticQuestion.Type.CHOICE, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL));
+ evaluations(evaluation("choice", null, 0.5, 0, "fail"));
}
}
});
@@ -417,7 +445,7 @@ class SemanticLanguageTest {
assertThatThrownBy(() -> language.createPredicate("ref:q"))
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("boolean");
// A failed initialization must not register a predicate constraint.
- questions(question(SemanticQuestion.Type.CHOICE, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL));
+ evaluations(evaluation("choice", null, 0.5, 0, "fail"));
assertThat(language.createExpression("ref:q")).isNotNull();
}
@@ -426,11 +454,11 @@ class SemanticLanguageTest {
void reloadRejectsInvalidStateSelectorsBeforePublication(String selector) throws Exception {
context.start();
Expression expression = language.createExpression("ref:q");
- SemanticQuestion previous = SemanticQuestions.get(context).get("q");
- assertThatThrownBy(() -> questions(
- question(SemanticQuestion.Type.BOOLEAN, selector, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL)))
+ SemanticEvaluation previous = SemanticEvaluations.get(context).get("q");
+ assertThatThrownBy(() -> evaluations(
+ evaluation("boolean", selector, 0.5, 0, "fail")))
.isInstanceOf(RuntimeException.class);
- assertThat(SemanticQuestions.get(context).get("q")).isSameAs(previous);
+ assertThat(SemanticEvaluations.get(context).get("q")).isSameAs(previous);
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody("content");
assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
@@ -441,17 +469,17 @@ class SemanticLanguageTest {
Expression expression = language.createExpression("ref:q");
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody("content");
- questions(
- question(SemanticQuestion.Type.BOOLEAN, "${header.updated}", 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL));
+ evaluations(
+ evaluation("boolean", "${header.updated}", 0.5, 0, "fail"));
exchange.getMessage().setHeader("updated", "new state");
expression.evaluate(exchange, Boolean.class);
CountingAdapter adapter
= context.getRegistry().lookupByNameAndType(SemanticLanguage.ADAPTER_NAME, CountingAdapter.class);
assertThat(adapter.state).isEqualTo("new state");
assertThatThrownBy(
- () -> SemanticQuestions.get(context).replace("other", Map.of("q", SemanticQuestions.get(context).get("q"))))
+ () -> SemanticEvaluations.get(context).replace("other", Map.of("q", SemanticEvaluations.get(context).get("q"))))
.hasMessageContaining("Duplicate");
- SemanticQuestions.get(context).replace("test", Map.of());
+ SemanticEvaluations.get(context).replace("test", Map.of());
assertThatThrownBy(() -> expression.evaluate(exchange, Object.class)).hasMessageContaining("Unknown");
}
@@ -464,14 +492,14 @@ class SemanticLanguageTest {
assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNotNull();
// Registering an expression for the same name must not replace the predicate's validation.
language.createExpression("ref:q");
- SemanticQuestion previous = SemanticQuestions.get(context).get("q");
- assertThatThrownBy(() -> questions(
- question(SemanticQuestion.Type.CHOICE, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL)))
+ SemanticEvaluation previous = SemanticEvaluations.get(context).get("q");
+ assertThatThrownBy(() -> evaluations(
+ evaluation("choice", null, 0.5, 0, "fail")))
.isExactlyInstanceOf(IllegalArgumentException.class).hasMessageContaining("boolean");
- assertThat(SemanticQuestions.get(context).get("q")).isSameAs(previous);
+ assertThat(SemanticEvaluations.get(context).get("q")).isSameAs(previous);
assertThat(predicate.matches(exchange)).isTrue();
exchange.setProperty(SemanticLanguage.RESULT, "old");
- SemanticQuestions.get(context).replace("test", Map.of());
+ SemanticEvaluations.get(context).replace("test", Map.of());
assertThatThrownBy(() -> predicate.matches(exchange))
.isExactlyInstanceOf(IllegalArgumentException.class).hasMessageContaining("Unknown");
assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
@@ -480,7 +508,7 @@ class SemanticLanguageTest {
@Test
void labelOnlyProviderWorksWithoutInventedProbabilitiesAndRejectsUnsupportedKinds() {
language.setAdapter(LabelAdapter.class.getName());
- questions(question(SemanticQuestion.Type.CHOICE, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL));
+ evaluations(evaluation("choice", null, 0.5, 0, "fail"));
Expression expression = language.createExpression("ref:q");
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody("invoice");
@@ -490,8 +518,8 @@ class SemanticLanguageTest {
assertThat(result.getProbabilities()).isEmpty();
assertThat(result.getConfidence()).isNull();
assertThatThrownBy(() -> language.createPredicate("ref:q")).hasMessageContaining("boolean");
- assertThatThrownBy(() -> questions(question(SemanticQuestion.Type.SCORE, null, 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL))).hasMessageContaining("only choice");
+ assertThatThrownBy(() -> evaluations(evaluation("score", null, 0.5, 0,
+ "fail"))).hasMessageContaining("only choice");
assertThat(expression.evaluate(exchange, String.class)).isEqualTo("billing");
}
@@ -508,16 +536,16 @@ class SemanticLanguageTest {
}
@Override
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) {
this.state = state;
if (failure != null) {
throw failure;
}
- return applyPolicy(question, answer);
+ return applyPolicy(evaluation, answer);
}
@Override
@@ -556,14 +584,14 @@ class SemanticLanguageTest {
public static class LabelAdapter extends TestSemanticAdapter {
@Override
- public void validate(SemanticQuestion question) {
- if (question.getType() != SemanticQuestion.Type.CHOICE) {
+ public void validate(SemanticEvaluation evaluation) {
+ if (!"choice".equals(evaluation.getOperation())) {
throw new IllegalArgumentException("Supports only choice");
}
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) {
return new SemanticResult("billing", null, null, null, Map.of("provider", "fixed-classifier"));
}
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticPropertiesTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticPropertiesTest.java
index efc18abe3711..0de84226f8e4 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticPropertiesTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticPropertiesTest.java
@@ -47,9 +47,9 @@ class SemanticPropertiesTest {
main.addProperty("camel.language.semantic.default-state", "${header.selected}");
main.configure().addRoutesBuilder(new RouteBuilder() {
public void configure() {
- SemanticQuestions.get(getContext()).replace("test", Map.of("q",
- SemanticLanguageTest.question(SemanticQuestion.Type.CHOICE, null, 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL)));
+ SemanticEvaluations.get(getContext()).replace("test", Map.of("q",
+ SemanticLanguageTest.evaluation("choice", null, 0.5, 0,
+ "fail")));
from("direct:start").setHeader("answer").language("semantic", "ref:q");
}
});
@@ -83,9 +83,9 @@ class SemanticPropertiesTest {
main.addProperty("camel.language.semantic.adapter", WrongType.class.getName());
main.configure().addRoutesBuilder(new RouteBuilder() {
public void configure() {
- SemanticQuestions.get(getContext()).replace("test", Map.of("q",
- SemanticLanguageTest.question(SemanticQuestion.Type.CHOICE, null, 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL)));
+ SemanticEvaluations.get(getContext()).replace("test", Map.of("q",
+ SemanticLanguageTest.evaluation("choice", null, 0.5, 0,
+ "fail")));
from("direct:start").setHeader("answer").language("semantic", "ref:q");
}
});
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
index aa9f57f60989..4e2a56c52154 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
@@ -54,25 +54,25 @@ class SemanticRetryAndValidationTest extends CamelTestSupport {
public void configure() {
context.getRegistry().bind("evaluator", new TestSemanticAdapter() {
@Override
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) throws Exception {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) throws Exception {
evaluatedStates.add(state);
if (evaluationFailure != null) {
throw evaluationFailure;
}
return malformedResult
? new SemanticResult("unexpected category", null, null, null, null)
- : applyPolicy(question, new SemanticResult(null, probability, null, null, null));
+ : applyPolicy(evaluation, new SemanticResult(null, probability, null, null, null));
}
});
SemanticLanguage language = (SemanticLanguage) context.resolveLanguage("semantic");
language.setAdapter("evaluator");
- SemanticQuestions.get(context).replace("test", Map.of(
- "retryable", question("Is another attempt worthwhile?", "${exchangeProperty.retryState}"),
- "withinScope", question("Does the proposed action serve the approved task?", "${body}")));
+ SemanticEvaluations.get(context).replace("test", Map.of(
+ "retryable", evaluation("Is another attempt worthwhile?", "${exchangeProperty.retryState}"),
+ "withinScope", evaluation("Does the proposed action serve the approved task?", "${body}")));
Predicate retryable = language.createPredicate("ref:retryable");
errorHandler(defaultErrorHandler().maximumRedeliveries(0).logExhausted(false));
@@ -115,10 +115,10 @@ class SemanticRetryAndValidationTest extends CamelTestSupport {
};
}
- private static SemanticQuestion question(String instructions, String state) {
- return new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, instructions, state, null, null,
- 0.8, 0.05, SemanticQuestion.UncertaintyPolicy.FAIL);
+ private static SemanticEvaluation evaluation(String instructions, String state) {
+ return new SemanticEvaluation(
+ "boolean", null, state,
+ Map.of("instructions", instructions, "threshold", 0.8, "uncertainty", 0.05, "uncertaintyPolicy", "fail"));
}
@Test
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java
index d39915fe5d9d..adb8f948a675 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java
@@ -47,9 +47,9 @@ class SemanticXmlAutoDiscoveryTest {
<?xml version="1.0"?>
<routes xmlns="http://camel.apache.org/schema/xml-io">
<semantic xmlns="http://camel.apache.org/schema/semantic">
- <question name="urgent" type="boolean">
+ <evaluation name="urgent" type="boolean">
<instructions>Urgent?</instructions>
- </question>
+ </evaluation>
</semantic>
<route id="located">
<from uri="direct:located"/>
@@ -71,10 +71,10 @@ class SemanticXmlAutoDiscoveryTest {
void mainLoadsOrdinaryXmlWithoutLoaderRegistration(boolean standalone, String filename) throws Exception {
String declarations = """
<semantic xmlns="http://camel.apache.org/schema/semantic">
- <question name="department" type="choice">
+ <evaluation name="department" type="choice">
<instructions>Which department?</instructions>
<criterion key="billing" value="Invoices"/>
- </question>
+ </evaluation>
</semantic>
""";
String route = """
@@ -85,12 +85,12 @@ class SemanticXmlAutoDiscoveryTest {
Path routes = directory.resolve(filename);
String files;
if (standalone) {
- Path questions = directory.resolve(filename.endsWith(".semantic.xml")
- ? "my.questions.semantic.xml" : "my.questions.xml");
- Files.writeString(questions, declarations);
+ Path evaluations = directory.resolve(filename.endsWith(".semantic.xml")
+ ? "my.evaluations.semantic.xml" : "my.evaluations.xml");
+ Files.writeString(evaluations, declarations);
Files.writeString(routes, "<routes>" + route + "</routes>");
// The consumer is deliberately listed first.
- files = routes.toUri() + "," + questions.toUri();
+ files = routes.toUri() + "," + evaluations.toUri();
} else {
Files.writeString(routes, "<routes>" + declarations + route + "</routes>");
files = routes.toUri().toString();
@@ -141,7 +141,7 @@ class SemanticXmlAutoDiscoveryTest {
assertThat(error).hasMessageContaining("invalid.tickets.xml, line 12")
.hasMessageContaining("<semantic/>");
});
- assertThat(SemanticQuestions.get(context).isEmpty()).isTrue();
+ assertThat(SemanticEvaluations.get(context).isEmpty()).isTrue();
}
}
@@ -188,7 +188,7 @@ class SemanticXmlAutoDiscoveryTest {
"""));
assertThat(context.getRouteConfigurationDefinitions()).hasSize(1);
assertThat(context.getRegistry().lookupByName("counter")).isInstanceOf(AtomicInteger.class);
- assertThat(context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)).isNull();
+ assertThat(context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class)).isNull();
try (var template = context.createProducerTemplate()) {
assertThat(template.requestBody("direct:ordinary", "test", Integer.class)).isEqualTo(1);
}
@@ -231,10 +231,11 @@ class SemanticXmlAutoDiscoveryTest {
try (var context = new DefaultCamelContext(new SimpleRegistry())) {
context.getRegistry().bind("fixture", new SemanticLanguageTest.CountingAdapter());
context.start();
- PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("questions.xml", """
- <semantic><question name="urgent" type="boolean"><instructions>Urgent?</instructions></question></semantic>
- """));
- assertThat(SemanticQuestions.get(context).get("urgent").getInstructions()).isEqualTo("Urgent?");
+ PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("evaluations.xml",
+ """
+ <semantic><evaluation name="urgent" type="boolean"><instructions>Urgent?</instructions></evaluation></semantic>
+ """));
+ assertThat(SemanticEvaluations.get(context).get("urgent").getParameters().get("instructions")).isEqualTo("Urgent?");
}
}
@@ -244,11 +245,12 @@ class SemanticXmlAutoDiscoveryTest {
for (int i = 0; i < 2; i++) {
context.getRegistry().bind("fixture", new SemanticLanguageTest.CountingAdapter());
context.start();
- PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("questions.xml",
+ PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("evaluations.xml",
"""
- <semantic><question name="urgent" type="boolean"><instructions>Urgent?</instructions></question></semantic>
+ <semantic><evaluation name="urgent" type="boolean"><instructions>Urgent?</instructions></evaluation></semantic>
"""));
- assertThat(SemanticQuestions.get(context).get("urgent").getInstructions()).isEqualTo("Urgent?");
+ assertThat(SemanticEvaluations.get(context).get("urgent").getParameters().get("instructions"))
+ .isEqualTo("Urgent?");
context.stop();
assertThat(context.getRegistry().lookupByName(SemanticXmlLoader.REGISTRY_KEY)).isNull();
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java
index 84cdde3408b7..59861db1c7b1 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java
@@ -106,7 +106,7 @@ class SemanticXmlLoaderTest {
</routes>
""".formatted(declarations ? """
<semantic>
- <question name="urgent" type="boolean"><instructions>Urgent?</instructions></question>
+ <evaluation name="urgent" type="boolean"><instructions>Urgent?</instructions></evaluation>
</semantic>
""" : "");
try (var context = new DefaultCamelContext()) {
@@ -115,9 +115,10 @@ class SemanticXmlLoaderTest {
context.start();
assertThat(context.getRouteDefinitions()).hasSize(2);
if (declarations) {
- assertThat(SemanticQuestions.get(context).get("urgent").getInstructions()).isEqualTo("Urgent?");
+ assertThat(SemanticEvaluations.get(context).get("urgent").getParameters().get("instructions"))
+ .isEqualTo("Urgent?");
} else {
- assertThat(context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)).isNull();
+ assertThat(context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class)).isNull();
}
try (var template = context.createProducerTemplate()) {
assertThat(template.requestBody("direct:nested", "hello", String.class)).isEqualTo("<semantic/>");
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/TestSemanticAdapter.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/TestSemanticAdapter.java
index 910b589519f8..24c4a73d9fcc 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/TestSemanticAdapter.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/TestSemanticAdapter.java
@@ -86,21 +86,21 @@ import org.apache.camel.support.service.ServiceSupport;
required = true, minSize = 1, maxSize = 10) }) })
abstract class TestSemanticAdapter extends ServiceSupport implements SemanticAdapter {
@Override
- public void validate(SemanticQuestion question) {
- SemanticCapabilities.from(getClass()).validate(question);
+ public void validate(SemanticEvaluation evaluation) {
+ SemanticCapabilities.from(getClass()).validate(evaluation);
}
// This fixture models an expert that owns its probability policy.
- static SemanticResult applyPolicy(SemanticQuestion question, SemanticResult result) {
- if (question.getType() != SemanticQuestion.Type.BOOLEAN || result.getProbability() == null) {
+ static SemanticResult applyPolicy(SemanticEvaluation evaluation, SemanticResult result) {
+ if (!"boolean".equals(evaluation.getOperation()) || result.getProbability() == null) {
return result;
}
double probability = result.getProbability();
- double threshold = ((Number) question.getParameters().getOrDefault("threshold", 0.5)).doubleValue();
- double uncertainty = ((Number) question.getParameters().getOrDefault("uncertainty", 0.0)).doubleValue();
+ double threshold = ((Number) evaluation.getParameters().getOrDefault("threshold", 0.5)).doubleValue();
+ double uncertainty = ((Number) evaluation.getParameters().getOrDefault("uncertainty", 0.0)).doubleValue();
boolean value = probability >= threshold;
if (uncertainty > 0 && probability >= threshold - uncertainty && probability <= threshold + uncertainty) {
- if (!"non-match".equals(question.getParameters().get("uncertaintyPolicy"))) {
+ if (!"non-match".equals(evaluation.getParameters().get("uncertaintyPolicy"))) {
throw new IllegalStateException("Semantic boolean decision is uncertain");
}
value = false;
diff --git a/components/camel-ai/camel-typesafe-ai/src/main/docs/typesafe-ai-component.adoc b/components/camel-ai/camel-typesafe-ai/src/main/docs/typesafe-ai-component.adoc
index 9504107aebe2..4b7591a3a0e6 100644
--- a/components/camel-ai/camel-typesafe-ai/src/main/docs/typesafe-ai-component.adoc
+++ b/components/camel-ai/camel-typesafe-ai/src/main/docs/typesafe-ai-component.adoc
@@ -27,7 +27,7 @@ For Boolean evaluations it applies the requested `threshold`, `uncertainty` and
once and returns both the typed verdict and the provider probability. Omitted policy parameters
use this adapter's defaults: threshold `0.5`, no uncertainty band, and failure for uncertain results.
The semantic runtime validates the result and does not apply a second policy.
-Named boolean, choice and score questions use the common contract and inherit this component's
+Named boolean, choice and score evaluations use the common contract and inherit this component's
credentials, model and transport settings without requiring a TypeSafe AI endpoint in the route.
[source,xml]
diff --git a/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java b/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java
index f7df36b14063..6f6c99dcf8c2 100644
--- a/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java
+++ b/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java
@@ -25,17 +25,17 @@ import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.semantic.SemanticAdapter;
import org.apache.camel.semantic.SemanticCapabilities;
+import org.apache.camel.semantic.SemanticEvaluation;
import org.apache.camel.semantic.SemanticExpert;
import org.apache.camel.semantic.SemanticExpert.InputType;
import org.apache.camel.semantic.SemanticExpert.ResultType;
import org.apache.camel.semantic.SemanticOperation;
import org.apache.camel.semantic.SemanticParameter;
-import org.apache.camel.semantic.SemanticQuestion;
import org.apache.camel.semantic.SemanticResult;
import org.apache.camel.spi.annotations.JdkService;
import org.apache.camel.util.json.JsonObject;
-/** Maps common questions to TypeSafe AI using the component's configured, managed transport. */
+/** Maps semantic evaluations to TypeSafe AI using the component's configured, managed transport. */
@JdkService("semantic-adapter")
@SemanticExpert(name = "typesafe-ai", provider = "typesafe-ai", artifactId = "camel-typesafe-ai",
description = "Instruction-driven decisions using TypeSafe AI",
@@ -112,14 +112,14 @@ public class TypeSafeAiSemanticAdapter implements SemanticAdapter, CamelContextA
}
@Override
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation question) {
SemanticCapabilities.from(getClass()).validate(question);
- if (question.getCriteria().entrySet().stream().anyMatch(entry -> entry.getKey().isBlank() || entry.getValue().isBlank())
- || question.getLevels().stream().anyMatch(String::isBlank)) {
+ if (criteria(question).entrySet().stream().anyMatch(entry -> entry.getKey().isBlank() || entry.getValue().isBlank())
+ || levels(question).stream().anyMatch(String::isBlank)) {
throw new IllegalArgumentException("Parameter 'criteria' requires nonblank names and descriptions");
}
- if (question.getType() == SemanticQuestion.Type.BOOLEAN) {
- if (question.getCriteria().keySet().stream().anyMatch(key -> !key.equals("true") && !key.equals("false"))) {
+ if ("boolean".equals(question.getOperation())) {
+ if (criteria(question).keySet().stream().anyMatch(key -> !key.equals("true") && !key.equals("false"))) {
throw new IllegalArgumentException("Parameter 'criteria' requires true or false keys");
}
double threshold = threshold(question);
@@ -145,12 +145,12 @@ public class TypeSafeAiSemanticAdapter implements SemanticAdapter, CamelContextA
}
@Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) throws Exception {
+ public SemanticResult evaluate(SemanticEvaluation question, Object state) throws Exception {
return evaluateBatch(Map.of("question", question), state).get("question");
}
@Override
- public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticQuestion> questions, Object state) throws Exception {
+ public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticEvaluation> questions, Object state) throws Exception {
Map<String, Object> definitions = new LinkedHashMap<>();
questions.forEach((name, question) -> definitions.put(name, definition(question)));
JsonObject response = endpoint().evaluate(Map.of("state", state, "questions", definitions));
@@ -160,37 +160,37 @@ public class TypeSafeAiSemanticAdapter implements SemanticAdapter, CamelContextA
return results;
}
- private Map<String, Object> definition(SemanticQuestion question) {
+ private Map<String, Object> definition(SemanticEvaluation question) {
validate(question);
Map<String, Object> definition = new HashMap<>();
- definition.put("instructions", question.getInstructions());
+ definition.put("instructions", question.getParameters().get("instructions"));
definition.put("type", type(question));
- if (question.getType() == SemanticQuestion.Type.SCORE) {
- definition.put("criteria", question.getLevels());
- } else if (!question.getCriteria().isEmpty()) {
- definition.put("criteria", question.getCriteria());
+ if ("score".equals(question.getOperation())) {
+ definition.put("criteria", levels(question));
+ } else if (!criteria(question).isEmpty()) {
+ definition.put("criteria", criteria(question));
}
return definition;
}
- private String type(SemanticQuestion question) {
- return switch (question.getType()) {
- case BOOLEAN -> "noul";
- case CHOICE -> "choice";
- case SCORE -> "score";
+ private String type(SemanticEvaluation question) {
+ return switch (question.getOperation()) {
+ case "boolean" -> "noul";
+ case "choice" -> "choice";
+ case "score" -> "score";
default -> throw new IllegalArgumentException("Unsupported TypeSafe AI operation");
};
}
- private SemanticResult result(SemanticQuestion question, JsonObject answer, JsonObject response) {
+ private SemanticResult result(SemanticEvaluation question, JsonObject answer, JsonObject response) {
Map<String, Double> probabilities = new HashMap<>();
if (answer.get("probabilities") instanceof Map<?, ?> values) {
values.forEach((key, value) -> probabilities.put((String) key, ((Number) value).doubleValue()));
}
- Double probability = question.getType() == SemanticQuestion.Type.BOOLEAN ? answer.getDouble("noul") : null;
- Object value = switch (question.getType()) {
- case BOOLEAN -> booleanDecision(question, probability);
- case SCORE -> answer.getDouble("score");
+ Double probability = "boolean".equals(question.getOperation()) ? answer.getDouble("noul") : null;
+ Object value = switch (question.getOperation()) {
+ case "boolean" -> booleanDecision(question, probability);
+ case "score" -> answer.getDouble("score");
default -> answer.get(type(question));
};
SemanticResult result = new SemanticResult(
@@ -199,27 +199,39 @@ public class TypeSafeAiSemanticAdapter implements SemanticAdapter, CamelContextA
probabilities, answer.get("confidence") == null ? null : answer.getDouble("confidence"),
Map.of("provider", "typesafe-ai", "model", response.get("model"), "usage", response.get("usage")));
SemanticCapabilities.from(getClass()).operation(question.getOperation()).validateResult(result);
- if (question.getType() == SemanticQuestion.Type.CHOICE
- && (!question.getCriteria().containsKey(value)
- || !probabilities.isEmpty() && !probabilities.keySet().equals(question.getCriteria().keySet()))) {
+ if ("choice".equals(question.getOperation())
+ && (!criteria(question).containsKey(value)
+ || !probabilities.isEmpty() && !probabilities.keySet().equals(criteria(question).keySet()))) {
throw new IllegalArgumentException("TypeSafe AI choice result must match the supplied criteria");
}
- if (question.getType() == SemanticQuestion.Type.SCORE
- && ((Number) value).doubleValue() > question.getLevels().size() - 1) {
+ if ("score".equals(question.getOperation())
+ && ((Number) value).doubleValue() > levels(question).size() - 1) {
throw new IllegalArgumentException("TypeSafe AI score result must be within the supplied levels");
}
return result;
}
- private static double threshold(SemanticQuestion question) {
+ @SuppressWarnings("unchecked")
+ private static Map<String, String> criteria(SemanticEvaluation evaluation) {
+ return evaluation.getParameters().get("criteria") instanceof Map<?, ?> map
+ ? (Map<String, String>) map : Map.of();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<String> levels(SemanticEvaluation evaluation) {
+ return evaluation.getParameters().get("criteria") instanceof List<?> list
+ ? (List<String>) list : List.of();
+ }
+
+ private static double threshold(SemanticEvaluation question) {
return ((Number) question.getParameters().getOrDefault("threshold", 0.5)).doubleValue();
}
- private static double uncertainty(SemanticQuestion question) {
+ private static double uncertainty(SemanticEvaluation question) {
return ((Number) question.getParameters().getOrDefault("uncertainty", 0.0)).doubleValue();
}
- private static boolean booleanDecision(SemanticQuestion question, double probability) {
+ private static boolean booleanDecision(SemanticEvaluation question, double probability) {
double threshold = threshold(question);
double uncertainty = uncertainty(question);
if (uncertainty > 0 && probability >= threshold - uncertainty && probability <= threshold + uncertainty) {
diff --git a/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiApiPathTest.java b/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiApiPathTest.java
index ce01b1ed80f1..f084ad8b550b 100644
--- a/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiApiPathTest.java
+++ b/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiApiPathTest.java
@@ -16,14 +16,13 @@
*/
package org.apache.camel.component.typesafeai;
-import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.main.Main;
-import org.apache.camel.semantic.SemanticQuestion;
-import org.apache.camel.semantic.SemanticQuestions;
+import org.apache.camel.semantic.SemanticEvaluation;
+import org.apache.camel.semantic.SemanticEvaluations;
import org.apache.camel.util.json.JsonObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
@@ -95,9 +94,11 @@ class TypeSafeAiApiPathTest extends TypeSafeAiTestSupport {
@Override
public void configure() {
if (semantic) {
- SemanticQuestions.get(getContext()).replace("test", Map.of("refund", new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, "Refund requested?", null,
- Map.of(), List.of(), 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL)));
+ SemanticEvaluations.get(getContext()).replace("test",
+ Map.of("refund",
+ new SemanticEvaluation(
+ "boolean", null, null, Map.of("instructions", "Refund requested?", "threshold", 0.5,
+ "uncertainty", 0.0, "uncertaintyPolicy", "fail"))));
from("direct:request").setBody().language("semantic", "ref:refund");
} else {
from("direct:request").to("typesafe-ai:configured");
diff --git a/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java b/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
index e72334809c7a..c637ba51db2e 100644
--- a/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
+++ b/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
@@ -25,8 +25,8 @@ import org.apache.camel.Exchange;
import org.apache.camel.Expression;
import org.apache.camel.language.semantic.SemanticLanguage;
import org.apache.camel.semantic.SemanticCapabilities;
-import org.apache.camel.semantic.SemanticQuestion;
-import org.apache.camel.semantic.SemanticQuestions;
+import org.apache.camel.semantic.SemanticEvaluation;
+import org.apache.camel.semantic.SemanticEvaluations;
import org.apache.camel.semantic.SemanticResult;
import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.PluginHelper;
@@ -47,7 +47,7 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
respond = request -> result(Map.of("question", Map.of("type", "noul", "noul", probability)));
var adapter = new TypeSafeAiSemanticAdapter();
adapter.setCamelContext(context);
- var question = new SemanticQuestion(
+ var question = new SemanticEvaluation(
"boolean", null, null, Map.of(
"instructions", "Classify", "threshold", 0.5, "uncertainty", 0.25, "uncertaintyPolicy", policy));
if (policy.equals("fail")) {
@@ -67,7 +67,7 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
respond = request -> result(Map.of("question", Map.of("type", "noul", "noul", probability)));
var adapter = new TypeSafeAiSemanticAdapter();
adapter.setCamelContext(context);
- var question = new SemanticQuestion(
+ var question = new SemanticEvaluation(
"boolean", null, null,
Map.of("instructions", "Classify", "threshold", 0.5, "uncertainty", 0.25));
assertThat(adapter.evaluate(question, "text").getValue()).isEqualTo(expected);
@@ -77,7 +77,7 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
@ValueSource(doubles = { 0.1, 0.9 })
void invalidUncertaintyBandFailsWithoutTransport(double threshold) {
var adapter = new TypeSafeAiSemanticAdapter();
- var question = new SemanticQuestion(
+ var question = new SemanticEvaluation(
"boolean", null, null,
Map.of("instructions", "Classify", "threshold", threshold, "uncertainty", 0.25));
assertThatThrownBy(() -> adapter.validate(question)).isInstanceOf(IllegalArgumentException.class)
@@ -90,7 +90,7 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
respond = request -> result(Map.of("question", Map.of("type", "noul", "noul", 0.5)));
var adapter = new TypeSafeAiSemanticAdapter();
adapter.setCamelContext(context);
- var question = new SemanticQuestion("boolean", null, null, Map.of("instructions", "Classify"));
+ var question = new SemanticEvaluation("boolean", null, null, Map.of("instructions", "Classify"));
assertThat(adapter.evaluate(question, "text").getValue()).isEqualTo(true);
assertThat(question.getParameters()).containsOnlyKeys("instructions");
}
@@ -106,9 +106,8 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
assertThat(capabilities.getOperations()).containsOnlyKeys("boolean", "choice", "score");
assertThat(capabilities.operation("boolean").isProbability()).isTrue();
assertThat(capabilities.operation("boolean").isConfidence()).isFalse();
- SemanticQuestion fixed = new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, null, null,
- null, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
+ SemanticEvaluation fixed = new SemanticEvaluation(
+ "boolean", null, null, Map.of("threshold", 0.5, "uncertainty", 0.0, "uncertaintyPolicy", "fail"));
assertThatThrownBy(() -> adapter.validate(fixed)).hasMessageContaining("Parameter 'instructions' is required");
assertThat(requests).isEmpty();
}
@@ -119,13 +118,12 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
};
assertThat(SemanticCapabilities.from(subclass.getClass())).usingRecursiveComparison()
.isEqualTo(SemanticCapabilities.from(TypeSafeAiSemanticAdapter.class));
- SemanticQuestion missing = new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, null, null,
- null, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
+ SemanticEvaluation missing = new SemanticEvaluation(
+ "boolean", null, null, Map.of("threshold", 0.5, "uncertainty", 0.0, "uncertaintyPolicy", "fail"));
assertThatThrownBy(() -> subclass.validate(missing)).hasMessageContaining("Parameter 'instructions' is required");
- SemanticQuestion score = new SemanticQuestion(
- SemanticQuestion.Type.SCORE, "Score", null, null,
- IntStream.range(0, 11).mapToObj(i -> "Level " + i).toList(), 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
+ SemanticEvaluation score = new SemanticEvaluation(
+ "score", null, null,
+ Map.of("instructions", "Score", "criteria", IntStream.range(0, 11).mapToObj(i -> "Level " + i).toList()));
assertThatThrownBy(() -> subclass.validate(score))
.hasMessageContaining("Parameter 'criteria' is outside its size constraints");
}
@@ -135,9 +133,9 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
respond = request -> result(Map.of("question", Map.of("type", "noul", "noul", 0.9)));
TypeSafeAiSemanticAdapter adapter = new TypeSafeAiSemanticAdapter();
adapter.setCamelContext(context);
- SemanticQuestion question = new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, "Classify", null,
- Map.of(), List.of(), 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
+ SemanticEvaluation question = new SemanticEvaluation(
+ "boolean", null, null,
+ Map.of("instructions", "Classify", "threshold", 0.5, "uncertainty", 0.0, "uncertaintyPolicy", "fail"));
assertThat(adapter.evaluate(question, "original").getValue()).isEqualTo(true);
assertThat(requests).hasSize(1);
assertThat(authorization).containsExactly("Bearer test-key");
@@ -148,13 +146,11 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
TypeSafeAiSemanticAdapter adapter = new TypeSafeAiSemanticAdapter();
Map<String, String> criteria = IntStream.range(0, 256).boxed()
.collect(Collectors.toMap(Object::toString, i -> "Criterion " + i));
- SemanticQuestion choice = new SemanticQuestion(
- SemanticQuestion.Type.CHOICE, "Classify", null,
- criteria, List.of(), 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
- SemanticQuestion score = new SemanticQuestion(
- SemanticQuestion.Type.SCORE, "Score", null,
- Map.of(), IntStream.range(0, 11).mapToObj(i -> "Level " + i).toList(), 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL);
+ SemanticEvaluation choice
+ = new SemanticEvaluation("choice", null, null, Map.of("instructions", "Classify", "criteria", criteria));
+ SemanticEvaluation score = new SemanticEvaluation(
+ "score", null, null,
+ Map.of("instructions", "Score", "criteria", IntStream.range(0, 11).mapToObj(i -> "Level " + i).toList()));
assertThatThrownBy(() -> adapter.validate(choice)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Parameter 'criteria' is outside its size constraints");
assertThatThrownBy(() -> adapter.evaluate(score, "original")).isInstanceOf(IllegalArgumentException.class)
@@ -162,20 +158,22 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
assertThat(requests).isEmpty();
}
- private Expression expression(SemanticQuestion.Type type) {
- SemanticQuestion question = new SemanticQuestion(
- type, "Classify", null,
- type == SemanticQuestion.Type.CHOICE ? Map.of("billing", "Payments", "technical", "Bugs") : Map.of(),
- type == SemanticQuestion.Type.SCORE ? List.of("low", "high") : List.of(),
- 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
- SemanticQuestions.get(context).replace("test", Map.of("q", question));
+ private Expression expression(String type) {
+ SemanticEvaluation question = new SemanticEvaluation(type, null, null, switch (type) {
+ case "boolean" -> Map.of("instructions", "Classify", "threshold", 0.5,
+ "uncertainty", 0.0, "uncertaintyPolicy", "fail");
+ case "choice" -> Map.of("instructions", "Classify", "criteria", Map.of("billing", "Payments", "technical", "Bugs"));
+ case "score" -> Map.of("instructions", "Classify", "criteria", List.of("low", "high"));
+ default -> throw new IllegalArgumentException("Unknown fixture operation");
+ });
+ SemanticEvaluations.get(context).replace("test", Map.of("q", question));
return context.resolveLanguage("semantic").createExpression("ref:q");
}
@Test
void autoDiscoversAndUsesConfiguredComponentWithoutProviderRoute() {
respond = request -> result(Map.of("question", Map.of("type", "noul", "noul", 0.9)));
- Expression expression = expression(SemanticQuestion.Type.BOOLEAN);
+ Expression expression = expression("boolean");
assertThat(requests).isEmpty();
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody("original");
@@ -196,13 +194,13 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
"confidence", 0.6, "probabilities", Map.of("billing", 0.1, "technical", 0.9))));
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody("outage");
- assertThat(expression(SemanticQuestion.Type.CHOICE).evaluate(exchange, String.class)).isEqualTo("technical");
+ assertThat(expression("choice").evaluate(exchange, String.class)).isEqualTo("technical");
SemanticResult choice = exchange.getProperty(SemanticLanguage.RESULT, SemanticResult.class);
assertThat(choice.getConfidence()).isEqualTo(0.6);
assertThat(choice.getProbabilities()).containsEntry("technical", 0.9);
respond = request -> result(Map.of("question", Map.of("type", "score", "score", 0.7,
"confidence", 0.4, "probabilities", Map.of("0", 0.3, "1", 0.7), "legend", Map.of("0", "low", "1", "high"))));
- assertThat(expression(SemanticQuestion.Type.SCORE).evaluate(exchange, Double.class)).isEqualTo(0.7);
+ assertThat(expression("score").evaluate(exchange, Double.class)).isEqualTo(0.7);
}
private Expression batchExpression() {
@@ -211,17 +209,21 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
}
private void batchQuestions() {
- SemanticQuestions.get(context).replace("test", Map.of(
- "refund", new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, "Refund requested?", null,
- Map.of(), List.of(), 0.95, 0, SemanticQuestion.UncertaintyPolicy.FAIL),
- "department", new SemanticQuestion(
- SemanticQuestion.Type.CHOICE, "Which department?", null,
- Map.of("billing", "Refunds", "technical", "Faults", "other", "Anything else"), List.of(),
- 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL),
- "urgency", new SemanticQuestion(
- SemanticQuestion.Type.SCORE, "How urgent?", null,
- Map.of(), List.of("Routine", "Urgent", "Critical"), 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL)));
+ SemanticEvaluations.get(context).replace("test", Map.of(
+ "refund",
+ new SemanticEvaluation(
+ "boolean", null, null,
+ Map.of("instructions", "Refund requested?", "threshold", 0.95, "uncertainty", 0.0, "uncertaintyPolicy",
+ "fail")),
+ "department",
+ new SemanticEvaluation(
+ "choice", null, null,
+ Map.of("instructions", "Which department?", "criteria",
+ Map.of("billing", "Refunds", "technical", "Faults", "other", "Anything else"))),
+ "urgency",
+ new SemanticEvaluation(
+ "score", null, null,
+ Map.of("instructions", "How urgent?", "criteria", List.of("Routine", "Urgent", "Critical")))));
}
@Test
@@ -318,7 +320,7 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
@ParameterizedTest
@ValueSource(booleans = { false, true })
void providerErrorsClearPreviousResult(boolean batch) {
- Expression expression = batch ? batchExpression() : expression(SemanticQuestion.Type.BOOLEAN);
+ Expression expression = batch ? batchExpression() : expression("boolean");
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody("private-input");
exchange.setProperty(SemanticLanguage.RESULT, "old");
@@ -333,7 +335,7 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
void componentTimeoutBoundsSemanticEvaluation(boolean batch) {
context.getComponent("typesafe-ai", TypeSafeAiComponent.class).getConfiguration().setRequestTimeout(100);
holdHeaders = true;
- Expression expression = batch ? batchExpression() : expression(SemanticQuestion.Type.BOOLEAN);
+ Expression expression = batch ? batchExpression() : expression("boolean");
var exchange = new DefaultExchange(context);
exchange.getMessage().setBody("private-input");
assertThatThrownBy(() -> expression.evaluate(exchange, Object.class))
diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/switch-eip.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/switch-eip.adoc
index 4fab8169deb6..3c51ccd67dae 100644
--- a/core/camel-core-engine/src/main/docs/modules/eips/pages/switch-eip.adoc
+++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/switch-eip.adoc
@@ -169,7 +169,7 @@ handling routes shown above.
[source,yaml]
----
- semantic:
- question:
+ evaluation:
department:
type: choice
instructions: Which department should handle this message?
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateExpression.java
new file mode 100644
index 000000000000..6b351d6e1504
--- /dev/null
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateExpression.java
@@ -0,0 +1,32 @@
+/*
+ * 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.language.simple;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+
+/**
+ * Opts a Simple function into using its predicate contract for negation, including any validation in
+ * {@link #matches(org.apache.camel.Exchange)}. Existing expressions retain value-based truthiness, even when they also
+ * implement {@link Predicate}.
+ */
+public interface SimplePredicateExpression extends Expression, Predicate {
+ @Override
+ default void init(CamelContext context) {
+ }
+}
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
index b4bc64a647ab..ba4fdf74b61f 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
@@ -25,12 +25,12 @@ import org.apache.camel.Predicate;
import org.apache.camel.language.simple.FileExpressionBuilder;
import org.apache.camel.language.simple.SimpleFunctionDispatcher;
import org.apache.camel.language.simple.SimpleFunctionHelper;
+import org.apache.camel.language.simple.SimplePredicateExpression;
import org.apache.camel.language.simple.SimplePredicateParser;
import org.apache.camel.language.simple.SimpleSyntaxHints;
import org.apache.camel.language.simple.functions.DirectFunctionFactory;
import org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.language.simple.types.SimpleToken;
-import org.apache.camel.support.ExpressionToPredicateAdapter;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.builder.ExpressionBuilder;
import org.apache.camel.util.ObjectHelper;
@@ -141,6 +141,10 @@ public class SimpleFunctionExpression extends LiteralExpression {
private Expression createNegatedExpression(CamelContext camelContext, String function) {
final String name = function.substring(1).trim();
final Expression exp = doCreateSimpleExpression(camelContext, name);
+ // Preserve the value-based rule for existing functions: ${body} is true and a missing header is false
+ // (CAMEL-24984). Only functions opting in explicitly use their predicate contract under negation.
+ final Predicate predicate = exp instanceof SimplePredicateExpression p
+ ? p : exchange -> ObjectHelper.evaluateValuePredicate(exp.evaluate(exchange, Object.class));
return new Expression() {
@Override
public void init(CamelContext context) {
@@ -149,8 +153,7 @@ public class SimpleFunctionExpression extends LiteralExpression {
@Override
public <T> T evaluate(Exchange exchange, Class<T> type) {
- // Preserve a function's predicate contract, including semantic's boolean-only guard.
- boolean matches = ExpressionToPredicateAdapter.toPredicate(exp).matches(exchange);
+ boolean matches = predicate.matches(exchange);
return exchange.getContext().getTypeConverter().convertTo(type, exchange, !matches);
}
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java
index e9eeeeb66b6b..95aa736cc4a3 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java
@@ -24,11 +24,12 @@ import org.apache.camel.CamelExchangeException;
import org.apache.camel.Exchange;
import org.apache.camel.Expression;
import org.apache.camel.NoTypeConversionAvailableException;
+import org.apache.camel.Predicate;
import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.language.simple.SimplePredicateExpression;
import org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.language.simple.types.SimpleToken;
import org.apache.camel.language.simple.types.UnaryOperatorType;
-import org.apache.camel.support.ExpressionToPredicateAdapter;
import org.apache.camel.util.ObjectHelper;
/**
@@ -95,6 +96,10 @@ public class UnaryExpression extends BaseSimpleNode {
}
private Expression createNotExpression(CamelContext camelContext, final Expression exp) {
+ // Preserve the value-based rule for existing functions: ${body} is true and a missing header is false
+ // (CAMEL-24984). Only functions opting in explicitly use their predicate contract under negation.
+ final Predicate predicate = exp instanceof SimplePredicateExpression p
+ ? p : exchange -> ObjectHelper.evaluateValuePredicate(exp.evaluate(exchange, Object.class));
return new Expression() {
@Override
public void init(CamelContext context) {
@@ -103,7 +108,7 @@ public class UnaryExpression extends BaseSimpleNode {
@Override
public <T> T evaluate(Exchange exchange, Class<T> type) {
- boolean matches = ExpressionToPredicateAdapter.toPredicate(exp).matches(exchange);
+ boolean matches = predicate.matches(exchange);
return camelContext.getTypeConverter().convertTo(type, exchange, !matches);
}
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/QueryLanguageFunctionFactory.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/QueryLanguageFunctionFactory.java
index 91619d557840..2d1977066b12 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/QueryLanguageFunctionFactory.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/QueryLanguageFunctionFactory.java
@@ -17,12 +17,16 @@
package org.apache.camel.language.simple.functions;
import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
import org.apache.camel.Expression;
import org.apache.camel.NoSuchLanguageException;
+import org.apache.camel.Predicate;
import org.apache.camel.language.simple.MiscExpressionBuilder;
+import org.apache.camel.language.simple.SimplePredicateExpression;
import org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.spi.Language;
import org.apache.camel.spi.SimpleLanguageFunctionFactory;
+import org.apache.camel.support.ExpressionToPredicateAdapter;
import org.apache.camel.support.builder.ExpressionBuilder;
import org.apache.camel.util.StringHelper;
@@ -64,7 +68,8 @@ public final class QueryLanguageFunctionFactory implements SimpleLanguageFunctio
throw new SimpleParserException(
"The semantic function requires camel-semantic on the classpath", index);
}
- return semantic.createExpression("ref:" + name);
+ Expression expression = semantic.createExpression("ref:" + name);
+ return new SemanticExpression(expression, ExpressionToPredicateAdapter.toPredicate(expression));
}
remainder = ifStartsWithReturnRemainder("jq(", function);
@@ -130,6 +135,23 @@ public final class QueryLanguageFunctionFactory implements SimpleLanguageFunctio
return MiscExpressionBuilder.simpleJsonPathExpression(input, exp);
}
+ private record SemanticExpression(Expression expression, Predicate predicate) implements SimplePredicateExpression {
+ @Override
+ public void init(CamelContext context) {
+ expression.init(context);
+ }
+
+ @Override
+ public <T> T evaluate(Exchange exchange, Class<T> type) {
+ return expression.evaluate(exchange, type);
+ }
+
+ @Override
+ public boolean matches(Exchange exchange) {
+ return predicate.matches(exchange);
+ }
+ }
+
private static boolean hasInputSource(String exp) {
return exp.startsWith("header:") || exp.startsWith("property:")
|| exp.startsWith("exchangeProperty:") || exp.startsWith("variable:");
diff --git a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleNotOperatorTest.java b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleNotOperatorTest.java
index d8dc33a9c39d..b128474fbaa3 100644
--- a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleNotOperatorTest.java
+++ b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleNotOperatorTest.java
@@ -19,10 +19,18 @@ package org.apache.camel.language.simple;
import java.util.LinkedHashMap;
import java.util.Map;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
import org.apache.camel.ExchangeTestSupport;
import org.apache.camel.Expression;
import org.apache.camel.Predicate;
+import org.apache.camel.language.simple.ast.BaseSimpleNode;
+import org.apache.camel.language.simple.ast.UnaryExpression;
import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
+import org.apache.camel.language.simple.types.SimpleToken;
+import org.apache.camel.language.simple.types.SimpleTokenType;
+import org.apache.camel.language.simple.types.TokenType;
+import org.apache.camel.support.ExpressionAdapter;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -48,6 +56,45 @@ public class SimpleNotOperatorTest extends ExchangeTestSupport {
return e.evaluate(exchange, Object.class);
}
+ @Test
+ public void testExistingPredicateExpressionStillNegatesItsValue() {
+ assertFalse(negate(new DifferentPredicate()));
+ }
+
+ @Test
+ public void testPredicateExpressionCanOptIntoPredicateNegation() {
+ assertTrue(negate(new OptedInPredicate()));
+ }
+
+ private boolean negate(Expression expression) {
+ SimpleToken token = new SimpleToken(new SimpleTokenType(TokenType.unaryOperator, "!"), 0);
+ UnaryExpression node = new UnaryExpression(token);
+ node.acceptRight(new BaseSimpleNode(token) {
+ @Override
+ public Expression createExpression(CamelContext camelContext, String text) {
+ return expression;
+ }
+ });
+ Expression negated = node.createExpression(context, "!${fixture}");
+ negated.init(context);
+ return negated.evaluate(exchange, Boolean.class);
+ }
+
+ private static class DifferentPredicate extends ExpressionAdapter {
+ @Override
+ public Object evaluate(Exchange exchange) {
+ return "nonempty";
+ }
+
+ @Override
+ public boolean matches(Exchange exchange) {
+ return false;
+ }
+ }
+
+ private static class OptedInPredicate extends DifferentPredicate implements SimplePredicateExpression {
+ }
+
@Test
public void testNegatesAFunction() {
exchange.getMessage().setBody(new LinkedHashMap<>(Map.of("a", 1)));
diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
index eb80a830f9f7..0cb958258c48 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
+++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
@@ -1444,7 +1444,7 @@
},
{
"source": "switch-eip.adoc",
- "yaml": "- semantic:\n question:\n department:\n type: choice\n instructions: Which department should handle this message?\n criteria:\n billing: Invoices, payments and refunds\n technical: Bugs, outages and technical problems\n other: Any other request\n- route:\n from:\n uri: direct:classify\n steps:\n - switch:\n selector:\n language:\n language: semantic\n [...]
+ "yaml": "- semantic:\n evaluation:\n department:\n type: choice\n instructions: Which department should handle this message?\n criteria:\n billing: Invoices, payments and refunds\n technical: Bugs, outages and technical problems\n other: Any other request\n- route:\n from:\n uri: direct:classify\n steps:\n - switch:\n selector:\n language:\n language: semantic\n [...]
}
],
"threads": [
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java
index e10491a9a2e9..be067030d792 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java
@@ -433,7 +433,7 @@ public final class RouteDslConverter {
}
private static final Pattern YAML_SEMANTIC = Pattern.compile("(?m)^-\\s+semantic\\s*:");
- private static final Pattern JAVA_SEMANTIC = Pattern.compile("\\bsemanticQuestions\\s*\\(");
+ private static final Pattern JAVA_SEMANTIC = Pattern.compile("\\bsemanticEvaluations\\s*\\(");
// they live outside the route model the writers export: converting would lose them without a word
private static final String SEMANTIC_REFUSED
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java
index bc952e5cd541..59eb4c257b1b 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java
@@ -164,10 +164,10 @@ class RouteDslConverterTest {
String java = """
import org.apache.camel.builder.RouteBuilder;
- import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+ import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
public class SemanticRoute extends RouteBuilder {
public void configure() {
- semanticQuestions(this).question("urgent").type("boolean").register();
+ semanticEvaluations(this).evaluation("urgent").type("boolean").register();
from("direct:input").log("Hello");
}
}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
index fcee87765f55..4913a5fabcc3 100644
--- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
@@ -74,10 +74,10 @@ class TransformToolsTest {
void anEmptySemanticDeclarationIsRefusedToo(String target) {
String route = """
import org.apache.camel.builder.RouteBuilder;
- import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+ import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
public class EmptySemanticRoute extends RouteBuilder {
public void configure() {
- semanticQuestions(this).register();
+ semanticEvaluations(this).register();
from("direct:input").log("Hello");
}
}
@@ -105,10 +105,10 @@ class TransformToolsTest {
if (source.equals("java")) {
return """
import org.apache.camel.builder.RouteBuilder;
- import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+ import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
public class SemanticRoute extends RouteBuilder {
public void configure() {
- semanticQuestions(this).question("urgent").type("boolean").instructions("Urgent?")
+ semanticEvaluations(this).evaluation("urgent").type("boolean").instructions("Urgent?")
.threshold("%s").uncertainty(0).uncertaintyPolicy("fail").register();
from("direct:input").setBody().language("semantic", "ref:urgent");
}
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
index 54babbdcafc0..bed6c0469899 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
@@ -27,16 +27,16 @@ class SemanticSchemaTest {
void numericDecisionPoliciesAcceptRuntimeScalarForms(boolean canonical) throws Exception {
var validator = new YamlValidator(canonical);
for (String value : new String[] { "0.1", "\"0.1\"", "\"{{limit}}\"", "\"{{limit:0.1}}\"" }) {
- assertThat(validator.validate(booleanQuestion(value)))
+ assertThat(validator.validate(booleanEvaluation(value)))
.as("decision policy %s in canonical mode %s", value, canonical).isEmpty();
}
}
@ParameterizedTest
@ValueSource(booleans = { false, true })
- void scalarConversionsInDifferentQuestionsSelectTheirOwnSchema(boolean canonical) throws Exception {
+ void scalarConversionsInDifferentEvaluationsSelectTheirOwnSchema(boolean canonical) throws Exception {
var validator = new YamlValidator(canonical);
- String questions = booleanQuestion("\"{{limit:0.1}}\"") + """
+ String evaluations = booleanEvaluation("\"{{limit:0.1}}\"") + """
topic:
type: choice
instructions: Select the topic
@@ -46,19 +46,19 @@ class SemanticSchemaTest {
instructions: Assess urgency
criteria: [1, 2, 3]
""";
- assertThat(validator.validate(questions)).isEmpty();
+ assertThat(validator.validate(evaluations)).isEmpty();
}
@ParameterizedTest
@ValueSource(booleans = { false, true })
- void placeholdersDoNotHideInvalidQuestionFields(boolean canonical) throws Exception {
+ void placeholdersDoNotHideInvalidEvaluationFields(boolean canonical) throws Exception {
var validator = new YamlValidator(canonical);
- String question = booleanQuestion("\"{{limit:0.1}}\"");
+ String evaluation = booleanEvaluation("\"{{limit:0.1}}\"");
for (String invalid : new String[] {
- question.replace("threshold:", "thresholdd:"),
- question.replace("threshold: \"{{limit:0.1}}\"", "threshold: not-a-number"),
- question.replace("threshold: \"{{limit:0.1}}\"", "threshold: {value: 0.1}") }) {
- assertThat(validator.validate(invalid)).as("invalid question: %s", invalid).isNotEmpty();
+ evaluation.replace("threshold:", "thresholdd:"),
+ evaluation.replace("threshold: \"{{limit:0.1}}\"", "threshold: not-a-number"),
+ evaluation.replace("threshold: \"{{limit:0.1}}\"", "threshold: {value: 0.1}") }) {
+ assertThat(validator.validate(invalid)).as("invalid evaluation: %s", invalid).isNotEmpty();
}
}
@@ -66,32 +66,30 @@ class SemanticSchemaTest {
@ValueSource(booleans = { false, true })
void expertContractsOwnOperationNamesAndRequiredParameters(boolean canonical) throws Exception {
var validator = new YamlValidator(canonical);
- for (String declaration : new String[] { "question", "evaluation" }) {
- String yaml = """
- - semantic:
- expert: content
- state: "${body}"
- %s:
- safety:
- operation: classify
- parameters:
- policy:
- labels: [privacy, unsafe]
- limit: 2
- enabled: true
- department:
- type: choice
- instructions: Department?
- criteria: {billing: Invoices, support: Questions}
- score:
- operation: rank
- criteria: [Low, High]
- """.formatted(declaration);
- assertThat(validator.validate(yaml)).isEmpty();
- assertThat(validator.validate(yaml.replace("parameters:", "paramters:"))).isNotEmpty();
- assertThat(validator.validate(yaml.replace("parameters:", "parameters: []\n ignored:")))
- .isNotEmpty();
- }
+ String yaml = """
+ - semantic:
+ expert: content
+ state: "${body}"
+ evaluation:
+ safety:
+ operation: classify
+ parameters:
+ policy:
+ labels: [privacy, unsafe]
+ limit: !number "{{limit:2}}"
+ enabled: true
+ department:
+ type: choice
+ instructions: Department?
+ criteria: {billing: Invoices, support: Questions}
+ score:
+ operation: rank
+ criteria: [Low, High]
+ """;
+ assertThat(validator.validate(yaml)).isEmpty();
+ assertThat(validator.validate(yaml.replace("parameters:", "paramters:"))).isNotEmpty();
+ assertThat(validator.validate(yaml.replace("parameters:", "parameters: []\n ignored:")))
+ .isNotEmpty();
}
@ParameterizedTest
@@ -99,15 +97,16 @@ class SemanticSchemaTest {
void semanticRequiresExactlyOneDeclarationBlock(boolean canonical) throws Exception {
var validator = new YamlValidator(canonical);
assertThat(validator.validate("- semantic: {expert: content}")).isNotEmpty();
+ assertThat(validator.validate("- semantic: {question: {}}")).isNotEmpty();
assertThat(validator.validate("- semantic: {question: {}, evaluation: {}}")).isNotEmpty();
assertThat(validator.validate("- semantic: {evaluation: {q: {operation: detect, uncertaintyPolicy: expert-policy}}}"))
.isEmpty();
}
- private static String booleanQuestion(String value) {
+ private static String booleanEvaluation(String value) {
return """
- semantic:
- question:
+ evaluation:
actionable:
type: boolean
instructions: Is this actionable?
@@ -118,11 +117,11 @@ class SemanticSchemaTest {
@ParameterizedTest
@ValueSource(booleans = { false, true })
- void questionSyntaxIsClosedAndExpertConstraintsAreValidatedAtRuntime(boolean canonical) throws Exception {
+ void evaluationSyntaxIsClosedAndExpertConstraintsAreValidatedAtRuntime(boolean canonical) throws Exception {
YamlValidator validator = new YamlValidator(canonical);
String choice = """
- semantic:
- question:
+ evaluation:
topic:
type: choice
instructions: Select the topic
@@ -137,11 +136,11 @@ class SemanticSchemaTest {
choice.replace("instructions:", "typo:"),
choice.replace("instructions:", "uncertainty-policy: fail\n instructions:") }) {
assertThat(validator.validate(invalid)).isNotEmpty().allSatisfy(
- error -> assertThat(error.getInstanceLocation().toString()).startsWith("/0/semantic/question/topic"));
+ error -> assertThat(error.getInstanceLocation().toString()).startsWith("/0/semantic/evaluation/topic"));
}
assertThat(validator.validate("""
- semantic:
- question:
+ evaluation:
actionable:
type: boolean
instructions: Is this actionable?
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
index 3477e3878693..1730c3808dc1 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
@@ -13580,28 +13580,6 @@
}
},
"org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer" : {
- "type" : "object",
- "anyOf" : [ {
- "oneOf" : [ {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionBlockSchema"
- }, {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationBlockSchema"
- } ]
- } ]
- },
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$BlockSchema" : {
- "type" : "object",
- "additionalProperties" : false,
- "properties" : {
- "expert" : {
- "type" : "string"
- },
- "state" : {
- "type" : "string"
- }
- }
- },
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationBlockSchema" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
@@ -13708,25 +13686,6 @@
"type" : "string"
}
}
- },
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionBlockSchema" : {
- "type" : "object",
- "additionalProperties" : false,
- "properties" : {
- "expert" : {
- "type" : "string"
- },
- "question" : {
- "type" : "object",
- "additionalProperties" : {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema"
- }
- },
- "state" : {
- "type" : "string"
- }
- },
- "required" : [ "question" ]
}
},
"properties" : {
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
index 6a67d3001303..4fe171054c8c 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
@@ -20525,34 +20525,6 @@
},
"semantic" : {
"isListItem" : true,
- "children" : [ {
- "name" : "expert",
- "type" : "string"
- }, {
- "name" : "question",
- "type" : "map",
- "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
- "required" : true
- }, {
- "name" : "state",
- "type" : "string"
- }, {
- "name" : "evaluation",
- "type" : "map",
- "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
- "required" : true
- } ]
- },
- "semanticDefinitionDeserializer$BlockSchema" : {
- "children" : [ {
- "name" : "expert",
- "type" : "string"
- }, {
- "name" : "state",
- "type" : "string"
- } ]
- },
- "semanticDefinitionDeserializer$EvaluationBlockSchema" : {
"children" : [ {
"name" : "evaluation",
"type" : "map",
@@ -20665,20 +20637,6 @@
"name" : "uncertaintyPolicy",
"type" : "string"
} ]
- },
- "semanticDefinitionDeserializer$QuestionBlockSchema" : {
- "children" : [ {
- "name" : "expert",
- "type" : "string"
- }, {
- "name" : "question",
- "type" : "map",
- "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
- "required" : true
- }, {
- "name" : "state",
- "type" : "string"
- } ]
}
}
}
\ No newline at end of file
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
index cae51c5e06a0..9b46684876a6 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
@@ -16480,28 +16480,6 @@
}
},
"org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer" : {
- "type" : "object",
- "anyOf" : [ {
- "oneOf" : [ {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionBlockSchema"
- }, {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationBlockSchema"
- } ]
- } ]
- },
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$BlockSchema" : {
- "type" : "object",
- "additionalProperties" : false,
- "properties" : {
- "expert" : {
- "type" : "string"
- },
- "state" : {
- "type" : "string"
- }
- }
- },
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationBlockSchema" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
@@ -16609,25 +16587,6 @@
}
}
},
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionBlockSchema" : {
- "type" : "object",
- "additionalProperties" : false,
- "properties" : {
- "expert" : {
- "type" : "string"
- },
- "question" : {
- "type" : "object",
- "additionalProperties" : {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema"
- }
- },
- "state" : {
- "type" : "string"
- }
- },
- "required" : [ "question" ]
- },
"org.apache.camel.dsl.yaml.deserializers.OutputAwareFromDefinition$Inline" : {
"type" : "object",
"properties" : {
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java
index 9f6d4a306905..e7f85c7bf8a4 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java
@@ -36,6 +36,7 @@ import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.builder.RouteConfigurationBuilder;
import org.apache.camel.dsl.yaml.common.YamlDeserializationContext;
import org.apache.camel.dsl.yaml.common.YamlDeserializerSupport;
+import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
import org.apache.camel.dsl.yaml.deserializers.OutputAwareFromDefinition;
import org.apache.camel.model.InterceptDefinition;
import org.apache.camel.model.InterceptFromDefinition;
@@ -128,7 +129,14 @@ public class YamlRoutesBuilderLoader extends YamlRoutesBuilderLoaderSupport {
protected RouteBuilder builder(final YamlDeserializationContext ctx, final Node root) {
// All resources have prepared their beans before declarations are validated and routes constructed.
setDeserializationContext(root, ctx);
- ctx.preParse(root);
+ try {
+ ctx.preParse(root);
+ } catch (YamlDeserializationException e) {
+ // Keep the precise node location already supplied by the deserializer.
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeCamelException("Error pre-parsing resource: " + ctx.getResource().getLocation(), e);
+ }
// we need to keep track of already configured items as the yaml-dsl returns a
// RouteConfigurationBuilder that is capable of both route and route
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticEvaluationTest.java
similarity index 81%
rename from dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java
rename to dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticEvaluationTest.java
index 7a3b3674f595..d40c0c3c2c18 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticEvaluationTest.java
@@ -30,11 +30,11 @@ import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
import org.apache.camel.dsl.yaml.support.YamlTestSupport;
import org.apache.camel.language.semantic.SemanticLanguage;
import org.apache.camel.semantic.SemanticAdapter;
+import org.apache.camel.semantic.SemanticEvaluation;
+import org.apache.camel.semantic.SemanticEvaluations;
import org.apache.camel.semantic.SemanticExpert;
import org.apache.camel.semantic.SemanticOperation;
import org.apache.camel.semantic.SemanticParameter;
-import org.apache.camel.semantic.SemanticQuestion;
-import org.apache.camel.semantic.SemanticQuestions;
import org.apache.camel.semantic.SemanticResult;
import org.apache.camel.spi.Resource;
import org.apache.camel.support.DefaultExchange;
@@ -48,11 +48,11 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
-import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.apache.camel.semantic.SemanticEvaluationsBuilder.semanticEvaluations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
-class SemanticQuestionTest extends YamlTestSupport {
+class SemanticEvaluationTest extends YamlTestSupport {
@TempDir
Path directory;
private final AtomicInteger calls = new AtomicInteger();
@@ -69,7 +69,7 @@ class SemanticQuestionTest extends YamlTestSupport {
inputRequirements = "Text", resultType = SemanticExpert.ResultType.CHOICE,
resultMeaning = "Department",
parameters = {
- @SemanticParameter(name = "instructions", description = "Question", required = true,
+ @SemanticParameter(name = "instructions", description = "Evaluation", required = true,
minSize = 1),
@SemanticParameter(name = "criteria", description = "Labels", type = Map.class,
itemType = String.class,
@@ -78,7 +78,7 @@ class SemanticQuestionTest extends YamlTestSupport {
inputRequirements = "Text", resultType = SemanticExpert.ResultType.BOOLEAN,
resultMeaning = "Decision",
parameters = {
- @SemanticParameter(name = "instructions", description = "Question", required = true,
+ @SemanticParameter(name = "instructions", description = "Evaluation", required = true,
minSize = 1),
@SemanticParameter(name = "threshold", description = "Threshold", type = Number.class,
minimum = 0,
@@ -93,16 +93,16 @@ class SemanticQuestionTest extends YamlTestSupport {
inputRequirements = "Text", resultType = SemanticExpert.ResultType.SCORE,
resultMeaning = "Score",
parameters = {
- @SemanticParameter(name = "instructions", description = "Question", required = true,
+ @SemanticParameter(name = "instructions", description = "Evaluation", required = true,
minSize = 1),
@SemanticParameter(name = "criteria", description = "Levels", type = List.class,
itemType = String.class,
required = true, minSize = 2) }) })
private class Classifier implements SemanticAdapter {
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
}
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) {
calls.incrementAndGet();
selected = state;
return new SemanticResult(
@@ -113,7 +113,7 @@ class SemanticQuestionTest extends YamlTestSupport {
private static String declarations(String state) {
return """
- semantic:
- question:
+ evaluation:
department:
type: choice
state: %s
@@ -136,12 +136,12 @@ class SemanticQuestionTest extends YamlTestSupport {
resultType = SemanticExpert.ResultType.CLASSIFICATION,
resultMeaning = "Content labels", labels = "privacy") })
public static class SecurityExpert implements SemanticAdapter {
- public void validate(SemanticQuestion question) {
+ public void validate(SemanticEvaluation evaluation) {
}
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) {
return new SemanticResult(
- question.getOperation().equals("injection")
+ evaluation.getOperation().equals("injection")
? state.toString().contains("injection") : Set.of("privacy"),
null, null, null, null);
}
@@ -154,7 +154,7 @@ class SemanticQuestionTest extends YamlTestSupport {
String beans = """
- beans:
- name: newSecurity
- type: org.apache.camel.dsl.yaml.SemanticQuestionTest$SecurityExpert
+ type: org.apache.camel.dsl.yaml.SemanticEvaluationTest$SecurityExpert
""";
String declarations = """
- semantic:
@@ -214,15 +214,15 @@ class SemanticQuestionTest extends YamlTestSupport {
context.addRoutes(new RouteBuilder() {
@Override
public void configure() {
- semanticQuestions(this).question("javaDepartment").type("choice").state("${header.selected}")
+ semanticEvaluations(this).evaluation("javaDepartment").type("choice").state("${header.selected}")
.instructions("Which department?")
.criterion("billing", "Invoices and refunds").criterion("technical", "Bugs and outages").register();
}
});
loadRoutes(declarations("${header.selected}") + route());
context.start();
- SemanticQuestion java = SemanticQuestions.get(context).get("javaDepartment");
- SemanticQuestion yaml = SemanticQuestions.get(context).get("department");
+ SemanticEvaluation java = SemanticEvaluations.get(context).get("javaDepartment");
+ SemanticEvaluation yaml = SemanticEvaluations.get(context).get("department");
assertThat(java).usingRecursiveComparison().isEqualTo(yaml);
assertThat(calls).hasValue(0);
try (var template = context.createProducerTemplate()) {
@@ -239,14 +239,41 @@ class SemanticQuestionTest extends YamlTestSupport {
}
@Test
- void ordinaryResourceDoesNotCreateSemanticQuestionState() throws Exception {
+ void ordinaryResourceDoesNotCreateSemanticEvaluationState() throws Exception {
loadRoutes("""
- from:
uri: direct:ordinary
steps:
- to: mock:ordinary
""");
- assertThat(context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)).isNull();
+ assertThat(context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class)).isNull();
+ }
+
+ @Test
+ void preParseRuntimeFailuresRetainTheResourceLocation() throws Exception {
+ context.getRegistry().bind("failing", new FailingValidationExpert());
+ context.start();
+ String yaml = """
+ - semantic:
+ expert: failing
+ evaluation:
+ invalid:
+ operation: injection
+ """;
+ assertThatThrownBy(() -> PluginHelper.getRoutesLoader(context)
+ .loadRoutes(ResourceHelper.fromString("failing-expert.yaml", yaml)))
+ .hasStackTraceContaining("Error pre-parsing resource: failing-expert.yaml")
+ .hasStackTraceContaining("expert 'failing'")
+ .hasStackTraceContaining("Expert validation failed");
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).get("invalid"))
+ .hasMessageContaining("Unknown semantic evaluation");
+ }
+
+ public static class FailingValidationExpert extends SecurityExpert {
+ @Override
+ public void validate(SemanticEvaluation evaluation) {
+ throw new IllegalStateException("Expert validation failed");
+ }
}
private static String route() {
@@ -281,7 +308,7 @@ class SemanticQuestionTest extends YamlTestSupport {
}
@Test
- void batchExpressionReusesNamedQuestionsAndResults() throws Exception {
+ void batchExpressionReusesNamedEvaluationsAndResults() throws Exception {
loadRoutes(declarations("${body}") + declarations("${body}").replace("department:", "second:") + """
- route:
from:
@@ -335,7 +362,7 @@ class SemanticQuestionTest extends YamlTestSupport {
@Test
void declarationsInAnotherResourceResolveBeforeTraffic() throws Exception {
loadRoutes(ResourceHelper.fromString("routes.yaml", route()),
- ResourceHelper.fromString("questions.yaml", declarations("${body}")));
+ ResourceHelper.fromString("evaluations.yaml", declarations("${body}")));
context.start();
try (var template = context.createProducerTemplate()) {
template.sendBody("direct:tickets", "invoice");
@@ -344,31 +371,31 @@ class SemanticQuestionTest extends YamlTestSupport {
}
@Test
- void resourceReloadUpdatesStateAndRemovesObsoleteQuestions() throws Exception {
- loadRoutes(ResourceHelper.fromString("questions.yaml", declarations("${body}")),
+ void resourceReloadUpdatesStateAndRemovesObsoleteEvaluations() throws Exception {
+ loadRoutes(ResourceHelper.fromString("evaluations.yaml", declarations("${body}")),
ResourceHelper.fromString("routes.yaml", route()));
context.start();
PluginHelper.getRoutesLoader(context)
- .updateRoutes(ResourceHelper.fromString("questions.yaml", declarations("${header.updated}")));
+ .updateRoutes(ResourceHelper.fromString("evaluations.yaml", declarations("${header.updated}")));
try (var template = context.createProducerTemplate()) {
template.sendBodyAndHeader("direct:tickets", "original", "updated", "invoice");
}
assertThat(selected).isEqualTo("invoice");
- PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.yaml", "[]"));
- assertThatThrownBy(() -> SemanticQuestions.get(context).get("department")).hasMessageContaining("Unknown");
+ PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("evaluations.yaml", "[]"));
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).get("department")).hasMessageContaining("Unknown");
}
@ParameterizedTest
@MethodSource("malformedTopLevelResources")
- void malformedTopLevelEntriesUseNormalLoaderErrorsAndKeepQuestions(String yaml) throws Exception {
- loadRoutes(ResourceHelper.fromString("questions.yaml", declarations("${body}")));
+ void malformedTopLevelEntriesUseNormalLoaderErrorsAndKeepEvaluations(String yaml) throws Exception {
+ loadRoutes(ResourceHelper.fromString("evaluations.yaml", declarations("${body}")));
context.start();
- SemanticQuestion original = SemanticQuestions.get(context).get("department");
+ SemanticEvaluation original = SemanticEvaluations.get(context).get("department");
assertThatThrownBy(() -> PluginHelper.getRoutesLoader(context)
- .updateRoutes(ResourceHelper.fromString("questions.yaml", yaml)))
+ .updateRoutes(ResourceHelper.fromString("evaluations.yaml", yaml)))
.isInstanceOf(YamlDeserializationException.class)
.hasMessageContaining("Unable to find constructor for node");
- assertThat(SemanticQuestions.get(context).get("department")).isSameAs(original);
+ assertThat(SemanticEvaluations.get(context).get("department")).isSameAs(original);
}
static Stream<String> malformedTopLevelResources() {
@@ -377,8 +404,8 @@ class SemanticQuestionTest extends YamlTestSupport {
}
@Test
- void watcherDropsDeletedQuestionOnlyResourcesBeforeLoadingRenamedFiles() throws Exception {
- Path original = directory.resolve("questions.yaml");
+ void watcherDropsDeletedEvaluationOnlyResourcesBeforeLoadingRenamedFiles() throws Exception {
+ Path original = directory.resolve("evaluations.yaml");
Files.writeString(original, declarations("${body}"));
Resource source = ResourceHelper.resolveResource(context, original.toUri().toString());
loadRoutes(source);
@@ -387,15 +414,15 @@ class SemanticQuestionTest extends YamlTestSupport {
watcher.setCamelContext(context);
Path renamed = Files.move(original, directory.resolve("q.yaml"));
watcher.reload(source);
- assertThatThrownBy(() -> SemanticQuestions.get(context).get("department"))
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).get("department"))
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Unknown");
Resource replacement = ResourceHelper.resolveResource(context, renamed.toUri().toString());
watcher.reload(replacement);
assertThat(watcher.getLastError()).isNull();
- assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${body}");
+ assertThat(SemanticEvaluations.get(context).get("department").getState()).isEqualTo("${body}");
Files.delete(renamed);
watcher.reload(replacement);
- assertThatThrownBy(() -> SemanticQuestions.get(context).get("department"))
+ assertThatThrownBy(() -> SemanticEvaluations.get(context).get("department"))
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Unknown");
}
@@ -408,7 +435,7 @@ class SemanticQuestionTest extends YamlTestSupport {
@Test
void duplicatesAndInvalidDefinitionsAreRejected() {
assertThatThrownBy(() -> loadRoutesNoValidate(declarations("${body}") + declarations("${body}")))
- .hasStackTraceContaining("Duplicate semantic question");
+ .hasStackTraceContaining("Duplicate semantic evaluation");
assertThatThrownBy(() -> loadRoutesNoValidate(declarations("${body}").replace("instructions:", "typo:")))
.hasStackTraceContaining("Unknown property");
assertThatThrownBy(() -> loadRoutesNoValidate(declarations("${body}")
@@ -420,7 +447,7 @@ class SemanticQuestionTest extends YamlTestSupport {
void schemaAcceptsBooleanAndScoreDefinitionsAndRejectsWrongCriteria() throws Exception {
loadRoutes("""
- semantic:
- question:
+ evaluation:
actionable:
type: boolean
instructions: Is the request actionable?
@@ -432,7 +459,8 @@ class SemanticQuestionTest extends YamlTestSupport {
instructions: How urgent?
criteria: [Routine, Urgent]
""");
- assertThat(SemanticQuestions.get(context).get("urgency").getLevels()).containsExactly("Routine", "Urgent");
+ assertThat(SemanticEvaluations.get(context).get("urgency").getParameters().get("criteria"))
+ .isEqualTo(List.of("Routine", "Urgent"));
context.start();
assertThatThrownBy(() -> loadRoutesNoValidate(declarations("${body}").replace("type: choice", "type: score")))
.hasStackTraceContaining("criteria").hasStackTraceContaining("List");
@@ -442,23 +470,23 @@ class SemanticQuestionTest extends YamlTestSupport {
void numericPlaceholdersResolveBeforeValidation() throws Exception {
loadRoutesNoValidate("""
- semantic:
- question:
+ evaluation:
urgent:
type: boolean
instructions: Urgent?
threshold: "{{threshold:0.8}}"
uncertainty: "{{uncertainty:0.1}}"
""");
- assertThat(SemanticQuestions.get(context).get("urgent").getThreshold()).isEqualTo(0.8);
- assertThat(SemanticQuestions.get(context).get("urgent").getUncertainty()).isEqualTo(0.1);
+ assertThat(SemanticEvaluations.get(context).get("urgent").getParameters().get("threshold")).isEqualTo(0.8);
+ assertThat(SemanticEvaluations.get(context).get("urgent").getParameters().get("uncertainty")).isEqualTo(0.1);
}
@ParameterizedTest
@ValueSource(strings = { "threshold", "uncertainty" })
- void invalidNumericValuesIdentifyQuestionFieldAndLocation(String field) {
+ void invalidNumericValuesIdentifyEvaluationFieldAndLocation(String field) {
String yaml = """
- semantic:
- question:
+ evaluation:
spam:
type: boolean
instructions: Is this spam?
@@ -468,7 +496,7 @@ class SemanticQuestionTest extends YamlTestSupport {
.hasMessageContaining("route-0.yaml")
.isInstanceOfSatisfying(YamlDeserializationException.class, error -> {
assertThat(error).hasMessageContaining(
- "Invalid numeric value for '" + field + "' in semantic question 'spam'");
+ "Invalid numeric value for '" + field + "' in semantic evaluation 'spam'");
assertThat(error.getProblemMark()).hasValueSatisfying(mark -> {
assertThat(mark.getLine()).isEqualTo(5);
assertThat(mark.getColumn()).isEqualTo(8 + field.length() + 2);
@@ -481,7 +509,7 @@ class SemanticQuestionTest extends YamlTestSupport {
void invalidContractValuesFailAtStartupWithoutInference(String field) throws Exception {
String yaml = """
- semantic:
- question:
+ evaluation:
spam:
type: boolean
instructions: Is this spam?
@@ -501,15 +529,15 @@ class SemanticQuestionTest extends YamlTestSupport {
void enumParsingFollowsYamlDslConventions(String policy) throws Exception {
loadRoutesNoValidate("""
- semantic:
- question:
+ evaluation:
spam:
type: BoOlEaN
instructions: Is this spam?
uncertaintyPolicy: %s
""".formatted(policy));
- SemanticQuestion question = SemanticQuestions.get(context).get("spam");
- assertThat(question.getType()).isEqualTo(SemanticQuestion.Type.BOOLEAN);
- assertThat(question.getUncertaintyPolicy()).isEqualTo(SemanticQuestion.UncertaintyPolicy.NON_MATCH);
+ SemanticEvaluation evaluation = SemanticEvaluations.get(context).get("spam");
+ assertThat(evaluation.getOperation()).isEqualTo("boolean");
+ assertThat(evaluation.getParameters().get("uncertaintyPolicy")).isEqualTo("non-match");
}
@ParameterizedTest
@@ -528,21 +556,21 @@ class SemanticQuestionTest extends YamlTestSupport {
static Stream<Arguments> invalidStructures() {
String declaration = declarations("${body}");
- String question = """
+ String evaluation = """
- semantic:
- question:
+ evaluation:
q: {type: boolean, instructions: Is it valid?}
""";
return Stream.of(
Arguments.of(declaration.replace("instructions:", "typo:"),
- "Unknown property 'typo' in semantic question 'department'", 5, 14),
+ "Unknown property 'typo' in semantic evaluation 'department'", 5, 14),
Arguments.of(declaration.replace(" type: choice\n", ""),
"Specify exactly one operation or type: department", 3, 8),
Arguments.of(declaration.replace("type: choice", "type: choice\n type: choice"),
- "Duplicate key 'type' in semantic question 'department'", 4, 8),
+ "Duplicate key 'type' in semantic evaluation 'department'", 4, 8),
Arguments.of("- semantic: {other: {}}", "Unknown property 'other' in semantic declaration", 0, 20),
- Arguments.of(question + question, "Duplicate semantic question: q", 4, 4),
- Arguments.of(question + " q: {type: boolean, instructions: Is it valid?}\n",
+ Arguments.of(evaluation + evaluation, "Duplicate semantic evaluation: q", 4, 4),
+ Arguments.of(evaluation + " q: {type: boolean, instructions: Is it valid?}\n",
"Duplicate key 'q' in semantic evaluations", 3, 6));
}
(camel) 07/08: CAMEL-25382: Isolate semantic replacement validation
Posted by ld...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 921bea1b1420dfb5dac01cca87d39400445db899
Author: Luigi De Masi <ld...@redhat.com>
AuthorDate: Thu Oct 8 08:43:21 2026 +0200
CAMEL-25382: Isolate semantic replacement validation
Keep public evaluation lookups on the published snapshot. Pass candidate
snapshots into validation explicitly and consolidate the two ThreadLocals
into one private language compilation scope, suspended across expert
callbacks. Preserve existing Simple APIs and lazy compilation behavior.
Cover callback reads, reentrant validation, rollback and nested selectors.
Add HTTP-level tests proving that missing TypeSafe response fields are
rejected before adapter mapping, and address the review documentation nits.
Co-authored-by: Codex <no...@openai.com>
Signed-off-by: Luigi De Masi <ld...@redhat.com>
---
.../camel/catalog/docs/semantic-language.adoc | 3 +
.../src/main/docs/semantic-language.adoc | 3 +
.../camel/language/semantic/SemanticLanguage.java | 70 +++++++++++---
.../apache/camel/semantic/SemanticEvaluations.java | 64 ++++++------
.../camel/semantic/SemanticContractTest.java | 107 +++++++++++++++++++++
.../camel/semantic/SemanticInitializationTest.java | 3 +-
.../typesafeai/TypeSafeAiSemanticAdapterTest.java | 26 +++++
.../language/simple/SimplePredicateExpression.java | 2 +
8 files changed, 225 insertions(+), 53 deletions(-)
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
index 699f055934d8..be66d12c1a70 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
@@ -374,6 +374,9 @@ Use `.type("boolean")` with optional `.threshold(0.8)`, `.uncertainty(0.1)` and
`.uncertaintyPolicy("non-match")` for boolean evaluations. Use `.type("score")` and successive
`.level("description")` calls for ordered score levels. Applications can also register immutable
`SemanticEvaluation` definitions using `SemanticEvaluations.get(context).replace(source, evaluations)`.
+On a running context, replacement definitions are validated before publication. If validation fails,
+the previous definitions remain available. The public `get(name)` and `get(names)` methods always
+read published definitions, including calls made from an expert's validation callback.
Each Java resource owns one group. Register all of its evaluations together; registering again
replaces that resource's previous group. Reloading a Java resource without the helper removes
diff --git a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
index 699f055934d8..be66d12c1a70 100644
--- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
+++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
@@ -374,6 +374,9 @@ Use `.type("boolean")` with optional `.threshold(0.8)`, `.uncertainty(0.1)` and
`.uncertaintyPolicy("non-match")` for boolean evaluations. Use `.type("score")` and successive
`.level("description")` calls for ordered score levels. Applications can also register immutable
`SemanticEvaluation` definitions using `SemanticEvaluations.get(context).replace(source, evaluations)`.
+On a running context, replacement definitions are validated before publication. If validation fails,
+the previous definitions remain available. The public `get(name)` and `get(names)` methods always
+read published definitions, including calls made from an expert's validation callback.
Each Java resource owns one group. Register all of its evaluations together; registering again
replaces that resource's previous group. Reloading a Java resource without the helper removes
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
index f2eebc7c2c88..3ddc4d7620c4 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
@@ -68,7 +68,9 @@ public class SemanticLanguage extends LanguageSupport {
public static final String ADAPTER_FACTORY = "semantic-adapter";
public static final String ADAPTER_RESOURCE = FactoryFinder.DEFAULT_PATH + ADAPTER_FACTORY;
- private final ThreadLocal<Boolean> validating = new ThreadLocal<>();
+ // Nested Simple functions re-enter the language while compiling state selectors. Keep that scope private to
+ // compilation; registry reads and expert callbacks must always observe published declarations.
+ private final ThreadLocal<Validation> validation = new ThreadLocal<>();
private String adapter;
private String defaultExpert;
private String defaultState = "${body}";
@@ -225,9 +227,6 @@ public class SemanticLanguage extends LanguageSupport {
}
private synchronized void startAdapter(List<Group> groups) {
- if (Boolean.TRUE.equals(validating.get())) {
- return;
- }
ManagedAdapter owned = managedAdapter;
if (owned == null || groups.stream().noneMatch(group -> group.expert.provider == owned.instance)) {
return;
@@ -299,7 +298,13 @@ public class SemanticLanguage extends LanguageSupport {
/** Validate declarations, including unused evaluations, without starting managed expert resources. */
public void validateDeclarations(Map<String, SemanticEvaluation> declarations) {
- validationOnly(() -> {
+ validateDeclarations(declarations, null);
+ }
+
+ /** Validate changed declarations against the complete candidate snapshot without publishing it. */
+ public void validateDeclarations(
+ Map<String, SemanticEvaluation> declarations, Map<String, SemanticEvaluation> snapshot) {
+ validationOnly(snapshot, () -> {
declarations.forEach((name, evaluation) -> {
expert(name, evaluation);
String selector = evaluation.getState() != null ? evaluation.getState() : defaultState;
@@ -314,21 +319,33 @@ public class SemanticLanguage extends LanguageSupport {
});
}
- private <T> T validationOnly(Supplier<T> action) {
- Boolean previous = validating.get();
- validating.set(true);
+ private <T> T validationOnly(Map<String, SemanticEvaluation> snapshot, Supplier<T> action) {
+ Validation previous = validation.get();
+ validation.set(snapshot != null || previous == null ? new Validation(snapshot) : previous);
try {
return action.get();
} finally {
if (previous == null) {
- validating.remove();
+ validation.remove();
} else {
- validating.set(previous);
+ validation.set(previous);
}
}
}
private ResolvedExpert expert(String name, SemanticEvaluation evaluation) {
+ Validation previous = validation.get();
+ validation.remove();
+ try {
+ return resolveExpert(name, evaluation);
+ } finally {
+ if (previous != null) {
+ validation.set(previous);
+ }
+ }
+ }
+
+ private ResolvedExpert resolveExpert(String name, SemanticEvaluation evaluation) {
String reference = evaluation.getExpert() != null ? evaluation.getExpert() : defaultExpert;
String label = reference != null ? reference : adapter != null ? adapter : "automatic";
try {
@@ -431,7 +448,7 @@ public class SemanticLanguage extends LanguageSupport {
private final List<String> names;
private final boolean batch;
private final boolean predicate;
- private final Consumer<Map<String, SemanticEvaluation>> validator = this::prepare;
+ private final Consumer<Map<String, SemanticEvaluation>> validator = this::validateReplacement;
private volatile Compiled compiled;
private volatile SemanticEvaluations evaluations;
@@ -445,9 +462,11 @@ public class SemanticLanguage extends LanguageSupport {
public void init(CamelContext context) {
super.init(context);
evaluations = SemanticEvaluations.get(context);
- Map<String, SemanticEvaluation> selected = evaluations.get(names);
+ Validation scope = validation.get();
+ Map<String, SemanticEvaluation> selected = scope != null && scope.snapshot != null
+ ? select(scope.snapshot, true) : evaluations.get(names);
compile(selected);
- if (!Boolean.TRUE.equals(validating.get())) {
+ if (scope == null) {
evaluations.setValidator(names, validator);
}
}
@@ -459,7 +478,7 @@ public class SemanticLanguage extends LanguageSupport {
}
private synchronized Compiled compile(Map<String, SemanticEvaluation> selected) {
- if (Boolean.TRUE.equals(validating.get())) {
+ if (validation.get() != null) {
// Revisit cached nested selectors against the candidate snapshot without publishing compilation state.
return prepare(selected);
}
@@ -472,7 +491,25 @@ public class SemanticLanguage extends LanguageSupport {
}
private Compiled prepare(Map<String, SemanticEvaluation> selected) {
- return validationOnly(() -> prepareDeclarations(selected));
+ return validationOnly(null, () -> prepareDeclarations(selected));
+ }
+
+ private void validateReplacement(Map<String, SemanticEvaluation> snapshot) {
+ validationOnly(snapshot, () -> prepareDeclarations(select(snapshot, false)));
+ }
+
+ private Map<String, SemanticEvaluation> select(Map<String, SemanticEvaluation> snapshot, boolean required) {
+ Map<String, SemanticEvaluation> selected = new LinkedHashMap<>();
+ for (String name : names) {
+ SemanticEvaluation evaluation = snapshot.get(name);
+ // Removed declarations remain removable; their existing expressions fail if evaluated again.
+ if (evaluation != null) {
+ selected.put(name, evaluation);
+ } else if (required) {
+ throw new IllegalArgumentException("Unknown semantic evaluation: " + name);
+ }
+ }
+ return Collections.unmodifiableMap(selected);
}
private Compiled prepareDeclarations(Map<String, SemanticEvaluation> selected) {
@@ -643,6 +680,9 @@ public class SemanticLanguage extends LanguageSupport {
}
}
+ private record Validation(Map<String, SemanticEvaluation> snapshot) {
+ }
+
private record ResolvedExpert(SemanticAdapter provider, SemanticCapabilities capabilities, String reference) {
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
index 80816e246840..30f65e8a1730 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
@@ -35,7 +35,6 @@ public final class SemanticEvaluations {
private final Map<String, Map<String, SemanticEvaluation>> sources = new HashMap<>();
private final Map<String, Resource> resources = new HashMap<>();
private volatile Map<String, SemanticEvaluation> evaluations = Map.of();
- private final ThreadLocal<Map<String, SemanticEvaluation>> candidate = new ThreadLocal<>();
private final Map<Consumer<Map<String, SemanticEvaluation>>, List<String>> validators = new WeakHashMap<>();
private SemanticEvaluations(CamelContext context) {
@@ -49,10 +48,14 @@ public final class SemanticEvaluations {
}
}
- /** Validate current declarations and weakly track a callback owned by its initialized expression. */
+ /**
+ * Validate current declarations and weakly track a callback owned by its initialized expression. The callback
+ * receives the complete immutable snapshot so nested references can be validated against the same replacement.
+ */
public synchronized void setValidator(List<String> names, Consumer<Map<String, SemanticEvaluation>> validator) {
// A replacement may have occurred between the expression's initial compilation and registration.
- validator.accept(get(names));
+ get(names);
+ validator.accept(evaluations);
validators.put(validator, List.copyOf(names));
}
@@ -93,39 +96,23 @@ public final class SemanticEvaluations {
throw new IllegalArgumentException("Duplicate semantic evaluation: " + name);
}
});
- Map<String, SemanticEvaluation> previous = candidate.get();
- candidate.set(Map.copyOf(replacement));
- try {
- if (context.isStarted() && !definitions.isEmpty()) {
- ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(definitions);
- }
- validators.forEach((validator, names) -> {
- if (!Collections.disjoint(names, definitions.keySet())) {
- Map<String, SemanticEvaluation> selected = new LinkedHashMap<>();
- names.forEach(name -> {
- SemanticEvaluation evaluation = replacement.get(name);
- // Removed declarations remain removable; their existing expressions fail if evaluated again.
- if (evaluation != null) {
- selected.put(name, evaluation);
- }
- });
- validator.accept(Collections.unmodifiableMap(selected));
- }
- });
- } finally {
- if (previous == null) {
- candidate.remove();
- } else {
- candidate.set(previous);
- }
+ Map<String, SemanticEvaluation> snapshot = Map.copyOf(replacement);
+ if (context.isStarted() && !definitions.isEmpty()) {
+ ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(definitions, snapshot);
}
+ // Expert callbacks may initialize expressions and register additional validators reentrantly.
+ new LinkedHashMap<>(validators).forEach((validator, names) -> {
+ if (!Collections.disjoint(names, definitions.keySet())) {
+ validator.accept(snapshot);
+ }
+ });
if (definitions.isEmpty()) {
sources.remove(source);
} else {
sources.put(source, Map.copyOf(definitions));
}
resources.remove(source);
- evaluations = Map.copyOf(replacement);
+ evaluations = snapshot;
}
/** Track a route resource so deleted files can be discarded before development-mode reload. */
@@ -157,17 +144,25 @@ public final class SemanticEvaluations {
return evaluations.isEmpty();
}
+ /**
+ * Resolve a named evaluation from the published snapshot, including when called during replacement validation.
+ * Candidate definitions become visible only after validation succeeds.
+ */
public SemanticEvaluation get(String name) {
- SemanticEvaluation evaluation = snapshot().get(name);
+ SemanticEvaluation evaluation = evaluations.get(name);
if (evaluation == null) {
throw new IllegalArgumentException("Unknown semantic evaluation: " + name);
}
return evaluation;
}
- /** Resolve all requested names from one immutable snapshot, preserving reference order. */
+ /**
+ * Resolve all requested names from one immutable published snapshot, preserving reference order. This also reads
+ * the published snapshot during replacement validation; candidate definitions become visible only after validation
+ * succeeds.
+ */
public Map<String, SemanticEvaluation> get(List<String> names) {
- Map<String, SemanticEvaluation> snapshot = snapshot();
+ Map<String, SemanticEvaluation> snapshot = evaluations;
Map<String, SemanticEvaluation> selected = new LinkedHashMap<>();
for (String name : names) {
SemanticEvaluation evaluation = snapshot.get(name);
@@ -179,9 +174,4 @@ public final class SemanticEvaluations {
return Collections.unmodifiableMap(selected);
}
- private Map<String, SemanticEvaluation> snapshot() {
- Map<String, SemanticEvaluation> validation = candidate.get();
- return validation != null ? validation : evaluations;
- }
-
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
index 868bc6188692..7825fc0cffce 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
@@ -36,6 +36,7 @@ import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.ResourceHelper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.snakeyaml.engine.v2.api.LoadSettings;
import org.snakeyaml.engine.v2.api.lowlevel.Compose;
@@ -158,6 +159,8 @@ class SemanticContractTest {
@Override
public void validate(SemanticEvaluation evaluation) {
if (evaluation == candidate) {
+ assertThat(registry.get("q")).as("callback reads published evaluation").isSameAs(original);
+ assertThat(registry.get(List.of("q"))).containsEntry("q", original);
entered.countDown();
try {
assertThat(release.await(10, TimeUnit.SECONDS)).isTrue();
@@ -185,6 +188,110 @@ class SemanticContractTest {
}
}
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void expertCallbacksDoNotInheritValidationScope(boolean fail) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var registry = SemanticEvaluations.get(context);
+ var original = new SemanticEvaluation("detect", "content", null, Map.of());
+ var candidate = new SemanticEvaluation("detect", "content", "${semantic('added')} ready", Map.of());
+ var nested = new SemanticEvaluation("detect", "content", null, Map.of("threshold", 0.9));
+ var expert = new ContentExpert() {
+ boolean checked;
+
+ @Override
+ public void validate(SemanticEvaluation evaluation) {
+ if (evaluation == candidate) {
+ checked = true;
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("published state");
+ var language = context.resolveLanguage("semantic");
+ assertThat(language.createExpression("ref:nested").evaluate(exchange, Boolean.class)).isTrue();
+ // Reentrant validation must restore the outer replacement's candidate snapshot.
+ registry.validate();
+ if (fail) {
+ throw new IllegalArgumentException("Reject replacement");
+ }
+ }
+ }
+ };
+ context.getRegistry().bind("content", expert);
+ registry.replace("test", Map.of("q", original, "nested", original));
+ context.start();
+ var current = context.resolveLanguage("semantic").createExpression("ref:q");
+ Map<String, SemanticEvaluation> replacement = Map.of("q", candidate, "nested", nested, "added", original);
+ if (fail) {
+ assertThatThrownBy(() -> registry.replace("test", replacement)).hasMessageContaining("Reject replacement");
+ assertThat(registry.get("q")).isSameAs(original);
+ assertThatThrownBy(() -> registry.get("added")).hasMessageContaining("Unknown semantic evaluation");
+ } else {
+ registry.replace("test", replacement);
+ assertThat(registry.get("q")).isSameAs(candidate);
+ }
+ assertThat(expert.checked).isTrue();
+ // A later successful replacement and runtime compilation must not retain either validation scope.
+ registry.replace("test", Map.of("q", original, "nested", nested));
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("published state");
+ assertThat(context.resolveLanguage("semantic").createExpression("ref:nested")
+ .evaluate(exchange, Boolean.class)).isFalse();
+ assertThat(current.evaluate(exchange, Boolean.class)).isTrue();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "${semantic('inner')} ready",
+ "${semantic('inner')} ? 'yes' : 'no'" })
+ void nestedSimpleFunctionsValidateCandidateReferences(String selector) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("content", new ContentExpert());
+ var registry = SemanticEvaluations.get(context);
+ var inner = new SemanticEvaluation("detect", "content", null, Map.of());
+ registry.replace("test", Map.of("inner", inner));
+ context.start();
+ var outer = new SemanticEvaluation("detect", "content", selector, Map.of());
+ registry.replace("test", Map.of("inner", inner, "outer", outer));
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("content");
+ var expression = context.resolveLanguage("semantic").createExpression("ref:outer");
+ assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
+ assertThatThrownBy(() -> registry.replace("test", Map.of("outer", outer)))
+ .hasMessageContaining("Unknown semantic evaluation: inner");
+ assertThat(registry.get("inner")).isSameAs(inner);
+ assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource(delimiter = '|', value = {
+ "${not(${semantic('inner')})} ready | false ready | true ready",
+ "${iif(${semantic('inner')},yes,no)} | yes | no" })
+ void lazyNestedSimpleFunctionsReadPublishedReplacements(String selector, String before, String after) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var expert = new ContentExpert();
+ context.getRegistry().bind("content", expert);
+ var registry = SemanticEvaluations.get(context);
+ var inner = new SemanticEvaluation("detect", "content", null, Map.of());
+ var outer = new SemanticEvaluation("detect", "content", selector, Map.of());
+ registry.replace("test", Map.of("inner", inner, "outer", outer));
+ context.start();
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("content");
+ var expression = context.resolveLanguage("semantic").createExpression("ref:outer");
+ assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
+ assertThat(expert.state).isEqualTo(before);
+ registry.replace("test", Map.of("inner",
+ new SemanticEvaluation("detect", "content", null, Map.of("threshold", 0.9)), "outer", outer));
+ assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
+ assertThat(expert.state).isEqualTo(after);
+ // Simple compiles these composite functions lazily, when they are evaluated.
+ registry.replace("test", Map.of("outer", outer));
+ assertThatThrownBy(() -> expression.evaluate(exchange, Boolean.class))
+ .hasMessageContaining("Unknown semantic evaluation: inner");
+ }
+ }
+
@Test
void invalidProgrammaticReplacementRetainsUnusedDeclarations() throws Exception {
try (var context = new DefaultCamelContext()) {
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
index ef47233e353f..c5e55284da86 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
@@ -101,7 +101,8 @@ class SemanticInitializationTest {
assertThat(lookup.get(10, TimeUnit.SECONDS)).isSameAs(expected);
} finally {
release.countDown();
- assertThat(creation.get(10, TimeUnit.SECONDS)).isNotNull();
+ assertThat(creation.get(10, TimeUnit.SECONDS))
+ .as("registry created after the lazy lookup returns no plugin").isNotNull();
}
} finally {
release.countDown();
diff --git a/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java b/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
index c637ba51db2e..b77fa9f4b8cb 100644
--- a/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
+++ b/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.typesafeai;
+import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -85,6 +86,31 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
assertThat(requests).isEmpty();
}
+ @ParameterizedTest
+ @CsvSource({
+ "probability, Expected a number",
+ "answer, Answer names must match question names",
+ "answers, Expected an object"
+ })
+ void missingBooleanResponseFieldsFailBeforeAdapterMapping(String missing, String message) throws Exception {
+ JsonObject response = TypeSafeAiJson.parse(result(Map.of("question", Map.of("type", "noul", "noul", 0.9))));
+ JsonObject answers = response.getJsonObject("answers");
+ switch (missing) {
+ case "probability" -> answers.getJsonObject("question").remove("noul");
+ case "answer" -> answers.remove("question");
+ case "answers" -> response.remove("answers");
+ default -> throw new IllegalArgumentException(missing);
+ }
+ respond = request -> response.toJson();
+ var adapter = new TypeSafeAiSemanticAdapter();
+ adapter.setCamelContext(context);
+ var evaluation = new SemanticEvaluation("boolean", null, null, Map.of("instructions", "Classify"));
+ assertThatThrownBy(() -> adapter.evaluate(evaluation, "private-input"))
+ .isExactlyInstanceOf(IOException.class)
+ .hasMessage("Invalid TypeSafe AI response: " + message);
+ assertThat(requests).hasSize(1);
+ }
+
@Test
void omittedPolicyUsesAdapterDefaults() throws Exception {
respond = request -> result(Map.of("question", Map.of("type", "noul", "noul", 0.5)));
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateExpression.java
index 6b351d6e1504..080018a34acf 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateExpression.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateExpression.java
@@ -24,6 +24,8 @@ import org.apache.camel.Predicate;
* Opts a Simple function into using its predicate contract for negation, including any validation in
* {@link #matches(org.apache.camel.Exchange)}. Existing expressions retain value-based truthiness, even when they also
* implement {@link Predicate}.
+ *
+ * @since 4.23
*/
public interface SimplePredicateExpression extends Expression, Predicate {
@Override
(camel) 05/08: CAMEL-25382: Fix JBang validation of semantic documentation
Posted by ld...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 6fd800813c384f819cf829367db29a10d4d8b439
Author: Luigi De Masi <ld...@redhat.com>
AuthorDate: Wed Oct 7 19:05:49 2026 +0200
CAMEL-25382: Fix JBang validation of semantic documentation
Treat the semantic function's missing runtime dependency as an unavailable
language in JBang's static Simple checks, matching the existing policy for
other optional languages. Keep reporting malformed semantic arguments.
Update the catalog excerpt regression to assert expert-owned evaluation
concepts instead of the removed question and TypeSafe policy model.
Co-authored-by: Codex <no...@openai.com>
Signed-off-by: Luigi De Masi <ld...@redhat.com>
---
.../dsl/jbang/core/commands/ai/SimpleChecks.java | 1 +
.../core/commands/ai/CatalogDocExcerptTest.java | 4 +--
.../commands/ai/SourceValidatorSimpleTest.java | 36 ++++++++++++++++++++++
3 files changed, 39 insertions(+), 2 deletions(-)
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
index 4cff024faf1f..5d71ae876aab 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
@@ -223,6 +223,7 @@ final class SimpleChecks {
static boolean isMissingDependency(String error) {
return error.startsWith("No language could be found for:")
+ || error.equals("The semantic function requires camel-semantic on the classpath")
|| error.startsWith("No " + SimpleLanguageFunctionFactory.FACTORY + "/")
&& error.contains("service could be found in the classpath");
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExcerptTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExcerptTest.java
index 8da10bbe9563..96bad2e799ea 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExcerptTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExcerptTest.java
@@ -87,12 +87,12 @@ class CatalogDocExcerptTest {
@Test
public void testALanguageCarriesWhatItsOptionsCannotSay() throws Exception {
- // CAMEL-25235: the semantic language is configured by named questions, which are not options at all, so its
+ // CAMEL-25235: the semantic language is configured by named evaluations, which are not options at all, so its
// option list (id, language, expression) says nothing about how to use it
JsonObject answer = catalogDoc(Map.of("name", "semantic", "kind", "language"));
String documentation = answer.getString("documentation");
assertNotNull(documentation, "no documentation in: " + answer.toJson());
- for (String needed : new String[] { "question", "instructions", "criteria", "threshold" }) {
+ for (String needed : new String[] { "evaluation", "expert", "operation", "parameters", "state" }) {
assertTrue(documentation.contains(needed), needed + " is not in the excerpt:\n" + documentation);
}
assertNotNull(answer.getString("documentationHint"));
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
index 3842cdb4046b..13a0917d8020 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
@@ -255,6 +255,42 @@ class SourceValidatorSimpleTest {
assertThat(msgs).isEmpty();
}
+ @Test
+ void semanticExpressionsAndPredicatesDoNotRequireTheRuntimeDependency() {
+ assertThat(catalog.validateLanguageExpression(null, "simple", "${semantic('department')}").getShortError())
+ .isEqualTo("The semantic function requires camel-semantic on the classpath");
+ List<String> msgs = SourceValidator.validateYamlSimple("""
+ - from:
+ uri: direct:start
+ steps:
+ - setBody:
+ simple: "${semantic('department')}"
+ - filter:
+ simple: "${semantic('allowed')}"
+ steps:
+ - log: accepted
+ """, catalog);
+ assertThat(msgs).isEmpty();
+ }
+
+ @Test
+ void invalidSemanticArgumentsAreStillReportedWithoutTheRuntimeDependency() {
+ List<String> msgs = SourceValidator.validateYamlSimple("""
+ - from:
+ uri: direct:start
+ steps:
+ - setBody:
+ simple: "${semantic(department)}"
+ - filter:
+ simple: "${semantic('allowed', 'other')}"
+ steps:
+ - log: accepted
+ """, catalog);
+ assertThat(msgs).hasSize(2);
+ assertThat(msgs).allSatisfy(msg -> assertThat(msg)
+ .contains("Simple syntax error", "Semantic requires one quoted evaluation name"));
+ }
+
@Test
void thePredicateEipsComeFromTheCatalog() {
// the same text is a predicate (error: no ${}) where the catalog marks the option asPredicate, and a
(camel) 01/08: CAMEL-25382: Introduce expert-owned semantic evaluation contracts
Posted by ld...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit d3cf0051628a99ffacbfc2f637d93574047dcfe9
Author: Luigi De Masi <ld...@redhat.com>
AuthorDate: Wed Oct 7 10:37:25 2026 +0200
CAMEL-25382: Introduce expert-owned semantic evaluation contracts
Declare static operation, input, parameter and result contracts on semantic
experts. Validate declarations and typed results across Java, YAML and XML,
and expose named evaluations through the Simple semantic function.
Keep decision policy in the expert, migrate TypeSafe AI, and cover structured
input, classification, numeric precision and failure cleanup. Regenerate
schemas and catalog metadata and document the preview API changes.
Co-authored-by: Codex <no...@openai.com>
Signed-off-by: Luigi De Masi <ld...@redhat.com>
---
.../camel/catalog/docs/semantic-language.adoc | 316 ++++++++----
.../camel/catalog/docs/simple-functions.adoc | 8 +
.../camel/catalog/docs/typesafe-ai-component.adoc | 6 +
.../apache/camel/catalog/languages/semantic.json | 2 +-
.../org/apache/camel/catalog/languages/simple.json | 141 +++---
.../camel/catalog/schemas/camelYamlDsl-model.json | 110 +++--
.../camel/catalog/simple/camel-simple-validator.js | 1 +
components/camel-ai/camel-semantic/pom.xml | 2 +-
.../apache/camel/language/semantic/semantic.json | 2 +-
.../services/org/apache/camel/language.properties | 2 +-
.../src/main/docs/semantic-language.adoc | 316 ++++++++----
.../camel/language/semantic/SemanticLanguage.java | 128 ++++-
.../org/apache/camel/semantic/SemanticAdapter.java | 15 +-
.../camel/semantic/SemanticCapabilities.java | 508 ++++++++++---------
.../org/apache/camel/semantic/SemanticExpert.java | 38 +-
.../apache/camel/semantic/SemanticOperation.java | 74 +++
.../apache/camel/semantic/SemanticParameter.java | 58 +++
.../apache/camel/semantic/SemanticQuestion.java | 176 ++++---
.../camel/semantic/SemanticQuestionBuilder.java | 143 +++---
.../apache/camel/semantic/SemanticQuestions.java | 67 ++-
.../camel/semantic/SemanticQuestionsBuilder.java | 24 +
.../camel/semantic/SemanticReloadPlugin.java | 8 +
.../org/apache/camel/semantic/SemanticResult.java | 51 +-
.../semantic/SemanticXmlRoutesBuilderLoader.java | 87 +++-
.../yaml/SemanticDefinitionDeserializer.java | 212 ++++----
.../apache/camel/semantic/FixedSemanticExpert.java | 84 ++++
.../apache/camel/semantic/SemanticBatchTest.java | 5 +-
.../camel/semantic/SemanticCapabilitiesTest.java | 190 +++++---
.../camel/semantic/SemanticContractTest.java | 536 +++++++++++++++++++++
.../camel/semantic/SemanticDeclarationDslTest.java | 33 +-
.../org/apache/camel/semantic/SemanticEipTest.java | 2 +-
.../apache/camel/semantic/SemanticExpertTest.java | 126 ++---
.../camel/semantic/SemanticExpertYamlTest.java | 47 +-
.../camel/semantic/SemanticInitializationTest.java | 11 +-
.../camel/semantic/SemanticLanguageTest.java | 12 +-
.../semantic/SemanticQuestionBuilderTest.java | 120 +++++
.../apache/camel/semantic/SemanticResultTest.java | 49 +-
.../semantic/SemanticRetryAndValidationTest.java | 6 +-
.../semantic/SemanticXmlAutoDiscoveryTest.java | 3 +
.../camel/semantic/SemanticXmlLoaderTest.java | 3 +
.../apache/camel/semantic/TestSemanticAdapter.java | 110 +++++
.../src/main/docs/typesafe-ai-component.adoc | 6 +
.../typesafeai/TypeSafeAiSemanticAdapter.java | 134 +++++-
.../typesafeai/TypeSafeAiSemanticAdapterTest.java | 86 +++-
.../org/apache/camel/language/simple/simple.json | 141 +++---
.../modules/languages/pages/simple-functions.adoc | 8 +
.../camel/language/simple/SimpleConstants.java | 6 +
.../camel/language/simple/SimpleSyntaxHints.java | 9 +-
.../simple/ast/SimpleFunctionExpression.java | 6 +-
.../camel/language/simple/ast/UnaryExpression.java | 6 +-
.../functions/QueryLanguageFunctionFactory.java | 27 ++
.../language/simple/SimpleSyntaxHintsTest.java | 8 +
.../java/org/apache/camel/util/SimpleUtils.java | 1 +
.../dsl/yaml/validator/SemanticSchemaTest.java | 59 ++-
.../resources/schema/camelYamlDsl-canonical.json | 120 +++--
.../resources/schema/camelYamlDsl-model.json | 110 +++--
.../generated/resources/schema/camelYamlDsl.json | 120 +++--
.../camel/dsl/yaml/YamlRoutesBuilderLoader.java | 4 +-
.../camel/dsl/yaml/SemanticQuestionTest.java | 202 ++++++--
59 files changed, 3517 insertions(+), 1368 deletions(-)
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
index 2dcf6a92fd57..0598a8714749 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
@@ -2,16 +2,17 @@
:doctitle: Semantic Evaluation
:shortname: semantic
:artifactid: camel-semantic
-:description: Evaluate named questions about message content to produce boolean decisions, categories and scores through provider adapters
+:description: Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters
:since: 4.23
:supportlevel: Preview
:tabs-sync-option:
*Since Camel {since}*
-The Semantic language evaluates named questions through a provider-independent adapter.
-Boolean questions produce decisions, choice questions produce category strings, and score
-questions produce numbers on an ordered rubric. Calls are synchronous and may block while
+The Semantic language invokes named evaluations through provider-independent experts.
+Each expert owns its operations, accepted inputs, evaluation parameters and result meaning.
+Boolean evaluations produce decisions, choice evaluations produce category strings, score
+evaluations produce numbers, and classification evaluations produce sets of labels. Calls are synchronous and may block while
inference runs. Provider errors propagate through normal Camel error handling.
== Dependencies and providers
@@ -48,50 +49,185 @@ The catalog describes the generic `language` expression model; it does not provi
semantic Spring Boot configuration class. Configure the language bean explicitly in Spring Boot
until generic-language starter configuration is available.
-== Fixed and instruction-driven experts
+== Expert-owned evaluations
-Instructions are optional in the common declaration model. The selected expert decides whether
-they are required, optional or unsupported. TypeSafe AI still requires nonblank instructions.
-A fixed detector can reject instructions and caller-defined criteria because its model already
-defines the question it answers. Unsupported result types, instructions, criteria and decision
-policies fail during initialization, before inference. Replacements for declarations already referenced
-by initialized expressions are validated before publication; failed replacements retain the previous definitions.
+An expert is a registry bean implementing `SemanticAdapter`, annotated with `@SemanticExpert`.
+The class annotation defines its operations. Each operation declares its accepted input shapes,
+parameters, result type and meaning, and any optional probabilities or confidence. A configured
+instance cannot redefine this contract. Endpoint addresses, credentials and model deployment
+settings remain bean configuration, separate from evaluation parameters.
-For example, given registered instances `security` (a fixed injection detector) and `general`
-(an instruction-driven expert):
+Register the beans with Camel Main or Camel JBang, for example:
+
+[source,properties]
+----
+camel.beans.security=#class:com.example.SecurityExpert
+camel.beans.decisions=#class:org.apache.camel.component.typesafeai.TypeSafeAiSemanticAdapter
+----
+
+`SecurityExpert` below is an illustrative application-provided expert exposing `injection` and
+`classify`; these names and its taxonomy are defined by that expert. The instruction-driven
+`decisions` bean uses the TypeSafe AI component configuration.
[source,yaml]
----
- semantic:
- question:
+ expert: security
+ state: "${body}"
+ evaluation:
injection:
- expert: security
- type: boolean
- state: "${body}"
- threshold: "{{security.injection.threshold}}"
- uncertainty: "{{security.injection.uncertainty}}"
- uncertaintyPolicy: fail
+ operation: injection
+ categories:
+ operation: classify
department:
- expert: general
+ expert: decisions
type: choice
- instructions: Which department should handle this message?
- criteria:
- billing: Invoices, payments and refunds
- technical: Bugs, outages and technical problems
+ parameters:
+ instructions: Which department should handle this message?
+ criteria:
+ billing: Payments, invoices and refunds
+ technical: Errors, outages and technical questions
+ sales: Product and purchasing questions
----
-Use `ref:injection` and `ref:department` in ordinary semantic expressions. Expert names can contain
-property placeholders, resolved during runtime selection. Java uses `.expert("security")`;
-XML uses `<question name="injection" type="boolean" expert="security"/>`. An omitted instruction
-stays absent in all three DSLs; an explicitly blank instruction is invalid.
+The block supplies default `expert` and `state` values; individual evaluations can override them.
+Evaluation names belong to the application, while operation names belong to the expert.
+`type: choice` is shorthand for the expert operation named `choice`; it does not let the route
+change an operation's result type. Specify exactly one of `operation` and `type`.
+Declaring an evaluation does not execute it.
+
+`parameters` can contain strings, booleans, numbers, nested maps and lists. Camel copies them
+into immutable declarations. Java, YAML and XML builders resolve property placeholders in string
+parameter values, including nested list and map values; map keys stay literal. Resolved strings
+remain strings: generic numeric parameters require numbers, while the `threshold` and `uncertainty`
+shorthand options also accept numeric placeholders. Direct `SemanticQuestion` construction takes
+already resolved parameter values and requires no Camel context. Omitted parameters stay absent: their behaviour belongs to the
+expert, and Camel does not insert threshold or confidence defaults. Parameter contracts use
+`String`, `Boolean`, `Number`, `Map` and `List` data shapes. Use `Number` with `integer = true`
+for whole-number parameters; do not use JVM numeric wrapper classes such as `Integer` in a
+contract. A provider consumes numeric values through `Number` across all DSLs.
+
+Camel validates recognised parameter names, required values, types and declared constraints.
+The expert checks operation-specific combinations and compatibility with its bean configuration.
+Startup validates all declarations, including unused evaluations, without inference. Route
+resource replacements also validate before publication, retaining previous declarations on failure.
+The low-level `SemanticQuestions.replace(String, Map)` API also validates new and changed
+unused declarations before publishing them on a started context.
+
+=== Calling an evaluation
+
+Use the Simple function `${semantic('evaluationName')}` or a semantic-language expression
+`ref:evaluationName`. Both return the operation's typed value, preserve the body, and publish the
+detailed `SemanticResult` in the `CamelSemanticResult` exchange property. Every occurrence invokes
+the expert; there is no implicit result caching. Retain a value in a Camel variable when it must
+be reused without another call.
+
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:incoming
+ steps:
+ - choice:
+ when:
+ - simple: "${semantic('injection')}"
+ steps:
+ - to: direct:security-review
+ otherwise:
+ steps:
+ - switch:
+ selector:
+ simple: "${semantic('department')}"
+ case:
+ - value: billing
+ uri: direct:billing
+ - value: technical
+ uri: direct:technical
+ - value: sales
+ uri: direct:sales
+ otherwise:
+ uri: direct:manual-review
+----
+
+Switch consumes scalar results. Classification returns a `Set<String>`, including an empty set
+when no labels apply. Use collections in predicates, for example
+`${semantic('categories')} contains 'privacy'`; labels are not numeric scores.
+The category and the meaning of a Boolean verdict must come from the selected expert's contract.
+A negative detection does not establish general safety or grant authorization.
+
+=== Structured input and specialised experts
+
+A specialised moderation expert can accept a map containing a user prompt, an assistant response,
+and supporting documents. Camel evaluates the state selector and passes that same object unchanged.
+It does not flatten a conversation into text, create prompts, or impose a universal conversation
+schema. The expert validates the content and must not modify the original state.
-The selected text is data. A fixed injection detector must not prepend a synthetic instruction.
-Its boolean `probability` is the score for the positive class (INJECTION), even when BENIGN wins.
-Threshold and uncertainty policies produce the decision. A negative result means this evaluation
-did not detect injection; it does not establish general safety or grant authorization.
-Probability is distinct from a rubric SCORE and from optional provider confidence. Missing optional
-metadata remains absent, and invalid output or provider failure remains an error. Continue,
-quarantine and review are application decisions expressed using Camel EIPs.
+For an application-provided `moderation` expert with operations `response_safety` and `categories`:
+
+[source,yaml]
+----
+- semantic:
+ expert: moderation
+ state: "${body}"
+ evaluation:
+ responseSafety:
+ operation: response_safety
+ safetyCategories:
+ operation: categories
+ parameters:
+ policy:
+ labels: [privacy, unsafe]
+ threshold: 0.8
+----
+
+Here `policy` is an illustrative nested expert parameter. The expert owns both its interpretation
+and any threshold filtering. The body might be a map with `prompt`, `response` and `documents` keys.
+A missing required response fails before inference; malformed provider output and operational
+failures follow normal Camel error handling, rather than becoming benign decisions.
+
+=== Equivalent Java and XML declarations
+
+Java uses the same declaration model and validation path:
+
+[source,java]
+----
+import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+
+semanticQuestions(this).expert("moderation").state("${body}")
+ .evaluation("responseSafety").operation("response_safety").end()
+ .evaluation("safetyCategories").operation("categories")
+ .parameter("policy", Map.of("labels", List.of("privacy", "unsafe"), "threshold", 0.8))
+ .register();
+----
+
+XML uses explicit typed values. A parameter and a map entry each contain exactly one value:
+`string`, `number`, `boolean`, `null`, `list` or `map`. Null is supported inside nested maps and
+lists when permitted by the expert. Top-level parameters must have their declared type; omit a
+parameter to request the expert's omission behaviour.
+
+[source,xml]
+----
+<semantic xmlns="http://camel.apache.org/schema/semantic" expert="moderation" state="${body}">
+ <evaluation name="responseSafety" operation="response_safety"/>
+ <evaluation name="safetyCategories" operation="categories">
+ <parameters>
+ <parameter name="policy">
+ <map>
+ <entry key="labels"><list><string>privacy</string><string>unsafe</string></list></entry>
+ <entry key="threshold"><number>0.8</number></entry>
+ </map>
+ </parameter>
+ </parameters>
+ </evaluation>
+</semantic>
+----
+
+== Instruction-driven shorthand
+
+The `question` syntax and Java convenience methods below target instruction-driven operations
+named `boolean`, `choice` and `score`. Their fields become expert parameters and are validated by
+the selected expert. TypeSafe AI implements these operations and requires nonblank instructions.
+Specialised operations normally use `evaluation` and `parameters` as shown above.
== Named questions
@@ -158,8 +294,9 @@ Existing expressions resolve the current definition on their next evaluation.
Reload validates the candidate declaration snapshot for initialized expressions before publishing
it, including expert capabilities, boolean predicate types, Simple state selectors, and compatible
batch selectors across resources. Failed validation leaves the previous declarations available.
-Unused declarations are validated when first referenced, as at startup. Removed declarations
-remain removable; an expression that still references a removed name fails when evaluated.
+Startup validates unused declarations as well. Resource reload validates new and changed declarations
+before publishing them, including unused evaluations. Removed declarations remain removable; an
+expression that still references a removed name fails when evaluated.
Validation registrations are weakly held: the registry does not keep expressions or their providers
alive. Constraints can remain until an unused expression is garbage-collected; stopping a route
does not necessarily release an expression that is still referenced elsewhere.
@@ -172,18 +309,11 @@ question declarations so the expressions compile against the replacement bean.
=== YAML reload and expert bean preparation
-YAML declarations are registered during the loader's existing pre-parse phase, before beans in
-the resource set are prepared. On initial loading, expert validation waits until expressions
-initialize, after bean preparation. On reload, replacements for initialized expressions are
-validated during pre-parsing, so the selected expert must already be registered.
-
-For example, changing an existing question to `expert: newSecurity` and introducing a
-`newSecurity` bean in the same reload fails if that bean is not yet in the registry. Keep expert
-beans registered outside the reloaded resource, prepare the new bean before reloading the
-declarations, or restart the application when changing both together. XML declaration loading
-runs after resource-set bean preparation; Java helpers validate when `register()` is called.
-The three DSLs share declaration fields and validation rules, but this YAML bean-order constraint
-remains because the existing resolver hook runs before bean preparation.
+YAML declarations are registered after all resources in the load operation have prepared their
+beans and before routes are constructed. This applies both at startup and on reload, so an expert
+bean and its evaluations can be introduced together, including in separate resources of the same
+load operation. XML declaration loading follows the same preparation order; Java helpers validate
+when `register()` is called.
=== Java declarations
@@ -341,12 +471,16 @@ in semantic evaluation, and Java applications using `camel-semantic` do not pull
== Results and policy
Only boolean questions can be predicates. A category string is never implicitly a boolean.
-A probability-based boolean uses an inclusive threshold (default `0.5`). A nonzero
+A direct `semantic` predicate rejects other result types at initialization. Simple's bare or negated
+`${semantic('name')}` predicate rejects them before inference when it is matched; comparisons and
+membership tests can consume their typed values.
+Decision policy is expert-owned. Camel never derives a Boolean from probability or applies a
+second threshold to the expert's returned value. The TypeSafe AI adapter uses an inclusive
+Boolean probability threshold (default `0.5`). A nonzero
`uncertainty` defines an inclusive band around that threshold. `fail` (the default) raises
-an error within the band; `non-match` returns false. An already-boolean provider supports the
-default policy without inventing a probability. Additional policy requirements must be
-supported by the provider. Choice results must name a declared criterion. Scores range from
-zero to the last rubric index and may be fractional.
+an error within the band; `non-match` returns false. Its Choice results must name a supplied
+criterion, and its scores range from zero to the last rubric index and may be fractional.
+Other experts declare their own policy parameters, labels and score ranges in their contracts.
Probabilities, provider confidence and selected values are separate. Missing optional fields
remain absent. Results can retain provider/model identity and usage metadata. These fields
@@ -407,7 +541,7 @@ For example, replace the route above with:
A result might contain `actionable -> true`, `department -> "billing"` and `urgency -> 1.2`.
Reading the stored map does not invoke the provider again. Each question retains its own
-threshold and uncertainty policy. There is no implicit AND/OR: batch expressions, including
+expert-owned threshold and uncertainty policy. There is no implicit AND/OR: batch expressions, including
a batch containing one boolean question, cannot be used as predicates.
`CamelSemanticResults` holds an immutable map of the detailed `SemanticResult` objects,
@@ -436,7 +570,7 @@ from resource reload. Questions that need a previous answer require separate eva
`SemanticAdapter.evaluateBatch(Map<String, SemanticQuestion>, Object)` has a default
implementation that invokes the existing single-question method sequentially, preserving
-compatibility with existing adapters. Questions are grouped by the resolved expert instance,
+experts that do not implement a provider-specific batch optimisation. Questions are grouped by the resolved expert instance,
and each group is passed to that instance's batch method. Two instances of the same class remain
separate groups. Each group must return exactly its assigned names. Camel validates the complete
combined result before publishing decisions or diagnostics, preserving the original reference order.
@@ -645,35 +779,52 @@ the action. The example's threshold is a decision policy, not an accuracy guaran
This pattern uses Validate and existing authorization services. It does not add a semantic
`AuthorizationPolicy` implementation or replace a component's internal guardrail interfaces.
-== Expert capabilities
+== Static operation contracts
-`SemanticAdapter.capabilities()` describes accepted text or structured input, supported result
-types, instruction and criteria support, optional probability/confidence semantics, and known limits.
-The default reads `@SemanticExpert` from the provider class. Unannotated legacy adapters report
-`known=false`, with no claimed capabilities. The language still requires instructions for these
-legacy adapters before calling their existing `validate` method. Providers that accept absent
-instructions must explicitly advertise optional or unsupported instructions.
-An adapter can override capabilities for its configured model using the named builder:
+Read `SemanticCapabilities.from(MyExpert.class)` to inspect a contract without constructing an
+expert, loading a model or contacting a service. The runtime uses the same cached class contract.
+The returned operation
+and parameter maps, supported labels and input shapes are immutable.
+
+For example, an expert can declare a fixed detection operation:
[source,java]
----
-@Override
-public SemanticCapabilities capabilities() {
- return SemanticAdapter.super.capabilities().toBuilder()
- .maxChoices(32)
- .build();
+@SemanticExpert(name = "detector", description = "Text injection detector",
+ provider = "example", artifactId = "example-expert",
+ operations = @SemanticOperation(name = "injection", description = "Detect injection",
+ inputTypes = SemanticExpert.InputType.TEXT,
+ inputRequirements = "Text to assess",
+ resultType = SemanticExpert.ResultType.BOOLEAN,
+ resultMeaning = "True means injection was detected",
+ probability = true, probabilityMeaning = "Probability of injection",
+ parameters = @SemanticParameter(name = "threshold", description = "Detection threshold",
+ type = Number.class, minimum = 0, maximum = 1,
+ omission = "Use the service's configured threshold")))
+public class SecurityExpert implements SemanticAdapter {
+ // validate, validateInput and evaluate implement this declared contract.
}
----
-This example narrows an annotated adapter's limits. `SemanticCapabilities.builder()` creates an
-explicit descriptor from scratch; input and result sets are empty until supplied. Descriptors
-are immutable. Capability reporting and declaration validation must not perform inference or
-load models. Runtime input and response validation still apply.
-
-Providers declare static capabilities with `org.apache.camel.semantic.SemanticExpert` from
-`camel-semantic`. The annotation describes the implementation; it neither selects an expert nor
-describes application bean names. Runtime capabilities and `validate` remain authoritative for
-configured instances. No dedicated expert catalog model or discovery API is provided.
+Each operation declares a result type and its meaning. Score operations document their scale and
+can declare numeric bounds. Choice and classification operations can declare a fixed label set;
+an instruction-driven expert validates its application-supplied vocabulary itself. Optional
+probability, per-label probability and confidence fields must declare their meaning. Camel does
+not manufacture confidence from a verdict, nor require optional information on every response.
+It rejects information the contract did not declare, malformed numeric values, wrong result types
+and results outside declared common constraints.
+
+An expert applies explicitly requested policy exactly once, either in its service or its adapter.
+It preserves service defaults when parameters are absent. If requested policy needs unavailable
+confidence or probability, the expert rejects the declaration when known statically, or fails that
+evaluation when the absence is response-dependent. It must not silently ignore the request.
+Uncertain outcomes use an operation's documented values or an error; Camel has no universal
+uncertainty sentinel. Classification filtering, including per-label thresholds, also belongs to
+the expert. TypeSafe AI's `uncertaintyPolicy` supports `fail` or `non-match`; other experts need not
+support that vocabulary.
+
+Generated expert descriptors, expert-specific offline validation and Catalog API extensions are
+separate work. This contract does not add a new Camel Catalog model.
== Implementing an adapter
@@ -691,8 +842,11 @@ Discovery checks the declarations for ambiguity before resolving the implementat
explicit selection. Only the selected adapter is constructed, through Camel's `Injector`.
Explicit bean or class selection also works when packaging does not expose discovery resources.
-The language checks known capabilities and then calls `validate` for model-specific restrictions
-before traffic. Providers should also validate when their SPI is called directly. `evaluate`
+The language checks the static operation contract and then calls `validate` for operation-specific
+parameter combinations and bean configuration before traffic. `validateInput` checks detailed
+message requirements before evaluation. All input checks in a batch run before any expert is invoked.
+`evaluate` returns a typed `SemanticResult` after applying the expert's policy. A probability-only
+Boolean result is invalid: the expert must supply its Boolean verdict. Providers should also validate when their SPI is called directly. `evaluate`
must be thread-safe, bound time and resource consumption, honor interruption and propagate
operational failures without disclosing input or credentials. Implement `CamelContextAware`
for context injection and Camel `Service` interfaces when the adapter owns resources.
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
index a6a26ee3573a..eb35a1bb283f 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
@@ -660,6 +660,14 @@ to call the `safeQuote` function as shown below:
----
+=== Semantic Functions
+
+[width="100%",cols="2,1,7",options="header"]
+|===
+|Function |Type |Description
+|`semantic(name)` | `Object` | Invokes a named xref:semantic-language.adoc[Semantic evaluation], for example `${semantic('injection')}`. Returns its typed result, preserves the body, and publishes `CamelSemanticResult`. Each occurrence invokes the expert. Requires camel-semantic.
+|===
+
=== XML & JSON Functions
Functions to work with XML and JSON payloads.
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/typesafe-ai-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/typesafe-ai-component.adoc
index 942b64630924..9504107aebe2 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/typesafe-ai-component.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/typesafe-ai-component.adoc
@@ -21,6 +21,12 @@ It calls the service directly using the JDK HTTP client and Camel JSON utilities
For semantic conditions in Java, XML and YAML, use the xref:languages:typesafe-ai-language.adoc[TypeSafe AI language] supplied by this component.
This component also advertises an adapter for the xref:languages:semantic-language.adoc[Semantic language].
+Its static expert contract exposes `boolean`, `choice` and `score` operations. The adapter requires
+instructions and validates the supplied criteria or score levels before transport is initialized.
+For Boolean evaluations it applies the requested `threshold`, `uncertainty` and `uncertaintyPolicy`
+once and returns both the typed verdict and the provider probability. Omitted policy parameters
+use this adapter's defaults: threshold `0.5`, no uncertainty band, and failure for uncertain results.
+The semantic runtime validates the result and does not apply a second policy.
Named boolean, choice and score questions use the common contract and inherit this component's
credentials, model and transport settings without requiring a TypeSafe AI endpoint in the route.
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json
index 4e05df3493c2..8d6da5d4eefc 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json
@@ -3,7 +3,7 @@
"kind": "language",
"name": "semantic",
"title": "Semantic Evaluation",
- "description": "Evaluate named questions about message content to produce boolean decisions, categories and scores through provider adapters",
+ "description": "Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters",
"deprecated": false,
"firstVersion": "4.23.0",
"label": "language,ai",
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
index d7286003a364..f11ea5d6b4bd 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
@@ -110,76 +110,77 @@
"isEmpty(exp)": { "index": 81, "kind": "function", "displayName": "Is Empty", "group": "condition", "label": "condition", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Whether the message body (or expression) is null or empty (list\/map types are tested if they have 0 elements).", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": fals [...]
"isNumeric(exp)": { "index": 82, "kind": "function", "displayName": "Is Numeric Value", "group": "condition", "label": "condition", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Whether the message body (or expression) is numeric value (0..9). For more advanced checks use the `regex` operator.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", " [...]
"jq(input,exp)": { "index": 83, "kind": "function", "displayName": "JQ", "group": "json", "label": "json", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with JSon data, then this allows using the JQ language, for example, to extract data from the message body (in JSon format). This requires having camel-jq JAR on the classpath. For input (optional), you can choose [...]
- "jsonpath(input,exp)": { "index": 84, "kind": "function", "displayName": "JSonPath", "group": "json", "label": "json", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with JSon data, then this allows using the JSonPath language, for example, to extract data from the message body (in JSon format). This requires having camel-jsonpath JAR on the classpath. For input (op [...]
- "join(separator,prefix,exp)": { "index": 85, "kind": "function", "displayName": "Join", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The join function iterates the message body\/expression and joins the data into a string. The separator is by default a comma. The prefix is optional. The join uses the message body as source by default. It i [...]
- "length(exp)": { "index": 86, "kind": "function", "displayName": "Length", "group": "core", "label": "core", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The payload length (number of bytes) of the message body (or expression).", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressi [...]
- "list(val...)": { "index": 87, "kind": "function", "displayName": "Create List of values", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.ArrayList", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The list function creates an ArrayList with the given set of values.", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": true, "description": [...]
- "listAdd(source,exp)": { "index": 88, "kind": "function", "displayName": "List Add", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Adds the result of the expression to the message body (or expression) which is a list", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaultValue" [...]
- "listRemove(source,exp)": { "index": 89, "kind": "function", "displayName": "List Remove", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Removes the result of the expression from the message body (or expression) which is a list", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "de [...]
- "load(file)": { "index": 90, "kind": "function", "displayName": "Load", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Loads the content of the resource from classpath (cannot load from file-system to avoid dangerous situations).", "ognl": false, "suffix": "}", "params": [ { "name": "file", "javaType": "String", "required": true, "description": [...]
- "lowercase(exp)": { "index": 91, "kind": "function", "displayName": "Lowercase", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Lowercases the message body (or expression)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitted [...]
- "mandatoryBodyAs(type)": { "index": 92, "kind": "function", "displayName": "Mandatory Body As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the message body to the given type (classname). If the body is null then the function will fail with an exception", "ognl": true, "suffix": "}", "params": [ { "name": "type", "javaType": "String" [...]
- "map(key1,value1,...)": { "index": 93, "kind": "function", "displayName": "Create Map of pairs", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.LinkedHashMap", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The map function creates a LinkedHashMap with the given set of pairs.", "ognl": false, "suffix": "}", "params": [ { "name": "key1", "javaType": "Object", "required": true, "d [...]
- "mapAdd(source,key,exp)": { "index": 94, "kind": "function", "displayName": "Map Add", "group": "collection", "label": "collection", "required": false, "javaType": "Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Adds the result of the expression to the message body (or expression) which is a map", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaultValue" [...]
- "mapRemove(source,key)": { "index": 95, "kind": "function", "displayName": "Map Remove", "group": "collection", "label": "collection", "required": false, "javaType": "Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Removes the result of the expression from the message body (or expression) which is a map", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaul [...]
- "max(val...)": { "index": 96, "kind": "function", "displayName": "Maximum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the maximum number from all the values (integral numbers only).", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "required": false, "defaultValue": "body", "description": "Val [...]
- "messageAs(type)": { "index": 97, "kind": "function", "displayName": "Message As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the message to the given type (classname).", "ognl": true, "suffix": "}", "params": [ { "name": "type", "javaType": "String", "required": true, "description": "The target type classname" } ] },
- "messageHistory(boolean)": { "index": 98, "kind": "function", "displayName": "Print Message History", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The message history of the current exchange (how it has been routed). This is similar to the route stack-trace message history the error handler logs in case of an unhandled exception. The boolean c [...]
- "messageTimestamp": { "index": 99, "kind": "function", "displayName": "Message Timestamp", "group": "core", "label": "core", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The message timestamp (millis since epoc) that this message originates from. Some systems like JMS, Kafka, AWS have a timestamp on the event\/message that Camel received. This method returns the timestamp if a t [...]
- "min(val...)": { "index": 100, "kind": "function", "displayName": "Minimum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the minimum number from all the values (integral numbers only).", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "required": false, "defaultValue": "body", "description": "Va [...]
- "newEmpty(type)": { "index": 101, "kind": "function", "displayName": "Create Empty Object", "group": "collection", "label": "collection", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Creates a new empty object (decided by type). Use `string` to create an empty String. Use `list` to create an empty `java.util.ArrayList`. Use `map` to create an empty `java.util.LinkedHashMap`. U [...]
- "normalizeWhitespace(exp)": { "index": 102, "kind": "function", "displayName": "Normalize Whitespace", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Normalizes the whitespace in the message body (or expression) by cleaning up excess whitespaces.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": f [...]
- "not(exp)": { "index": 103, "kind": "function", "displayName": "Not", "group": "condition", "label": "condition", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Evaluates the predicate and returns the opposite.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The predicate. When omitted [...]
- "null": { "index": 104, "kind": "function", "displayName": "Null", "group": "other", "label": "other", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a null value", "ognl": false, "suffix": "}", "examples": [ "${null} -> null" ] },
- "originalBody": { "index": 105, "kind": "function", "displayName": "Original Body", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The original incoming body (only available if allowUseOriginalMessage=true).", "ognl": false, "suffix": "}" },
- "pad(exp,width,separator)": { "index": 106, "kind": "function", "displayName": "Pad String", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Pads the expression with extra padding if necessary, according to the total width. The separator is by default a space. A positive width pads to the right (after the value), and a negative width pads to [...]
- "pretty(exp)": { "index": 107, "kind": "function", "displayName": "Pretty Print", "group": "xml", "label": "json,xml", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to a String, and attempts to pretty print if JSon or XML, otherwise the expression is returned as the String value.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "O [...]
- "prettyBody": { "index": 108, "kind": "function", "displayName": "Pretty Body", "group": "xml", "label": "json,xml", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to a String, and attempts to pretty print if JSon or XML; otherwise the body is returned as the String value.", "ognl": false, "suffix": "}" },
- "sort(exp,reverse)": { "index": 109, "kind": "function", "displayName": "Sort", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sorts the message body or expression in natural order", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression [...]
- "toPrettyJson(exp)": { "index": 110, "kind": "function", "displayName": "To Pretty JSon", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to JSon String representation.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressio [...]
- "toPrettyJsonBody": { "index": 111, "kind": "function", "displayName": "To Pretty JSon Body", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to JSon String representation.", "ognl": false, "suffix": "}" },
- "toJson(exp)": { "index": 112, "kind": "function", "displayName": "To JSon", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to JSon String representation.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitt [...]
- "toJsonBody": { "index": 113, "kind": "function", "displayName": "To JSon Body", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to JSon String representation.", "ognl": false, "suffix": "}" },
- "properties:key:default": { "index": 114, "kind": "function", "displayName": "Property Placeholder", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Lookup a property placeholder with the given key. If the key does not exist nor has a value, then an optional default value can be specified.", "ognl": false, "suffix": "}", "params": [ { "name": "ke [...]
- "propertiesExist:key": { "index": 115, "kind": "function", "displayName": "Property Placeholder Exists", "group": "core", "label": "core", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Checks whether a property placeholder with the given key exists or not. The result can be negated by prefixing the key with !", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaTy [...]
- "quote(exp)": { "index": 116, "kind": "function", "displayName": "Quote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) as a double quoted string", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressio [...]
- "random(min,max)": { "index": 117, "kind": "function", "displayName": "Random", "group": "number", "label": "number", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a random number between min and max (exclusive)", "ognl": false, "suffix": "}", "params": [ { "name": "min", "javaType": "int", "required": true, "description": "The minimum value (inclusive)" }, { "name": "max" [...]
- "range(min,max)": { "index": 118, "kind": "function", "displayName": "Range", "group": "number", "label": "number", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of increasing integers between the given interval (exclusive)", "ognl": false, "suffix": "}", "params": [ { "name": "min", "javaType": "int", "required": true, "description": "The start value (inclusive)" [...]
- "ref:name": { "index": 119, "kind": "function", "displayName": "Bean By Id", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "To look up a bean from the Registry with the given name.", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The bean name in the registry" } ], "examples": [...]
- "replace(from,to,exp)": { "index": 120, "kind": "function", "displayName": "Replace String Values", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Replace all the string values in the message body\/expression. To make it easier to replace single and double quotes, then you can use XML escaped values `\\"` as double quote, `\\'` as [...]
- "reverse(val...)": { "index": 121, "kind": "function", "displayName": "Reverse Values", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of all the values, but in reverse order", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": false, "defaultValue": "body", "description": "Value [...]
- "routeGroup": { "index": 122, "kind": "function", "displayName": "Route Group", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The route group of the current route the Exchange is being routed. Not all routes have a group assigned, so this may be null.", "ognl": false, "suffix": "}" },
- "routeId": { "index": 123, "kind": "function", "displayName": "Route Id", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The route id of the current route the Exchange is being routed", "ognl": false, "suffix": "}", "examples": [ "${routeId} -> myRoute" ] },
- "safeQuote(exp)": { "index": 124, "kind": "function", "displayName": "Safe Quote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) safely quoted if needed", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The ex [...]
- "setAttachment(key,exp)": { "index": 125, "kind": "function", "displayName": "Set Attachment", "group": "attachment", "label": "attachment", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets an attachment with payload from the message body\/expression.", "ognl": false, "suffix": "}" },
- "setHeader(name,type,exp)": { "index": 126, "kind": "function", "displayName": "Set Header", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets a message header with the given expression (optional converting to the given type)", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "T [...]
- "setVariable(name,type,exp)": { "index": 127, "kind": "function", "displayName": "Set Variable", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets a variable with the given expression (optional converting to the given type)", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The [...]
- "shuffle(val...)": { "index": 128, "kind": "function", "displayName": "Shuffle Values", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of all the values shuffled in random order", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": false, "defaultValue": "body", "description": "Va [...]
- "simpleJsonpath(input,exp)": { "index": 129, "kind": "function", "displayName": "Simple JSonPath", "group": "json", "label": "json", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with JSon data, then this allows using the Simple JSonPath language, for example, to extract data from the message body (in JSon format). For input (optional), you can choose `header:key`, [...]
- "size(exp)": { "index": 130, "kind": "function", "displayName": "Size", "group": "collection", "label": "collection", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the number of elements in collection or array based payloads. If the value is null then 0 is returned, otherwise 1.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": [...]
- "skip(num)": { "index": 131, "kind": "function", "displayName": "Skip First Items from the Message Body", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.Iterator", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The skip function iterates the message body and skips the first number of items. This can be used with the Splitter EIP to split a message body and skip the first N numbe [...]
- "split(exp,separator)": { "index": 132, "kind": "function", "displayName": "Split String Values", "group": "collection", "label": "collection", "required": false, "javaType": "String[]", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Splits the message body\/expression as a String value using the separator into a String array", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": [...]
- "stepId": { "index": 133, "kind": "function", "displayName": "Step Id", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the id of the current step the Exchange is being routed.", "ognl": false, "suffix": "}" },
- "substring(head,tail)": { "index": 134, "kind": "function", "displayName": "Substring", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression by clipping characters off its ends (not by positions). With one number, a positive number clips that many characters from the beginning, and a negative num [...]
- "substringAfter(exp,before)": { "index": 135, "kind": "function", "displayName": "Substring After", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that comes after. Returns null if nothing comes after.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "requi [...]
- "substringBefore(exp,before)": { "index": 136, "kind": "function", "displayName": "Substring Before", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that comes before. Returns null if nothing comes before.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "r [...]
- "substringBetween(exp,after,before)": { "index": 137, "kind": "function", "displayName": "Substring Between", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that are between after and before. Returns null if nothing comes between.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", [...]
- "sum(val...)": { "index": 138, "kind": "function", "displayName": "Calculate Sum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sums together all the values as integral numbers. This function can also be used to subtract by using negative numbers.", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "requir [...]
- "sys.name": { "index": 139, "kind": "function", "displayName": "JVM System Property", "group": "other", "label": "other", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The JVM system property with the given name", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The system property name" } ], "examples": [ "${sy [...]
- "threadId": { "index": 140, "kind": "function", "displayName": "Thread Id", "group": "other", "label": "other", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the id of the current thread. Can be used for logging.", "ognl": false, "suffix": "}" },
- "threadName": { "index": 141, "kind": "function", "displayName": "Thread Name", "group": "other", "label": "other", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the name of the current thread. Can be used for logging.", "ognl": false, "suffix": "}" },
- "throwException(msg,type)": { "index": 142, "kind": "function", "displayName": "Throw Exception", "group": "core", "label": "core", "required": false, "javaType": "java.lang.Exception", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Deliberately throws an error. Uses IllegalArgumentException by default if no type is specified (use fully qualified classname).", "ognl": false, "suffix": "}", "params": [ { "name": "msg", [...]
- "trim(exp)": { "index": 143, "kind": "function", "displayName": "Trim", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The trim function trims the message body (or expression) by removing all leading and trailing white spaces.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue" [...]
- "type:name.field": { "index": 144, "kind": "function", "displayName": "Java Field Value", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "To refer to a type or field by its classname. To refer to a field, you can append .FIELD_NAME. For example, you can refer to the constant field from Exchange as: `org.apache.camel.Exchange.FILE_NAME`", "ognl": [...]
- "kindOfType(exp)": { "index": 145, "kind": "function", "displayName": "Kind of Type", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "What kind of type is the value (null,number,string,boolean,array,object)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": [...]
- "unquote(exp)": { "index": 146, "kind": "function", "displayName": "Unquote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) with any leading\/ending quotes removed", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "descriptio [...]
- "uppercase(exp)": { "index": 147, "kind": "function", "displayName": "Uppercase", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Uppercases the message body (or expression)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitted [...]
- "uuid(type)": { "index": 148, "kind": "function", "displayName": "Generate UUID", "group": "other", "label": "other", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a UUID using the Camel `UuidGenerator`. You can choose between `default`, `classic`, `short`, `simple` and `random` as the type. If no type is given, the default is used. It is also possible to use a custom ` [...]
- "val(exp)": { "index": 149, "kind": "function", "displayName": "Value", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the expression as a constant value", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": true, "description": "The constant value expression" } ], "examples": [ "${val(42)} -> 42" [...]
- "variable.name": { "index": 150, "kind": "function", "displayName": "Variable", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The variable with the given name", "ognl": true, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The variable name" } ], "examples": [ "${variable.myVar} -> variable v [...]
- "variableAs(key,type)": { "index": 151, "kind": "function", "displayName": "Variable As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaType": "String", "required": true, "description": "The variable name" }, { "name": "type", [...]
- "variables": { "index": 152, "kind": "function", "displayName": "Variables", "group": "core", "label": "core", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}", "examples": [ "${variables} -> {myVar=value1, count=5}" ] },
- "xpath(input,exp)": { "index": 153, "kind": "function", "displayName": "XPath", "group": "xml", "label": "xml", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optional), you ca [...]
+ "semantic(name)": { "index": 84, "kind": "function", "displayName": "Semantic Evaluation", "group": "ai", "label": "ai", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Invoke a named semantic evaluation and return its typed value. Preserves the body and publishes CamelSemanticResult. Each occurrence invokes the expert. Requires camel-semantic.", "ognl": false, "suffix": "}", "pa [...]
+ "jsonpath(input,exp)": { "index": 85, "kind": "function", "displayName": "JSonPath", "group": "json", "label": "json", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with JSon data, then this allows using the JSonPath language, for example, to extract data from the message body (in JSon format). This requires having camel-jsonpath JAR on the classpath. For input (op [...]
+ "join(separator,prefix,exp)": { "index": 86, "kind": "function", "displayName": "Join", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The join function iterates the message body\/expression and joins the data into a string. The separator is by default a comma. The prefix is optional. The join uses the message body as source by default. It i [...]
+ "length(exp)": { "index": 87, "kind": "function", "displayName": "Length", "group": "core", "label": "core", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The payload length (number of bytes) of the message body (or expression).", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressi [...]
+ "list(val...)": { "index": 88, "kind": "function", "displayName": "Create List of values", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.ArrayList", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The list function creates an ArrayList with the given set of values.", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": true, "description": [...]
+ "listAdd(source,exp)": { "index": 89, "kind": "function", "displayName": "List Add", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Adds the result of the expression to the message body (or expression) which is a list", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaultValue" [...]
+ "listRemove(source,exp)": { "index": 90, "kind": "function", "displayName": "List Remove", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Removes the result of the expression from the message body (or expression) which is a list", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "de [...]
+ "load(file)": { "index": 91, "kind": "function", "displayName": "Load", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Loads the content of the resource from classpath (cannot load from file-system to avoid dangerous situations).", "ognl": false, "suffix": "}", "params": [ { "name": "file", "javaType": "String", "required": true, "description": [...]
+ "lowercase(exp)": { "index": 92, "kind": "function", "displayName": "Lowercase", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Lowercases the message body (or expression)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitted [...]
+ "mandatoryBodyAs(type)": { "index": 93, "kind": "function", "displayName": "Mandatory Body As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the message body to the given type (classname). If the body is null then the function will fail with an exception", "ognl": true, "suffix": "}", "params": [ { "name": "type", "javaType": "String" [...]
+ "map(key1,value1,...)": { "index": 94, "kind": "function", "displayName": "Create Map of pairs", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.LinkedHashMap", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The map function creates a LinkedHashMap with the given set of pairs.", "ognl": false, "suffix": "}", "params": [ { "name": "key1", "javaType": "Object", "required": true, "d [...]
+ "mapAdd(source,key,exp)": { "index": 95, "kind": "function", "displayName": "Map Add", "group": "collection", "label": "collection", "required": false, "javaType": "Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Adds the result of the expression to the message body (or expression) which is a map", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaultValue" [...]
+ "mapRemove(source,key)": { "index": 96, "kind": "function", "displayName": "Map Remove", "group": "collection", "label": "collection", "required": false, "javaType": "Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Removes the result of the expression from the message body (or expression) which is a map", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaul [...]
+ "max(val...)": { "index": 97, "kind": "function", "displayName": "Maximum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the maximum number from all the values (integral numbers only).", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "required": false, "defaultValue": "body", "description": "Val [...]
+ "messageAs(type)": { "index": 98, "kind": "function", "displayName": "Message As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the message to the given type (classname).", "ognl": true, "suffix": "}", "params": [ { "name": "type", "javaType": "String", "required": true, "description": "The target type classname" } ] },
+ "messageHistory(boolean)": { "index": 99, "kind": "function", "displayName": "Print Message History", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The message history of the current exchange (how it has been routed). This is similar to the route stack-trace message history the error handler logs in case of an unhandled exception. The boolean c [...]
+ "messageTimestamp": { "index": 100, "kind": "function", "displayName": "Message Timestamp", "group": "core", "label": "core", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The message timestamp (millis since epoc) that this message originates from. Some systems like JMS, Kafka, AWS have a timestamp on the event\/message that Camel received. This method returns the timestamp if a [...]
+ "min(val...)": { "index": 101, "kind": "function", "displayName": "Minimum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the minimum number from all the values (integral numbers only).", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "required": false, "defaultValue": "body", "description": "Va [...]
+ "newEmpty(type)": { "index": 102, "kind": "function", "displayName": "Create Empty Object", "group": "collection", "label": "collection", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Creates a new empty object (decided by type). Use `string` to create an empty String. Use `list` to create an empty `java.util.ArrayList`. Use `map` to create an empty `java.util.LinkedHashMap`. U [...]
+ "normalizeWhitespace(exp)": { "index": 103, "kind": "function", "displayName": "Normalize Whitespace", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Normalizes the whitespace in the message body (or expression) by cleaning up excess whitespaces.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": f [...]
+ "not(exp)": { "index": 104, "kind": "function", "displayName": "Not", "group": "condition", "label": "condition", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Evaluates the predicate and returns the opposite.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The predicate. When omitted [...]
+ "null": { "index": 105, "kind": "function", "displayName": "Null", "group": "other", "label": "other", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a null value", "ognl": false, "suffix": "}", "examples": [ "${null} -> null" ] },
+ "originalBody": { "index": 106, "kind": "function", "displayName": "Original Body", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The original incoming body (only available if allowUseOriginalMessage=true).", "ognl": false, "suffix": "}" },
+ "pad(exp,width,separator)": { "index": 107, "kind": "function", "displayName": "Pad String", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Pads the expression with extra padding if necessary, according to the total width. The separator is by default a space. A positive width pads to the right (after the value), and a negative width pads to [...]
+ "pretty(exp)": { "index": 108, "kind": "function", "displayName": "Pretty Print", "group": "xml", "label": "json,xml", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to a String, and attempts to pretty print if JSon or XML, otherwise the expression is returned as the String value.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "O [...]
+ "prettyBody": { "index": 109, "kind": "function", "displayName": "Pretty Body", "group": "xml", "label": "json,xml", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to a String, and attempts to pretty print if JSon or XML; otherwise the body is returned as the String value.", "ognl": false, "suffix": "}" },
+ "sort(exp,reverse)": { "index": 110, "kind": "function", "displayName": "Sort", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sorts the message body or expression in natural order", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression [...]
+ "toPrettyJson(exp)": { "index": 111, "kind": "function", "displayName": "To Pretty JSon", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to JSon String representation.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressio [...]
+ "toPrettyJsonBody": { "index": 112, "kind": "function", "displayName": "To Pretty JSon Body", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to JSon String representation.", "ognl": false, "suffix": "}" },
+ "toJson(exp)": { "index": 113, "kind": "function", "displayName": "To JSon", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to JSon String representation.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitt [...]
+ "toJsonBody": { "index": 114, "kind": "function", "displayName": "To JSon Body", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to JSon String representation.", "ognl": false, "suffix": "}" },
+ "properties:key:default": { "index": 115, "kind": "function", "displayName": "Property Placeholder", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Lookup a property placeholder with the given key. If the key does not exist nor has a value, then an optional default value can be specified.", "ognl": false, "suffix": "}", "params": [ { "name": "ke [...]
+ "propertiesExist:key": { "index": 116, "kind": "function", "displayName": "Property Placeholder Exists", "group": "core", "label": "core", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Checks whether a property placeholder with the given key exists or not. The result can be negated by prefixing the key with !", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaTy [...]
+ "quote(exp)": { "index": 117, "kind": "function", "displayName": "Quote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) as a double quoted string", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressio [...]
+ "random(min,max)": { "index": 118, "kind": "function", "displayName": "Random", "group": "number", "label": "number", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a random number between min and max (exclusive)", "ognl": false, "suffix": "}", "params": [ { "name": "min", "javaType": "int", "required": true, "description": "The minimum value (inclusive)" }, { "name": "max" [...]
+ "range(min,max)": { "index": 119, "kind": "function", "displayName": "Range", "group": "number", "label": "number", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of increasing integers between the given interval (exclusive)", "ognl": false, "suffix": "}", "params": [ { "name": "min", "javaType": "int", "required": true, "description": "The start value (inclusive)" [...]
+ "ref:name": { "index": 120, "kind": "function", "displayName": "Bean By Id", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "To look up a bean from the Registry with the given name.", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The bean name in the registry" } ], "examples": [...]
+ "replace(from,to,exp)": { "index": 121, "kind": "function", "displayName": "Replace String Values", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Replace all the string values in the message body\/expression. To make it easier to replace single and double quotes, then you can use XML escaped values `\\"` as double quote, `\\'` as [...]
+ "reverse(val...)": { "index": 122, "kind": "function", "displayName": "Reverse Values", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of all the values, but in reverse order", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": false, "defaultValue": "body", "description": "Value [...]
+ "routeGroup": { "index": 123, "kind": "function", "displayName": "Route Group", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The route group of the current route the Exchange is being routed. Not all routes have a group assigned, so this may be null.", "ognl": false, "suffix": "}" },
+ "routeId": { "index": 124, "kind": "function", "displayName": "Route Id", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The route id of the current route the Exchange is being routed", "ognl": false, "suffix": "}", "examples": [ "${routeId} -> myRoute" ] },
+ "safeQuote(exp)": { "index": 125, "kind": "function", "displayName": "Safe Quote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) safely quoted if needed", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The ex [...]
+ "setAttachment(key,exp)": { "index": 126, "kind": "function", "displayName": "Set Attachment", "group": "attachment", "label": "attachment", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets an attachment with payload from the message body\/expression.", "ognl": false, "suffix": "}" },
+ "setHeader(name,type,exp)": { "index": 127, "kind": "function", "displayName": "Set Header", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets a message header with the given expression (optional converting to the given type)", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "T [...]
+ "setVariable(name,type,exp)": { "index": 128, "kind": "function", "displayName": "Set Variable", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets a variable with the given expression (optional converting to the given type)", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The [...]
+ "shuffle(val...)": { "index": 129, "kind": "function", "displayName": "Shuffle Values", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of all the values shuffled in random order", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": false, "defaultValue": "body", "description": "Va [...]
+ "simpleJsonpath(input,exp)": { "index": 130, "kind": "function", "displayName": "Simple JSonPath", "group": "json", "label": "json", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with JSon data, then this allows using the Simple JSonPath language, for example, to extract data from the message body (in JSon format). For input (optional), you can choose `header:key`, [...]
+ "size(exp)": { "index": 131, "kind": "function", "displayName": "Size", "group": "collection", "label": "collection", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the number of elements in collection or array based payloads. If the value is null then 0 is returned, otherwise 1.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": [...]
+ "skip(num)": { "index": 132, "kind": "function", "displayName": "Skip First Items from the Message Body", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.Iterator", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The skip function iterates the message body and skips the first number of items. This can be used with the Splitter EIP to split a message body and skip the first N numbe [...]
+ "split(exp,separator)": { "index": 133, "kind": "function", "displayName": "Split String Values", "group": "collection", "label": "collection", "required": false, "javaType": "String[]", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Splits the message body\/expression as a String value using the separator into a String array", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": [...]
+ "stepId": { "index": 134, "kind": "function", "displayName": "Step Id", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the id of the current step the Exchange is being routed.", "ognl": false, "suffix": "}" },
+ "substring(head,tail)": { "index": 135, "kind": "function", "displayName": "Substring", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression by clipping characters off its ends (not by positions). With one number, a positive number clips that many characters from the beginning, and a negative num [...]
+ "substringAfter(exp,before)": { "index": 136, "kind": "function", "displayName": "Substring After", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that comes after. Returns null if nothing comes after.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "requi [...]
+ "substringBefore(exp,before)": { "index": 137, "kind": "function", "displayName": "Substring Before", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that comes before. Returns null if nothing comes before.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "r [...]
+ "substringBetween(exp,after,before)": { "index": 138, "kind": "function", "displayName": "Substring Between", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that are between after and before. Returns null if nothing comes between.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", [...]
+ "sum(val...)": { "index": 139, "kind": "function", "displayName": "Calculate Sum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sums together all the values as integral numbers. This function can also be used to subtract by using negative numbers.", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "requir [...]
+ "sys.name": { "index": 140, "kind": "function", "displayName": "JVM System Property", "group": "other", "label": "other", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The JVM system property with the given name", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The system property name" } ], "examples": [ "${sy [...]
+ "threadId": { "index": 141, "kind": "function", "displayName": "Thread Id", "group": "other", "label": "other", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the id of the current thread. Can be used for logging.", "ognl": false, "suffix": "}" },
+ "threadName": { "index": 142, "kind": "function", "displayName": "Thread Name", "group": "other", "label": "other", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the name of the current thread. Can be used for logging.", "ognl": false, "suffix": "}" },
+ "throwException(msg,type)": { "index": 143, "kind": "function", "displayName": "Throw Exception", "group": "core", "label": "core", "required": false, "javaType": "java.lang.Exception", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Deliberately throws an error. Uses IllegalArgumentException by default if no type is specified (use fully qualified classname).", "ognl": false, "suffix": "}", "params": [ { "name": "msg", [...]
+ "trim(exp)": { "index": 144, "kind": "function", "displayName": "Trim", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The trim function trims the message body (or expression) by removing all leading and trailing white spaces.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue" [...]
+ "type:name.field": { "index": 145, "kind": "function", "displayName": "Java Field Value", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "To refer to a type or field by its classname. To refer to a field, you can append .FIELD_NAME. For example, you can refer to the constant field from Exchange as: `org.apache.camel.Exchange.FILE_NAME`", "ognl": [...]
+ "kindOfType(exp)": { "index": 146, "kind": "function", "displayName": "Kind of Type", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "What kind of type is the value (null,number,string,boolean,array,object)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": [...]
+ "unquote(exp)": { "index": 147, "kind": "function", "displayName": "Unquote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) with any leading\/ending quotes removed", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "descriptio [...]
+ "uppercase(exp)": { "index": 148, "kind": "function", "displayName": "Uppercase", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Uppercases the message body (or expression)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitted [...]
+ "uuid(type)": { "index": 149, "kind": "function", "displayName": "Generate UUID", "group": "other", "label": "other", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a UUID using the Camel `UuidGenerator`. You can choose between `default`, `classic`, `short`, `simple` and `random` as the type. If no type is given, the default is used. It is also possible to use a custom ` [...]
+ "val(exp)": { "index": 150, "kind": "function", "displayName": "Value", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the expression as a constant value", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": true, "description": "The constant value expression" } ], "examples": [ "${val(42)} -> 42" [...]
+ "variable.name": { "index": 151, "kind": "function", "displayName": "Variable", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The variable with the given name", "ognl": true, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The variable name" } ], "examples": [ "${variable.myVar} -> variable v [...]
+ "variableAs(key,type)": { "index": 152, "kind": "function", "displayName": "Variable As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaType": "String", "required": true, "description": "The variable name" }, { "name": "type", [...]
+ "variables": { "index": 153, "kind": "function", "displayName": "Variables", "group": "core", "label": "core", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}", "examples": [ "${variables} -> {myVar=value1, count=5}" ] },
+ "xpath(input,exp)": { "index": 154, "kind": "function", "displayName": "XPath", "group": "xml", "label": "xml", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optional), you ca [...]
},
"operators": {
"==": { "index": 0, "kind": "operator", "displayName": "Eq", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests equality between left and right operand values. Camel will coerce the right operand type to match the left.", "operatorKind": "binary", "operatorSyntax": "LHS == RHS", "precedence": 10, "examples": [ "${header.foo} == 'bar'", "${header.count} == 5" ] },
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
index d6fc0c2c1c4c..6a67d3001303 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
@@ -20526,22 +20526,62 @@
"semantic" : {
"isListItem" : true,
"children" : [ {
+ "name" : "expert",
+ "type" : "string"
+ }, {
"name" : "question",
"type" : "map",
- "ref" : "semanticDefinitionDeserializer$QuestionSchema",
+ "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
+ "required" : true
+ }, {
+ "name" : "state",
+ "type" : "string"
+ }, {
+ "name" : "evaluation",
+ "type" : "map",
+ "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
+ "required" : true
+ } ]
+ },
+ "semanticDefinitionDeserializer$BlockSchema" : {
+ "children" : [ {
+ "name" : "expert",
+ "type" : "string"
+ }, {
+ "name" : "state",
+ "type" : "string"
+ } ]
+ },
+ "semanticDefinitionDeserializer$EvaluationBlockSchema" : {
+ "children" : [ {
+ "name" : "evaluation",
+ "type" : "map",
+ "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
"required" : true
+ }, {
+ "name" : "expert",
+ "type" : "string"
+ }, {
+ "name" : "state",
+ "type" : "string"
} ]
},
- "semanticDefinitionDeserializer$BooleanSchema" : {
+ "semanticDefinitionDeserializer$EvaluationSchema" : {
"children" : [ {
"name" : "criteria",
- "type" : "object"
+ "type" : "any"
}, {
"name" : "expert",
"type" : "string"
}, {
"name" : "instructions",
"type" : "string"
+ }, {
+ "name" : "operation",
+ "type" : "string"
+ }, {
+ "name" : "parameters",
+ "type" : "object"
}, {
"name" : "state",
"type" : "string"
@@ -20550,22 +20590,19 @@
"type" : "number"
}, {
"name" : "type",
- "type" : "enum",
- "enum" : [ "boolean" ],
- "required" : true
+ "type" : "string"
}, {
"name" : "uncertainty",
"type" : "number"
}, {
"name" : "uncertaintyPolicy",
- "type" : "enum",
- "enum" : [ "fail", "non-match" ]
+ "type" : "string"
} ]
},
- "semanticDefinitionDeserializer$ChoiceSchema" : {
+ "semanticDefinitionDeserializer$ListCriteriaSchema" : {
"children" : [ {
"name" : "criteria",
- "type" : "object",
+ "type" : "array",
"required" : true
}, {
"name" : "expert",
@@ -20573,26 +20610,45 @@
}, {
"name" : "instructions",
"type" : "string"
+ }, {
+ "name" : "operation",
+ "type" : "string"
+ }, {
+ "name" : "parameters",
+ "type" : "object"
}, {
"name" : "state",
"type" : "string"
+ }, {
+ "name" : "threshold",
+ "type" : "number"
}, {
"name" : "type",
- "type" : "enum",
- "enum" : [ "choice" ],
- "required" : true
+ "type" : "string"
+ }, {
+ "name" : "uncertainty",
+ "type" : "number"
+ }, {
+ "name" : "uncertaintyPolicy",
+ "type" : "string"
} ]
},
- "semanticDefinitionDeserializer$QuestionSchema" : {
+ "semanticDefinitionDeserializer$MapCriteriaSchema" : {
"children" : [ {
"name" : "criteria",
- "type" : "any"
+ "type" : "object"
}, {
"name" : "expert",
"type" : "string"
}, {
"name" : "instructions",
"type" : "string"
+ }, {
+ "name" : "operation",
+ "type" : "string"
+ }, {
+ "name" : "parameters",
+ "type" : "object"
}, {
"name" : "state",
"type" : "string"
@@ -20601,37 +20657,27 @@
"type" : "number"
}, {
"name" : "type",
- "type" : "enum",
- "enum" : [ "boolean", "choice", "score" ],
- "required" : true
+ "type" : "string"
}, {
"name" : "uncertainty",
"type" : "number"
}, {
"name" : "uncertaintyPolicy",
- "type" : "enum",
- "enum" : [ "fail", "non-match" ]
+ "type" : "string"
} ]
},
- "semanticDefinitionDeserializer$ScoreSchema" : {
+ "semanticDefinitionDeserializer$QuestionBlockSchema" : {
"children" : [ {
- "name" : "criteria",
- "type" : "array",
- "required" : true
- }, {
"name" : "expert",
"type" : "string"
}, {
- "name" : "instructions",
- "type" : "string"
+ "name" : "question",
+ "type" : "map",
+ "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
+ "required" : true
}, {
"name" : "state",
"type" : "string"
- }, {
- "name" : "type",
- "type" : "enum",
- "enum" : [ "score" ],
- "required" : true
} ]
}
}
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js
index 4ee093a5f0f8..ad95f2987cd5 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js
@@ -88,6 +88,7 @@ const FUNCTIONS = {
'isEmpty': true,
'isNumeric': true,
'jq': true,
+ 'semantic': true,
'jsonpath': true,
'join': true,
'length': true,
diff --git a/components/camel-ai/camel-semantic/pom.xml b/components/camel-ai/camel-semantic/pom.xml
index adb89ba78764..67549b19c64b 100644
--- a/components/camel-ai/camel-semantic/pom.xml
+++ b/components/camel-ai/camel-semantic/pom.xml
@@ -27,7 +27,7 @@
</parent>
<artifactId>camel-semantic</artifactId>
<name>Camel :: AI :: Semantic Evaluation</name>
- <description>Evaluate named questions about message content to produce boolean decisions, categories and scores through provider adapters</description>
+ <description>Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters</description>
<dependencies>
<dependency>
<groupId>org.apache.camel</groupId>
diff --git a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json
index 4e05df3493c2..8d6da5d4eefc 100644
--- a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json
+++ b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json
@@ -3,7 +3,7 @@
"kind": "language",
"name": "semantic",
"title": "Semantic Evaluation",
- "description": "Evaluate named questions about message content to produce boolean decisions, categories and scores through provider adapters",
+ "description": "Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters",
"deprecated": false,
"firstVersion": "4.23.0",
"label": "language,ai",
diff --git a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties
index 78cea1e65e68..09dbb24f7d38 100644
--- a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties
+++ b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties
@@ -4,4 +4,4 @@ groupId=org.apache.camel
artifactId=camel-semantic
version=4.23.0-SNAPSHOT
projectName=Camel :: AI :: Semantic Evaluation
-projectDescription=Evaluate named questions about message content to produce boolean decisions, categories and scores through provider adapters
+projectDescription=Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters
diff --git a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
index 2dcf6a92fd57..0598a8714749 100644
--- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
+++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
@@ -2,16 +2,17 @@
:doctitle: Semantic Evaluation
:shortname: semantic
:artifactid: camel-semantic
-:description: Evaluate named questions about message content to produce boolean decisions, categories and scores through provider adapters
+:description: Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters
:since: 4.23
:supportlevel: Preview
:tabs-sync-option:
*Since Camel {since}*
-The Semantic language evaluates named questions through a provider-independent adapter.
-Boolean questions produce decisions, choice questions produce category strings, and score
-questions produce numbers on an ordered rubric. Calls are synchronous and may block while
+The Semantic language invokes named evaluations through provider-independent experts.
+Each expert owns its operations, accepted inputs, evaluation parameters and result meaning.
+Boolean evaluations produce decisions, choice evaluations produce category strings, score
+evaluations produce numbers, and classification evaluations produce sets of labels. Calls are synchronous and may block while
inference runs. Provider errors propagate through normal Camel error handling.
== Dependencies and providers
@@ -48,50 +49,185 @@ The catalog describes the generic `language` expression model; it does not provi
semantic Spring Boot configuration class. Configure the language bean explicitly in Spring Boot
until generic-language starter configuration is available.
-== Fixed and instruction-driven experts
+== Expert-owned evaluations
-Instructions are optional in the common declaration model. The selected expert decides whether
-they are required, optional or unsupported. TypeSafe AI still requires nonblank instructions.
-A fixed detector can reject instructions and caller-defined criteria because its model already
-defines the question it answers. Unsupported result types, instructions, criteria and decision
-policies fail during initialization, before inference. Replacements for declarations already referenced
-by initialized expressions are validated before publication; failed replacements retain the previous definitions.
+An expert is a registry bean implementing `SemanticAdapter`, annotated with `@SemanticExpert`.
+The class annotation defines its operations. Each operation declares its accepted input shapes,
+parameters, result type and meaning, and any optional probabilities or confidence. A configured
+instance cannot redefine this contract. Endpoint addresses, credentials and model deployment
+settings remain bean configuration, separate from evaluation parameters.
-For example, given registered instances `security` (a fixed injection detector) and `general`
-(an instruction-driven expert):
+Register the beans with Camel Main or Camel JBang, for example:
+
+[source,properties]
+----
+camel.beans.security=#class:com.example.SecurityExpert
+camel.beans.decisions=#class:org.apache.camel.component.typesafeai.TypeSafeAiSemanticAdapter
+----
+
+`SecurityExpert` below is an illustrative application-provided expert exposing `injection` and
+`classify`; these names and its taxonomy are defined by that expert. The instruction-driven
+`decisions` bean uses the TypeSafe AI component configuration.
[source,yaml]
----
- semantic:
- question:
+ expert: security
+ state: "${body}"
+ evaluation:
injection:
- expert: security
- type: boolean
- state: "${body}"
- threshold: "{{security.injection.threshold}}"
- uncertainty: "{{security.injection.uncertainty}}"
- uncertaintyPolicy: fail
+ operation: injection
+ categories:
+ operation: classify
department:
- expert: general
+ expert: decisions
type: choice
- instructions: Which department should handle this message?
- criteria:
- billing: Invoices, payments and refunds
- technical: Bugs, outages and technical problems
+ parameters:
+ instructions: Which department should handle this message?
+ criteria:
+ billing: Payments, invoices and refunds
+ technical: Errors, outages and technical questions
+ sales: Product and purchasing questions
----
-Use `ref:injection` and `ref:department` in ordinary semantic expressions. Expert names can contain
-property placeholders, resolved during runtime selection. Java uses `.expert("security")`;
-XML uses `<question name="injection" type="boolean" expert="security"/>`. An omitted instruction
-stays absent in all three DSLs; an explicitly blank instruction is invalid.
+The block supplies default `expert` and `state` values; individual evaluations can override them.
+Evaluation names belong to the application, while operation names belong to the expert.
+`type: choice` is shorthand for the expert operation named `choice`; it does not let the route
+change an operation's result type. Specify exactly one of `operation` and `type`.
+Declaring an evaluation does not execute it.
+
+`parameters` can contain strings, booleans, numbers, nested maps and lists. Camel copies them
+into immutable declarations. Java, YAML and XML builders resolve property placeholders in string
+parameter values, including nested list and map values; map keys stay literal. Resolved strings
+remain strings: generic numeric parameters require numbers, while the `threshold` and `uncertainty`
+shorthand options also accept numeric placeholders. Direct `SemanticQuestion` construction takes
+already resolved parameter values and requires no Camel context. Omitted parameters stay absent: their behaviour belongs to the
+expert, and Camel does not insert threshold or confidence defaults. Parameter contracts use
+`String`, `Boolean`, `Number`, `Map` and `List` data shapes. Use `Number` with `integer = true`
+for whole-number parameters; do not use JVM numeric wrapper classes such as `Integer` in a
+contract. A provider consumes numeric values through `Number` across all DSLs.
+
+Camel validates recognised parameter names, required values, types and declared constraints.
+The expert checks operation-specific combinations and compatibility with its bean configuration.
+Startup validates all declarations, including unused evaluations, without inference. Route
+resource replacements also validate before publication, retaining previous declarations on failure.
+The low-level `SemanticQuestions.replace(String, Map)` API also validates new and changed
+unused declarations before publishing them on a started context.
+
+=== Calling an evaluation
+
+Use the Simple function `${semantic('evaluationName')}` or a semantic-language expression
+`ref:evaluationName`. Both return the operation's typed value, preserve the body, and publish the
+detailed `SemanticResult` in the `CamelSemanticResult` exchange property. Every occurrence invokes
+the expert; there is no implicit result caching. Retain a value in a Camel variable when it must
+be reused without another call.
+
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:incoming
+ steps:
+ - choice:
+ when:
+ - simple: "${semantic('injection')}"
+ steps:
+ - to: direct:security-review
+ otherwise:
+ steps:
+ - switch:
+ selector:
+ simple: "${semantic('department')}"
+ case:
+ - value: billing
+ uri: direct:billing
+ - value: technical
+ uri: direct:technical
+ - value: sales
+ uri: direct:sales
+ otherwise:
+ uri: direct:manual-review
+----
+
+Switch consumes scalar results. Classification returns a `Set<String>`, including an empty set
+when no labels apply. Use collections in predicates, for example
+`${semantic('categories')} contains 'privacy'`; labels are not numeric scores.
+The category and the meaning of a Boolean verdict must come from the selected expert's contract.
+A negative detection does not establish general safety or grant authorization.
+
+=== Structured input and specialised experts
+
+A specialised moderation expert can accept a map containing a user prompt, an assistant response,
+and supporting documents. Camel evaluates the state selector and passes that same object unchanged.
+It does not flatten a conversation into text, create prompts, or impose a universal conversation
+schema. The expert validates the content and must not modify the original state.
-The selected text is data. A fixed injection detector must not prepend a synthetic instruction.
-Its boolean `probability` is the score for the positive class (INJECTION), even when BENIGN wins.
-Threshold and uncertainty policies produce the decision. A negative result means this evaluation
-did not detect injection; it does not establish general safety or grant authorization.
-Probability is distinct from a rubric SCORE and from optional provider confidence. Missing optional
-metadata remains absent, and invalid output or provider failure remains an error. Continue,
-quarantine and review are application decisions expressed using Camel EIPs.
+For an application-provided `moderation` expert with operations `response_safety` and `categories`:
+
+[source,yaml]
+----
+- semantic:
+ expert: moderation
+ state: "${body}"
+ evaluation:
+ responseSafety:
+ operation: response_safety
+ safetyCategories:
+ operation: categories
+ parameters:
+ policy:
+ labels: [privacy, unsafe]
+ threshold: 0.8
+----
+
+Here `policy` is an illustrative nested expert parameter. The expert owns both its interpretation
+and any threshold filtering. The body might be a map with `prompt`, `response` and `documents` keys.
+A missing required response fails before inference; malformed provider output and operational
+failures follow normal Camel error handling, rather than becoming benign decisions.
+
+=== Equivalent Java and XML declarations
+
+Java uses the same declaration model and validation path:
+
+[source,java]
+----
+import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+
+semanticQuestions(this).expert("moderation").state("${body}")
+ .evaluation("responseSafety").operation("response_safety").end()
+ .evaluation("safetyCategories").operation("categories")
+ .parameter("policy", Map.of("labels", List.of("privacy", "unsafe"), "threshold", 0.8))
+ .register();
+----
+
+XML uses explicit typed values. A parameter and a map entry each contain exactly one value:
+`string`, `number`, `boolean`, `null`, `list` or `map`. Null is supported inside nested maps and
+lists when permitted by the expert. Top-level parameters must have their declared type; omit a
+parameter to request the expert's omission behaviour.
+
+[source,xml]
+----
+<semantic xmlns="http://camel.apache.org/schema/semantic" expert="moderation" state="${body}">
+ <evaluation name="responseSafety" operation="response_safety"/>
+ <evaluation name="safetyCategories" operation="categories">
+ <parameters>
+ <parameter name="policy">
+ <map>
+ <entry key="labels"><list><string>privacy</string><string>unsafe</string></list></entry>
+ <entry key="threshold"><number>0.8</number></entry>
+ </map>
+ </parameter>
+ </parameters>
+ </evaluation>
+</semantic>
+----
+
+== Instruction-driven shorthand
+
+The `question` syntax and Java convenience methods below target instruction-driven operations
+named `boolean`, `choice` and `score`. Their fields become expert parameters and are validated by
+the selected expert. TypeSafe AI implements these operations and requires nonblank instructions.
+Specialised operations normally use `evaluation` and `parameters` as shown above.
== Named questions
@@ -158,8 +294,9 @@ Existing expressions resolve the current definition on their next evaluation.
Reload validates the candidate declaration snapshot for initialized expressions before publishing
it, including expert capabilities, boolean predicate types, Simple state selectors, and compatible
batch selectors across resources. Failed validation leaves the previous declarations available.
-Unused declarations are validated when first referenced, as at startup. Removed declarations
-remain removable; an expression that still references a removed name fails when evaluated.
+Startup validates unused declarations as well. Resource reload validates new and changed declarations
+before publishing them, including unused evaluations. Removed declarations remain removable; an
+expression that still references a removed name fails when evaluated.
Validation registrations are weakly held: the registry does not keep expressions or their providers
alive. Constraints can remain until an unused expression is garbage-collected; stopping a route
does not necessarily release an expression that is still referenced elsewhere.
@@ -172,18 +309,11 @@ question declarations so the expressions compile against the replacement bean.
=== YAML reload and expert bean preparation
-YAML declarations are registered during the loader's existing pre-parse phase, before beans in
-the resource set are prepared. On initial loading, expert validation waits until expressions
-initialize, after bean preparation. On reload, replacements for initialized expressions are
-validated during pre-parsing, so the selected expert must already be registered.
-
-For example, changing an existing question to `expert: newSecurity` and introducing a
-`newSecurity` bean in the same reload fails if that bean is not yet in the registry. Keep expert
-beans registered outside the reloaded resource, prepare the new bean before reloading the
-declarations, or restart the application when changing both together. XML declaration loading
-runs after resource-set bean preparation; Java helpers validate when `register()` is called.
-The three DSLs share declaration fields and validation rules, but this YAML bean-order constraint
-remains because the existing resolver hook runs before bean preparation.
+YAML declarations are registered after all resources in the load operation have prepared their
+beans and before routes are constructed. This applies both at startup and on reload, so an expert
+bean and its evaluations can be introduced together, including in separate resources of the same
+load operation. XML declaration loading follows the same preparation order; Java helpers validate
+when `register()` is called.
=== Java declarations
@@ -341,12 +471,16 @@ in semantic evaluation, and Java applications using `camel-semantic` do not pull
== Results and policy
Only boolean questions can be predicates. A category string is never implicitly a boolean.
-A probability-based boolean uses an inclusive threshold (default `0.5`). A nonzero
+A direct `semantic` predicate rejects other result types at initialization. Simple's bare or negated
+`${semantic('name')}` predicate rejects them before inference when it is matched; comparisons and
+membership tests can consume their typed values.
+Decision policy is expert-owned. Camel never derives a Boolean from probability or applies a
+second threshold to the expert's returned value. The TypeSafe AI adapter uses an inclusive
+Boolean probability threshold (default `0.5`). A nonzero
`uncertainty` defines an inclusive band around that threshold. `fail` (the default) raises
-an error within the band; `non-match` returns false. An already-boolean provider supports the
-default policy without inventing a probability. Additional policy requirements must be
-supported by the provider. Choice results must name a declared criterion. Scores range from
-zero to the last rubric index and may be fractional.
+an error within the band; `non-match` returns false. Its Choice results must name a supplied
+criterion, and its scores range from zero to the last rubric index and may be fractional.
+Other experts declare their own policy parameters, labels and score ranges in their contracts.
Probabilities, provider confidence and selected values are separate. Missing optional fields
remain absent. Results can retain provider/model identity and usage metadata. These fields
@@ -407,7 +541,7 @@ For example, replace the route above with:
A result might contain `actionable -> true`, `department -> "billing"` and `urgency -> 1.2`.
Reading the stored map does not invoke the provider again. Each question retains its own
-threshold and uncertainty policy. There is no implicit AND/OR: batch expressions, including
+expert-owned threshold and uncertainty policy. There is no implicit AND/OR: batch expressions, including
a batch containing one boolean question, cannot be used as predicates.
`CamelSemanticResults` holds an immutable map of the detailed `SemanticResult` objects,
@@ -436,7 +570,7 @@ from resource reload. Questions that need a previous answer require separate eva
`SemanticAdapter.evaluateBatch(Map<String, SemanticQuestion>, Object)` has a default
implementation that invokes the existing single-question method sequentially, preserving
-compatibility with existing adapters. Questions are grouped by the resolved expert instance,
+experts that do not implement a provider-specific batch optimisation. Questions are grouped by the resolved expert instance,
and each group is passed to that instance's batch method. Two instances of the same class remain
separate groups. Each group must return exactly its assigned names. Camel validates the complete
combined result before publishing decisions or diagnostics, preserving the original reference order.
@@ -645,35 +779,52 @@ the action. The example's threshold is a decision policy, not an accuracy guaran
This pattern uses Validate and existing authorization services. It does not add a semantic
`AuthorizationPolicy` implementation or replace a component's internal guardrail interfaces.
-== Expert capabilities
+== Static operation contracts
-`SemanticAdapter.capabilities()` describes accepted text or structured input, supported result
-types, instruction and criteria support, optional probability/confidence semantics, and known limits.
-The default reads `@SemanticExpert` from the provider class. Unannotated legacy adapters report
-`known=false`, with no claimed capabilities. The language still requires instructions for these
-legacy adapters before calling their existing `validate` method. Providers that accept absent
-instructions must explicitly advertise optional or unsupported instructions.
-An adapter can override capabilities for its configured model using the named builder:
+Read `SemanticCapabilities.from(MyExpert.class)` to inspect a contract without constructing an
+expert, loading a model or contacting a service. The runtime uses the same cached class contract.
+The returned operation
+and parameter maps, supported labels and input shapes are immutable.
+
+For example, an expert can declare a fixed detection operation:
[source,java]
----
-@Override
-public SemanticCapabilities capabilities() {
- return SemanticAdapter.super.capabilities().toBuilder()
- .maxChoices(32)
- .build();
+@SemanticExpert(name = "detector", description = "Text injection detector",
+ provider = "example", artifactId = "example-expert",
+ operations = @SemanticOperation(name = "injection", description = "Detect injection",
+ inputTypes = SemanticExpert.InputType.TEXT,
+ inputRequirements = "Text to assess",
+ resultType = SemanticExpert.ResultType.BOOLEAN,
+ resultMeaning = "True means injection was detected",
+ probability = true, probabilityMeaning = "Probability of injection",
+ parameters = @SemanticParameter(name = "threshold", description = "Detection threshold",
+ type = Number.class, minimum = 0, maximum = 1,
+ omission = "Use the service's configured threshold")))
+public class SecurityExpert implements SemanticAdapter {
+ // validate, validateInput and evaluate implement this declared contract.
}
----
-This example narrows an annotated adapter's limits. `SemanticCapabilities.builder()` creates an
-explicit descriptor from scratch; input and result sets are empty until supplied. Descriptors
-are immutable. Capability reporting and declaration validation must not perform inference or
-load models. Runtime input and response validation still apply.
-
-Providers declare static capabilities with `org.apache.camel.semantic.SemanticExpert` from
-`camel-semantic`. The annotation describes the implementation; it neither selects an expert nor
-describes application bean names. Runtime capabilities and `validate` remain authoritative for
-configured instances. No dedicated expert catalog model or discovery API is provided.
+Each operation declares a result type and its meaning. Score operations document their scale and
+can declare numeric bounds. Choice and classification operations can declare a fixed label set;
+an instruction-driven expert validates its application-supplied vocabulary itself. Optional
+probability, per-label probability and confidence fields must declare their meaning. Camel does
+not manufacture confidence from a verdict, nor require optional information on every response.
+It rejects information the contract did not declare, malformed numeric values, wrong result types
+and results outside declared common constraints.
+
+An expert applies explicitly requested policy exactly once, either in its service or its adapter.
+It preserves service defaults when parameters are absent. If requested policy needs unavailable
+confidence or probability, the expert rejects the declaration when known statically, or fails that
+evaluation when the absence is response-dependent. It must not silently ignore the request.
+Uncertain outcomes use an operation's documented values or an error; Camel has no universal
+uncertainty sentinel. Classification filtering, including per-label thresholds, also belongs to
+the expert. TypeSafe AI's `uncertaintyPolicy` supports `fail` or `non-match`; other experts need not
+support that vocabulary.
+
+Generated expert descriptors, expert-specific offline validation and Catalog API extensions are
+separate work. This contract does not add a new Camel Catalog model.
== Implementing an adapter
@@ -691,8 +842,11 @@ Discovery checks the declarations for ambiguity before resolving the implementat
explicit selection. Only the selected adapter is constructed, through Camel's `Injector`.
Explicit bean or class selection also works when packaging does not expose discovery resources.
-The language checks known capabilities and then calls `validate` for model-specific restrictions
-before traffic. Providers should also validate when their SPI is called directly. `evaluate`
+The language checks the static operation contract and then calls `validate` for operation-specific
+parameter combinations and bean configuration before traffic. `validateInput` checks detailed
+message requirements before evaluation. All input checks in a batch run before any expert is invoked.
+`evaluate` returns a typed `SemanticResult` after applying the expert's policy. A probability-only
+Boolean result is invalid: the expert must supply its Boolean verdict. Providers should also validate when their SPI is called directly. `evaluate`
must be thread-safe, bound time and resource consumption, honor interruption and propagate
operational failures without disclosing input or credentials. Implement `CamelContextAware`
for context injection and Camel `Service` interfaces when the adapter owns resources.
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
index 4d477199e87d..7e932683a409 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
@@ -32,6 +32,7 @@ import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Consumer;
+import java.util.function.Supplier;
import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
@@ -41,6 +42,8 @@ import org.apache.camel.Predicate;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.semantic.SemanticAdapter;
import org.apache.camel.semantic.SemanticCapabilities;
+import org.apache.camel.semantic.SemanticCapabilities.Operation;
+import org.apache.camel.semantic.SemanticExpert.ResultType;
import org.apache.camel.semantic.SemanticQuestion;
import org.apache.camel.semantic.SemanticQuestions;
import org.apache.camel.semantic.SemanticResult;
@@ -56,7 +59,7 @@ import org.apache.camel.util.IOHelper;
/** Evaluates a named, provider-independent question against selected message state. */
@Language(value = "semantic", modelName = "language")
@Metadata(title = "Semantic Evaluation",
- description = "Evaluate named questions about message content to produce boolean decisions, categories and scores through provider adapters",
+ description = "Evaluate named questions about message content to produce boolean decisions, categories, scores and label sets through provider adapters",
label = "language,ai", firstVersion = "4.23.0")
public class SemanticLanguage extends LanguageSupport {
public static final String RESULT = "CamelSemanticResult";
@@ -65,6 +68,7 @@ public class SemanticLanguage extends LanguageSupport {
public static final String ADAPTER_FACTORY = "semantic-adapter";
public static final String ADAPTER_RESOURCE = FactoryFinder.DEFAULT_PATH + ADAPTER_FACTORY;
+ private final ThreadLocal<Boolean> validating = new ThreadLocal<>();
private String adapter;
private String defaultExpert;
private String defaultState = "${body}";
@@ -221,6 +225,9 @@ public class SemanticLanguage extends LanguageSupport {
}
private synchronized void startAdapter(List<Group> groups) {
+ if (Boolean.TRUE.equals(validating.get())) {
+ return;
+ }
ManagedAdapter owned = managedAdapter;
if (owned == null || groups.stream().noneMatch(group -> group.expert.provider == owned.instance)) {
return;
@@ -290,6 +297,37 @@ public class SemanticLanguage extends LanguageSupport {
return resolved;
}
+ /** Validate declarations, including unused evaluations, without starting managed expert resources. */
+ public void validateDeclarations(Map<String, SemanticQuestion> declarations) {
+ validationOnly(() -> {
+ declarations.forEach((name, question) -> {
+ expert(name, question);
+ String selector = question.getState() != null ? question.getState() : defaultState;
+ if (selector == null || selector.isBlank()) {
+ throw new IllegalArgumentException("Semantic evaluation '" + name + "': state must not be blank");
+ }
+ Expression state = getCamelContext().resolveLanguage("simple")
+ .createExpression(getCamelContext().resolvePropertyPlaceholders(selector));
+ state.init(getCamelContext());
+ });
+ return null;
+ });
+ }
+
+ private <T> T validationOnly(Supplier<T> action) {
+ Boolean previous = validating.get();
+ validating.set(true);
+ try {
+ return action.get();
+ } finally {
+ if (previous == null) {
+ validating.remove();
+ } else {
+ validating.set(previous);
+ }
+ }
+ }
+
private ResolvedExpert expert(String name, SemanticQuestion question) {
String reference = question.getExpert() != null ? question.getExpert() : defaultExpert;
String label = reference != null ? reference : adapter != null ? adapter : "automatic";
@@ -311,7 +349,7 @@ public class SemanticLanguage extends LanguageSupport {
label = selectedAdapterName;
}
}
- SemanticCapabilities capabilities = provider.capabilities();
+ SemanticCapabilities capabilities = SemanticCapabilities.from(provider.getClass());
capabilities.validate(question);
provider.validate(question);
return new ResolvedExpert(provider, capabilities, label);
@@ -409,16 +447,22 @@ public class SemanticLanguage extends LanguageSupport {
questions = SemanticQuestions.get(context);
Map<String, SemanticQuestion> selected = questions.get(names);
compile(selected);
- questions.setValidator(names, validator);
+ if (!Boolean.TRUE.equals(validating.get())) {
+ questions.setValidator(names, validator);
+ }
}
- private void requireBoolean(SemanticQuestion question) {
- if (question.getType() != SemanticQuestion.Type.BOOLEAN) {
+ private void requireBoolean(Operation operation) {
+ if (operation.getResultType() != ResultType.BOOLEAN) {
throw new IllegalArgumentException("Semantic predicate requires a boolean question: " + names.get(0));
}
}
private synchronized Compiled compile(Map<String, SemanticQuestion> selected) {
+ if (Boolean.TRUE.equals(validating.get())) {
+ // Revisit cached nested selectors against the candidate snapshot without publishing compilation state.
+ return prepare(selected);
+ }
if (compiled == null || !compiled.questions.equals(selected)) {
Compiled candidate = prepare(selected);
startAdapter(candidate.groups);
@@ -428,15 +472,22 @@ public class SemanticLanguage extends LanguageSupport {
}
private Compiled prepare(Map<String, SemanticQuestion> selected) {
- if (predicate) {
- requireBoolean(selected.get(names.get(0)));
- }
+ return validationOnly(() -> prepareDeclarations(selected));
+ }
+
+ private Compiled prepareDeclarations(Map<String, SemanticQuestion> selected) {
+ Map<String, Operation> operations = new LinkedHashMap<>();
String selector = null;
Map<SemanticAdapter, Group> byInstance = new IdentityHashMap<>();
List<Group> groups = new ArrayList<>();
for (var entry : selected.entrySet()) {
SemanticQuestion question = entry.getValue();
ResolvedExpert resolved = expert(entry.getKey(), question);
+ Operation operation = resolved.capabilities.operation(question.getOperation());
+ operations.put(entry.getKey(), operation);
+ if (predicate) {
+ requireBoolean(operation);
+ }
Group group = byInstance.get(resolved.provider);
if (group == null) {
group = new Group(resolved, new LinkedHashMap<>());
@@ -459,7 +510,7 @@ public class SemanticLanguage extends LanguageSupport {
Expression state = getCamelContext().resolveLanguage("simple").createExpression(selector);
state.init(getCamelContext());
return new Compiled(
- selected, state, groups.stream()
+ selected, Map.copyOf(operations), state, groups.stream()
.map(group -> new Group(group.expert, Collections.unmodifiableMap(group.questions))).toList());
}
@@ -477,7 +528,6 @@ public class SemanticLanguage extends LanguageSupport {
throw new IllegalArgumentException("Semantic batch expressions cannot be predicates");
}
single = questions.get(names.get(0));
- requireBoolean(single);
}
try {
Compiled current = compiled;
@@ -495,7 +545,14 @@ public class SemanticLanguage extends LanguageSupport {
current = compile(Map.of(name, single));
}
}
+ if (asPredicate) {
+ requireBoolean(current.operations.get(names.get(0)));
+ }
+ startAdapter(current.groups);
Object state = current.state.evaluate(exchange, Object.class);
+ // A state selector can itself evaluate another semantic expression.
+ exchange.removeProperty(RESULT);
+ exchange.removeProperty(RESULTS);
if (state == null) {
throw new IllegalArgumentException("Missing selected state for semantic questions: " + names);
}
@@ -507,7 +564,10 @@ public class SemanticLanguage extends LanguageSupport {
// Check every group's input before invoking any provider.
for (Group group : current.groups) {
try {
- group.expert.capabilities.validateInput(state);
+ for (var entry : group.questions.entrySet()) {
+ current.operations.get(entry.getKey()).validateInput(state);
+ group.expert.provider.validateInput(entry.getValue(), state);
+ }
} catch (IllegalArgumentException invalid) {
throw new IllegalArgumentException(
"Semantic evaluations " + group.questions.keySet() + ", expert '" + group.expert.reference
@@ -517,6 +577,7 @@ public class SemanticLanguage extends LanguageSupport {
}
if (batch) {
Map<String, SemanticResult> results = new LinkedHashMap<>();
+ Map<String, Object> decisions = new LinkedHashMap<>();
for (Group group : current.groups) {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException("Semantic batch evaluation interrupted");
@@ -525,37 +586,53 @@ public class SemanticLanguage extends LanguageSupport {
if (answers == null || !answers.keySet().equals(group.questions.keySet())) {
throw new IllegalArgumentException("Semantic batch result names must match question names");
}
+ for (String name : group.questions.keySet()) {
+ decisions.put(name,
+ validateResult(name, group.expert, current.operations.get(name), answers.get(name)));
+ }
results.putAll(answers);
}
- Map<String, Object> decisions = new LinkedHashMap<>();
Map<String, SemanticResult> details = new LinkedHashMap<>();
- for (var entry : current.questions.entrySet()) {
- SemanticResult result = results.get(entry.getKey());
- if (result == null) {
- throw new IllegalArgumentException("Missing result for semantic question: " + entry.getKey());
- }
- decisions.put(entry.getKey(), result.decision(entry.getValue()));
- details.put(entry.getKey(), result);
+ Map<String, Object> ordered = new LinkedHashMap<>();
+ for (String name : current.questions.keySet()) {
+ details.put(name, results.get(name));
+ ordered.put(name, decisions.get(name));
}
exchange.setProperty(RESULTS, Collections.unmodifiableMap(details));
- return Collections.unmodifiableMap(decisions);
+ return Collections.unmodifiableMap(ordered);
}
SemanticQuestion question = current.questions.get(names.get(0));
SemanticResult result = current.groups.get(0).expert.provider.evaluate(question, state);
- if (result == null) {
- throw new IllegalArgumentException("Missing result for semantic question: " + names.get(0));
- }
- Object decision = result.decision(question);
+ Object decision = validateResult(names.get(0), current.groups.get(0).expert,
+ current.operations.get(names.get(0)), result);
exchange.setProperty(RESULT, result);
return decision;
} catch (InterruptedException e) {
+ exchange.removeProperty(RESULT);
+ exchange.removeProperty(RESULTS);
Thread.currentThread().interrupt();
throw RuntimeCamelException.wrapRuntimeCamelException(e);
} catch (Exception e) {
+ exchange.removeProperty(RESULT);
+ exchange.removeProperty(RESULTS);
throw RuntimeCamelException.wrapRuntimeCamelException(e);
}
}
+ private Object validateResult(String name, ResolvedExpert expert, Operation operation, SemanticResult result) {
+ try {
+ if (result == null) {
+ throw new IllegalArgumentException("Missing result");
+ }
+ return operation.validateResult(result);
+ } catch (IllegalArgumentException invalid) {
+ throw new IllegalArgumentException(
+ "Semantic evaluation '" + name + "', expert '" + expert.reference
+ + "': " + invalid.getMessage(),
+ invalid);
+ }
+ }
+
@Override
public boolean matches(Exchange exchange) {
return (Boolean) evaluate(exchange, true);
@@ -573,6 +650,7 @@ public class SemanticLanguage extends LanguageSupport {
private record Group(ResolvedExpert expert, Map<String, SemanticQuestion> questions) {
}
- private record Compiled(Map<String, SemanticQuestion> questions, Expression state, List<Group> groups) {
+ private record Compiled(Map<String, SemanticQuestion> questions, Map<String, Operation> operations,
+ Expression state, List<Group> groups) {
}
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAdapter.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAdapter.java
index 767b2c5e727c..b91eb86a6baa 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAdapter.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAdapter.java
@@ -22,27 +22,28 @@ import java.util.Map;
/**
* Provider-independent semantic evaluation. Implementations must support concurrent calls, bound evaluation time and
* resource use, honor interruption, and release outstanding work on shutdown. Exceptions must not expose state or
- * credentials. Validation must not perform inference. Provider configuration belongs to the provider component.
+ * credentials. Validation must not perform inference or contact external services. Provider configuration belongs to
+ * the provider component.
*
* Adapters created by the language receive the Camel context when CamelContextAware and are managed when Service.
* Referenced registry beans retain their existing lifecycle owner.
*/
public interface SemanticAdapter {
- /** Static defaults, or unknown for legacy adapters. This method must not load models or contact services. */
- default SemanticCapabilities capabilities() {
- return SemanticCapabilities.from(getClass().getAnnotation(SemanticExpert.class));
- }
-
/** Reject unsupported question kinds, criteria, decision policies or input selectors before traffic starts. */
void validate(SemanticQuestion question);
+ /** Validate message-dependent requirements before any operation in a batch performs inference. */
+ default void validateInput(SemanticQuestion evaluation, Object state) {
+ }
+
/** Synchronous, potentially blocking evaluation. Operational errors must be thrown, never returned as decisions. */
SemanticResult evaluate(SemanticQuestion question, Object state) throws Exception;
/**
* Evaluate named questions against the same selected state, returning exactly one result per name. The default
* implementation calls the single-question method sequentially; providers may override it to use one request.
- * Operational errors must be thrown, never returned as partial results. The language applies each decision policy.
+ * Operational errors must be thrown, never returned as partial results. The expert applies any requested decision
+ * policy exactly once.
*/
default Map<String, SemanticResult> evaluateBatch(Map<String, SemanticQuestion> questions, Object state) throws Exception {
Map<String, SemanticResult> results = new LinkedHashMap<>();
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java
index 582ae12f7dcb..4c10a75b6a2b 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java
@@ -16,110 +16,58 @@
*/
package org.apache.camel.semantic;
+import java.math.BigDecimal;
import java.util.Collections;
-import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
import org.apache.camel.semantic.SemanticExpert.InputType;
-import org.apache.camel.semantic.SemanticExpert.Instructions;
import org.apache.camel.semantic.SemanticExpert.ResultType;
-/**
- * Immutable capabilities of a configured expert. Unknown legacy capabilities are not a claim of support. Providers may
- * override their static annotation with effective capabilities for the configured model. Probability/confidence flags
- * describe availability, not guarantees that every response includes optional metadata.
- */
+/** Immutable static expert contract. It is read from the class, never supplied by a configured instance. */
public final class SemanticCapabilities {
- private final boolean known;
+ private static final ClassValue<SemanticCapabilities> CONTRACTS = new ClassValue<>() {
+ @Override
+ protected SemanticCapabilities computeValue(Class<?> type) {
+ return from(type.getAnnotation(SemanticExpert.class));
+ }
+ };
private final String name;
private final String description;
private final String provider;
private final String artifactId;
- private final Set<InputType> inputTypes;
- private final Set<ResultType> resultTypes;
- private final Instructions instructions;
- private final boolean callerDefinedCriteria;
- private final boolean booleanProbability;
- private final boolean choiceProbabilities;
- private final Set<ResultType> confidenceTypes;
- private final String probabilityMeaning;
- private final String confidenceMeaning;
- private final String trueMeaning;
- private final int maxChoices;
- private final int maxScoreLevels;
-
- private SemanticCapabilities(boolean known, Builder builder) {
- this.known = known;
- this.name = builder.name;
- this.description = builder.description;
- this.provider = builder.provider;
- this.artifactId = builder.artifactId;
- this.inputTypes = immutableSet(InputType.class, builder.inputTypes);
- this.resultTypes = immutableSet(ResultType.class, builder.resultTypes);
- this.instructions = Objects.requireNonNull(builder.instructions, "Instruction support is required");
- this.callerDefinedCriteria = builder.callerDefinedCriteria;
- this.booleanProbability = builder.booleanProbability;
- this.choiceProbabilities = builder.choiceProbabilities;
- this.confidenceTypes = immutableSet(ResultType.class, builder.confidenceTypes);
- this.probabilityMeaning = builder.probabilityMeaning;
- this.confidenceMeaning = builder.confidenceMeaning;
- this.trueMeaning = builder.trueMeaning;
- this.maxChoices = builder.maxChoices;
- this.maxScoreLevels = builder.maxScoreLevels;
- }
-
- private static <E extends Enum<E>> Set<E> immutableSet(Class<E> type, Set<E> values) {
- EnumSet<E> copy = EnumSet.noneOf(type);
- copy.addAll(values);
- return Collections.unmodifiableSet(copy);
- }
-
- /** Build explicit capabilities using named attributes. Empty input/result sets claim no support. */
- public static Builder builder() {
- return new Builder();
- }
+ private final Map<String, Operation> operations;
- /**
- * Start an explicit capability declaration from this descriptor, for example to narrow a configured model's limits.
- */
- public Builder toBuilder() {
- return new Builder(this);
+ private SemanticCapabilities(SemanticExpert expert) {
+ if (expert == null) {
+ throw new IllegalArgumentException("Semantic adapter requires a @SemanticExpert contract");
+ }
+ name = required(expert.name(), "Expert name");
+ description = required(expert.description(), "Expert description");
+ provider = required(expert.provider(), "Expert provider");
+ artifactId = required(expert.artifactId(), "Expert artifactId");
+ Map<String, Operation> values = new LinkedHashMap<>();
+ for (SemanticOperation operation : expert.operations()) {
+ Operation contract = new Operation(operation);
+ if (values.putIfAbsent(contract.getName(), contract) != null) {
+ throw new IllegalArgumentException("Duplicate semantic operation: " + contract.getName());
+ }
+ }
+ if (values.isEmpty()) {
+ throw new IllegalArgumentException("Semantic expert must declare operations");
+ }
+ operations = Collections.unmodifiableMap(values);
}
- public static SemanticCapabilities unknown() {
- return new SemanticCapabilities(false, new Builder());
+ public static SemanticCapabilities from(Class<?> expertClass) {
+ return CONTRACTS.get(expertClass);
}
- /** Reads static capabilities without loading a model or transport. */
public static SemanticCapabilities from(SemanticExpert expert) {
- if (expert == null) {
- return unknown();
- }
- return builder()
- .name(expert.name())
- .description(expert.description())
- .provider(expert.provider())
- .artifactId(expert.artifactId())
- .inputTypes(expert.inputTypes())
- .resultTypes(expert.resultTypes())
- .instructions(expert.instructions())
- .callerDefinedCriteria(expert.callerDefinedCriteria())
- .booleanProbability(expert.booleanProbability())
- .choiceProbabilities(expert.choiceProbabilities())
- .confidenceTypes(expert.confidenceTypes())
- .probabilityMeaning(expert.probabilityMeaning())
- .confidenceMeaning(expert.confidenceMeaning())
- .trueMeaning(expert.trueMeaning())
- .maxChoices(expert.maxChoices())
- .maxScoreLevels(expert.maxScoreLevels())
- .build();
- }
-
- public boolean isKnown() {
- return known;
+ return new SemanticCapabilities(expert);
}
public String getName() {
@@ -138,221 +86,309 @@ public final class SemanticCapabilities {
return artifactId;
}
- public Set<InputType> getInputTypes() {
- return inputTypes;
- }
-
- public Set<ResultType> getResultTypes() {
- return resultTypes;
+ public Map<String, Operation> getOperations() {
+ return operations;
}
- public Instructions getInstructions() {
- return instructions;
+ public Operation operation(String name) {
+ Operation result = operations.get(name);
+ if (result == null) {
+ throw new IllegalArgumentException(
+ "Unknown operation '" + name + "'; supported operations: " + operations.keySet());
+ }
+ return result;
}
- public boolean isCallerDefinedCriteria() {
- return callerDefinedCriteria;
+ public void validate(SemanticQuestion evaluation) {
+ operation(evaluation.getOperation()).validate(evaluation.getParameters());
}
- public boolean isBooleanProbability() {
- return booleanProbability;
+ private static String required(String value, String field) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(field + " is required");
+ }
+ return value;
}
- public boolean isChoiceProbabilities() {
- return choiceProbabilities;
+ private static boolean withinBounds(Number number, double minimum, double maximum) {
+ if (minimum == Double.POSITIVE_INFINITY || maximum == Double.NEGATIVE_INFINITY) {
+ return false;
+ }
+ try {
+ // Preserve decimal precision, including values just outside a declared boundary.
+ BigDecimal value = new BigDecimal(number.toString());
+ return (minimum == Double.NEGATIVE_INFINITY || value.compareTo(BigDecimal.valueOf(minimum)) >= 0)
+ && (maximum == Double.POSITIVE_INFINITY || value.compareTo(BigDecimal.valueOf(maximum)) <= 0);
+ } catch (NumberFormatException invalid) {
+ return false;
+ }
}
- public Set<ResultType> getConfidenceTypes() {
- return confidenceTypes;
- }
+ /** Operation metadata and common validation, independent of any expert instance. */
+ public static final class Operation {
+ private final SemanticOperation declaration;
+ private final Set<InputType> inputTypes;
+ private final Set<String> labels;
+ private final Map<String, Parameter> parameters;
+
+ private Operation(SemanticOperation declaration) {
+ this.declaration = declaration;
+ required(declaration.name(), "Operation name");
+ required(declaration.description(), "Operation description");
+ required(declaration.inputRequirements(), "Operation input requirements");
+ required(declaration.resultMeaning(), "Operation result meaning");
+ inputTypes = Set.copyOf(List.of(declaration.inputTypes()));
+ labels = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(declaration.labels())));
+ if (inputTypes.isEmpty() || Double.isNaN(declaration.minimum()) || Double.isNaN(declaration.maximum())
+ || declaration.minimum() > declaration.maximum() || labels.stream().anyMatch(String::isBlank)) {
+ throw new IllegalArgumentException("Invalid operation contract: " + declaration.name());
+ }
+ if (declaration.probability() || declaration.probabilities()) {
+ required(declaration.probabilityMeaning(), "Probability meaning");
+ }
+ if (declaration.confidence()) {
+ required(declaration.confidenceMeaning(), "Confidence meaning");
+ }
+ Map<String, Parameter> values = new LinkedHashMap<>();
+ for (SemanticParameter parameter : declaration.parameters()) {
+ Parameter contract = new Parameter(parameter);
+ if (values.putIfAbsent(contract.getName(), contract) != null) {
+ throw new IllegalArgumentException("Duplicate parameter: " + contract.getName());
+ }
+ }
+ parameters = Collections.unmodifiableMap(values);
+ }
- public String getProbabilityMeaning() {
- return probabilityMeaning;
- }
+ public String getName() {
+ return declaration.name();
+ }
- public String getConfidenceMeaning() {
- return confidenceMeaning;
- }
+ public String getDescription() {
+ return declaration.description();
+ }
- public String getTrueMeaning() {
- return trueMeaning;
- }
+ public Set<InputType> getInputTypes() {
+ return inputTypes;
+ }
- public int getMaxChoices() {
- return maxChoices;
- }
+ public String getInputRequirements() {
+ return declaration.inputRequirements();
+ }
- public int getMaxScoreLevels() {
- return maxScoreLevels;
- }
+ public Map<String, Parameter> getParameters() {
+ return parameters;
+ }
- /** Validate the common contract before the provider checks any model-specific restrictions. */
- public void validate(SemanticQuestion question) {
- if ((!known || instructions == Instructions.REQUIRED) && question.getInstructions() == null) {
- throw new IllegalArgumentException("Question instructions are required");
+ public ResultType getResultType() {
+ return declaration.resultType();
}
- if (!known) {
- return;
+
+ public String getResultMeaning() {
+ return declaration.resultMeaning();
}
- if (!resultTypes.contains(ResultType.valueOf(question.getType().name()))) {
- throw new IllegalArgumentException(
- "Requires " + question.getType() + "; supports " + resultTypes + ": " + description);
+
+ public Set<String> getLabels() {
+ return labels;
}
- if (instructions == Instructions.UNSUPPORTED && question.getInstructions() != null) {
- throw new IllegalArgumentException("Instructions are unsupported by this fixed expert");
+
+ public double getMinimum() {
+ return declaration.minimum();
}
- if (!callerDefinedCriteria && (!question.getCriteria().isEmpty() || !question.getLevels().isEmpty())) {
- throw new IllegalArgumentException("Caller-defined criteria and score levels are unsupported by this fixed expert");
+
+ public double getMaximum() {
+ return declaration.maximum();
}
- if (question.getType() == SemanticQuestion.Type.CHOICE && maxChoices > 0
- && question.getCriteria().size() > maxChoices) {
- throw new IllegalArgumentException("Supports at most " + maxChoices + " choice criteria");
+
+ public boolean isProbability() {
+ return declaration.probability();
}
- if (question.getType() == SemanticQuestion.Type.SCORE && maxScoreLevels > 0
- && question.getLevels().size() > maxScoreLevels) {
- throw new IllegalArgumentException("Supports at most " + maxScoreLevels + " score levels");
+
+ public boolean isProbabilities() {
+ return declaration.probabilities();
}
- if (question.getType() == SemanticQuestion.Type.BOOLEAN && !booleanProbability
- && (question.getThreshold() != 0.5 || question.getUncertainty() != 0
- || question.getUncertaintyPolicy() != SemanticQuestion.UncertaintyPolicy.FAIL)) {
- throw new IllegalArgumentException("Boolean decision policy requires positive-class probability support");
+
+ public String getProbabilityMeaning() {
+ return declaration.probabilityMeaning();
}
- }
- /** Input shape can only be checked once the state selector has been evaluated. */
- public void validateInput(Object state) {
- if (known && !(state instanceof String && inputTypes.contains(InputType.TEXT)
- || (state instanceof Map<?, ?> || state instanceof List<?>) && inputTypes.contains(InputType.STRUCTURED))) {
- throw new IllegalArgumentException("Unsupported selected state; accepts " + inputTypes + " input");
+ public boolean isConfidence() {
+ return declaration.confidence();
}
- }
- /** Named construction of an immutable, explicitly supported contract. */
- public static final class Builder {
- private String name;
- private String description;
- private String provider;
- private String artifactId;
- private Set<InputType> inputTypes = Set.of();
- private Set<ResultType> resultTypes = Set.of();
- private Instructions instructions = Instructions.REQUIRED;
- private boolean callerDefinedCriteria;
- private boolean booleanProbability;
- private boolean choiceProbabilities;
- private Set<ResultType> confidenceTypes = Set.of();
- private String probabilityMeaning;
- private String confidenceMeaning;
- private String trueMeaning;
- private int maxChoices;
- private int maxScoreLevels;
+ public String getConfidenceMeaning() {
+ return declaration.confidenceMeaning();
+ }
- private Builder() {
+ public void validate(Map<String, Object> values) {
+ for (String name : values.keySet()) {
+ if (!parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Unknown parameter '" + name + "' for operation '" + getName() + "'");
+ }
+ }
+ parameters.forEach((name, parameter) -> parameter.validate(values.get(name), values.containsKey(name)));
}
- private Builder(SemanticCapabilities capabilities) {
- this.name = capabilities.name;
- this.description = capabilities.description;
- this.provider = capabilities.provider;
- this.artifactId = capabilities.artifactId;
- this.inputTypes = capabilities.inputTypes;
- this.resultTypes = capabilities.resultTypes;
- this.instructions = capabilities.instructions;
- this.callerDefinedCriteria = capabilities.callerDefinedCriteria;
- this.booleanProbability = capabilities.booleanProbability;
- this.choiceProbabilities = capabilities.choiceProbabilities;
- this.confidenceTypes = capabilities.confidenceTypes;
- this.probabilityMeaning = capabilities.probabilityMeaning;
- this.confidenceMeaning = capabilities.confidenceMeaning;
- this.trueMeaning = capabilities.trueMeaning;
- this.maxChoices = capabilities.maxChoices;
- this.maxScoreLevels = capabilities.maxScoreLevels;
+ public void validateInput(Object state) {
+ if (!(state instanceof String && inputTypes.contains(InputType.TEXT)
+ || (state instanceof Map<?, ?> || state instanceof List<?>) && inputTypes.contains(InputType.STRUCTURED))) {
+ throw new IllegalArgumentException("Unsupported selected state; accepts " + inputTypes + " input");
+ }
}
- public Builder name(String name) {
- this.name = name;
- return this;
+ /** Validate the typed answer; decision policy has already been applied by the expert. */
+ public Object validateResult(SemanticResult result) {
+ if (result == null) {
+ throw new IllegalArgumentException("Missing semantic result");
+ }
+ Object value = result.getValue();
+ boolean valid = switch (getResultType()) {
+ case BOOLEAN -> value instanceof Boolean;
+ case CHOICE -> value instanceof String text && !text.isBlank() && (labels.isEmpty() || labels.contains(text));
+ case SCORE -> value instanceof Number number && withinBounds(number, getMinimum(), getMaximum());
+ case CLASSIFICATION ->
+ value instanceof Set<?> set && set.stream().allMatch(label -> label instanceof String text
+ && !text.isBlank() && (labels.isEmpty() || labels.contains(text)));
+ };
+ if (!valid) {
+ throw new IllegalArgumentException("Semantic result does not match " + getResultType() + " contract");
+ }
+ if (result.getProbability() != null && !isProbability()
+ || !result.getProbabilities().isEmpty() && !isProbabilities()
+ || result.getConfidence() != null && !isConfidence()) {
+ throw new IllegalArgumentException("Semantic result contains undeclared probability or confidence information");
+ }
+ if (!labels.isEmpty() && !labels.containsAll(result.getProbabilities().keySet())) {
+ throw new IllegalArgumentException("Semantic result probabilities contain unknown labels");
+ }
+ return value;
}
+ }
- public Builder description(String description) {
- this.description = description;
- return this;
+ /** Immutable parameter metadata. Values are checked without coercion or inclusion in diagnostics. */
+ public static final class Parameter {
+ private final SemanticParameter declaration;
+ private final Set<String> values;
+
+ private Parameter(SemanticParameter declaration) {
+ this.declaration = declaration;
+ Set<Class<?>> types = Set.of(String.class, Boolean.class, Number.class, Map.class, List.class);
+ if (!types.contains(declaration.type())
+ || declaration.itemType() != Object.class && !types.contains(declaration.itemType())) {
+ throw new IllegalArgumentException(
+ "Parameter types must be String, Boolean, Number, Map or List; use Number for numeric values");
+ }
+ if (declaration.integer() && declaration.type() != Number.class) {
+ throw new IllegalArgumentException("Integer constraint requires a Number parameter");
+ }
+ required(declaration.name(), "Parameter name");
+ required(declaration.description(), "Parameter description");
+ if (!declaration.required()) {
+ required(declaration.omission(), "Parameter omission behaviour");
+ }
+ if (Double.isNaN(declaration.minimum()) || Double.isNaN(declaration.maximum())
+ || declaration.minimum() > declaration.maximum()
+ || declaration.minSize() < 0 || declaration.minSize() > declaration.maxSize()) {
+ throw new IllegalArgumentException("Invalid constraints for parameter '" + declaration.name() + "'");
+ }
+ values = Set.copyOf(List.of(declaration.values()));
+ if (!values.isEmpty() && declaration.type() != String.class) {
+ throw new IllegalArgumentException("Allowed values require a String parameter");
+ }
}
- public Builder provider(String provider) {
- this.provider = provider;
- return this;
+ public String getName() {
+ return declaration.name();
}
- public Builder artifactId(String artifactId) {
- this.artifactId = artifactId;
- return this;
+ public String getDescription() {
+ return declaration.description();
}
- public Builder inputTypes(InputType... inputTypes) {
- this.inputTypes = EnumSet.noneOf(InputType.class);
- Collections.addAll(this.inputTypes, inputTypes);
- return this;
+ public Class<?> getType() {
+ return declaration.type();
}
- public Builder resultTypes(ResultType... resultTypes) {
- this.resultTypes = EnumSet.noneOf(ResultType.class);
- Collections.addAll(this.resultTypes, resultTypes);
- return this;
+ public boolean isInteger() {
+ return declaration.integer();
}
- public Builder instructions(Instructions instructions) {
- this.instructions = instructions;
- return this;
+ public Class<?> getItemType() {
+ return declaration.itemType();
}
- public Builder callerDefinedCriteria(boolean callerDefinedCriteria) {
- this.callerDefinedCriteria = callerDefinedCriteria;
- return this;
+ public boolean isRequired() {
+ return declaration.required();
}
- public Builder booleanProbability(boolean booleanProbability) {
- this.booleanProbability = booleanProbability;
- return this;
+ public String getOmission() {
+ return declaration.omission();
}
- public Builder choiceProbabilities(boolean choiceProbabilities) {
- this.choiceProbabilities = choiceProbabilities;
- return this;
+ public double getMinimum() {
+ return declaration.minimum();
}
- public Builder confidenceTypes(ResultType... confidenceTypes) {
- this.confidenceTypes = EnumSet.noneOf(ResultType.class);
- Collections.addAll(this.confidenceTypes, confidenceTypes);
- return this;
+ public double getMaximum() {
+ return declaration.maximum();
}
- public Builder probabilityMeaning(String probabilityMeaning) {
- this.probabilityMeaning = probabilityMeaning;
- return this;
+ public int getMinSize() {
+ return declaration.minSize();
}
- public Builder confidenceMeaning(String confidenceMeaning) {
- this.confidenceMeaning = confidenceMeaning;
- return this;
+ public int getMaxSize() {
+ return declaration.maxSize();
}
- public Builder trueMeaning(String trueMeaning) {
- this.trueMeaning = trueMeaning;
- return this;
+ public Set<String> getValues() {
+ return values;
}
- public Builder maxChoices(int maxChoices) {
- this.maxChoices = maxChoices;
- return this;
+ private void validate(Object value, boolean present) {
+ if (!present) {
+ if (isRequired()) {
+ throw invalid("is required");
+ }
+ return;
+ }
+ if (value == null) {
+ throw invalid("must not be null; omit it to use the expert's omission behavior");
+ }
+ if (!getType().isInstance(value)) {
+ throw invalid("must be " + getType().getSimpleName());
+ }
+ if (value instanceof Number number && !withinBounds(number, getMinimum(), getMaximum())) {
+ throw invalid("is outside its numeric constraints");
+ }
+ if (isInteger() && new BigDecimal(value.toString()).stripTrailingZeros().scale() > 0) {
+ throw invalid("must be an integer");
+ }
+ int size = -1;
+ if (value instanceof String text) {
+ size = text.strip().length();
+ } else if (value instanceof Map<?, ?> map) {
+ size = map.size();
+ map.values().forEach(this::validateItem);
+ } else if (value instanceof List<?> list) {
+ size = list.size();
+ list.forEach(this::validateItem);
+ }
+ if (size >= 0 && (size < getMinSize() || size > getMaxSize())) {
+ throw invalid("is outside its size constraints");
+ }
+ if (!values.isEmpty() && !values.contains(value)) {
+ throw invalid("is not an allowed value");
+ }
}
- public Builder maxScoreLevels(int maxScoreLevels) {
- this.maxScoreLevels = maxScoreLevels;
- return this;
+ private void validateItem(Object value) {
+ if (getItemType() != Object.class && !getItemType().isInstance(value)) {
+ throw invalid("items must be " + getItemType().getSimpleName());
+ }
}
- public SemanticCapabilities build() {
- return new SemanticCapabilities(true, this);
+ private IllegalArgumentException invalid(String reason) {
+ return new IllegalArgumentException("Parameter '" + getName() + "' " + reason);
}
}
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticExpert.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticExpert.java
index ce77381f8c8a..1afa5a8023c7 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticExpert.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticExpert.java
@@ -24,7 +24,8 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
- * Static semantic expert capabilities, available without constructing providers or loading models.
+ * Static expert contract. Reading it never constructs an expert or contacts a service. Configured instances may reject
+ * incompatible configurations, but cannot redefine these operations or their semantics.
*
* @since 4.23
*/
@@ -41,13 +42,8 @@ public @interface SemanticExpert {
enum ResultType {
BOOLEAN,
CHOICE,
- SCORE
- }
-
- enum Instructions {
- REQUIRED,
- OPTIONAL,
- UNSUPPORTED
+ SCORE,
+ CLASSIFICATION
}
String name();
@@ -58,29 +54,5 @@ public @interface SemanticExpert {
String artifactId();
- InputType[] inputTypes();
-
- ResultType[] resultTypes();
-
- Instructions instructions();
-
- boolean callerDefinedCriteria();
-
- boolean booleanProbability() default false;
-
- boolean choiceProbabilities() default false;
-
- ResultType[] confidenceTypes() default {};
-
- String probabilityMeaning() default "";
-
- String confidenceMeaning() default "";
-
- String trueMeaning() default "";
-
- /** Zero means no statically known limit. */
- int maxChoices() default 0;
-
- /** Zero means no statically known limit. */
- int maxScoreLevels() default 0;
+ SemanticOperation[] operations();
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticOperation.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticOperation.java
new file mode 100644
index 000000000000..2755b485aa8c
--- /dev/null
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticOperation.java
@@ -0,0 +1,74 @@
+/*
+ * 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.semantic;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.camel.semantic.SemanticExpert.InputType;
+import org.apache.camel.semantic.SemanticExpert.ResultType;
+
+/** An expert-owned operation, including the meaning of its input, parameters and result. */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({})
+public @interface SemanticOperation {
+ /** Stable operation name used by declarations. */
+ String name();
+
+ /** Human-readable purpose of the operation. */
+ String description();
+
+ /** Supported shapes of the selected message state. */
+ InputType[] inputTypes();
+
+ /** Required content and structure within the supported input shapes. */
+ String inputRequirements();
+
+ /** Application parameters accepted by this operation. */
+ SemanticParameter[] parameters() default {};
+
+ /** Shape of the decision returned by the expert. */
+ ResultType resultType();
+
+ /** Meaning of the decision, including the positive class for a boolean result. */
+ String resultMeaning();
+
+ /** Fixed allowed categories or labels; empty leaves the vocabulary to the expert. */
+ String[] labels() default {};
+
+ /** Inclusive minimum for a score result. */
+ double minimum() default Double.NEGATIVE_INFINITY;
+
+ /** Inclusive maximum for a score result. */
+ double maximum() default Double.POSITIVE_INFINITY;
+
+ /** Whether results may carry one probability in addition to their decision. */
+ boolean probability() default false;
+
+ /** Whether results may carry probabilities keyed by category or label. */
+ boolean probabilities() default false;
+
+ /** Meaning of the probability or per-label probabilities, when supported. */
+ String probabilityMeaning() default "";
+
+ /** Whether results may carry a separate confidence value. */
+ boolean confidence() default false;
+
+ /** Meaning of the confidence value, when supported. */
+ String confidenceMeaning() default "";
+}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticParameter.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticParameter.java
new file mode 100644
index 000000000000..0c6cdb55e13f
--- /dev/null
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticParameter.java
@@ -0,0 +1,58 @@
+/*
+ * 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.semantic;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A parameter supplied by the application, separate from expert deployment configuration. Map and list values can
+ * contain nested values. Constraints requiring knowledge of their contents are validated by the expert.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({})
+public @interface SemanticParameter {
+ String name();
+
+ String description();
+
+ /** String, Boolean, Number, Map or List. Numeric wrapper classes are not portable across DSLs. */
+ Class<?> type() default String.class;
+
+ /** Require a mathematical integer for a Number parameter, regardless of its DSL representation. */
+ boolean integer() default false;
+
+ /** Type of map values or list elements, if constrained. */
+ Class<?> itemType() default Object.class;
+
+ boolean required() default false;
+
+ /** Documented behaviour when omitted. Camel does not insert a default value. */
+ String omission() default "";
+
+ double minimum() default Double.NEGATIVE_INFINITY;
+
+ double maximum() default Double.POSITIVE_INFINITY;
+
+ int minSize() default 0;
+
+ int maxSize() default Integer.MAX_VALUE;
+
+ /** Allowed values for a String parameter; empty leaves the vocabulary unrestricted. */
+ String[] values() default {};
+}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java
index cd7a4126110e..d1baa30076d5 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java
@@ -16,20 +16,26 @@
*/
package org.apache.camel.semantic;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
-/**
- * Immutable question definition. Probabilities and thresholds are provider-specific evidence, not accuracy guarantees.
- */
+/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
public final class SemanticQuestion {
+ private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, BigInteger.class, BigDecimal.class);
+
public enum Type {
BOOLEAN,
CHOICE,
- SCORE
+ SCORE,
+ CLASSIFICATION
}
public enum UncertaintyPolicy {
@@ -37,97 +43,151 @@ public final class SemanticQuestion {
NON_MATCH
}
- private final Type type;
+ private final String operation;
private final String expert;
- private final String instructions;
private final String state;
- private final Map<String, String> criteria;
- private final List<String> levels;
- private final double threshold;
- private final double uncertainty;
- private final UncertaintyPolicy uncertaintyPolicy;
+ private final Map<String, Object> parameters;
+
+ public SemanticQuestion(String operation, String expert, String state, Map<String, ?> parameters) {
+ if (operation == null || operation.isBlank()) {
+ throw new IllegalArgumentException("Evaluation operation is required");
+ }
+ if (expert != null && expert.isBlank()) {
+ throw new IllegalArgumentException("Evaluation expert must not be blank");
+ }
+ if (state != null && state.isBlank()) {
+ throw new IllegalArgumentException("Evaluation state selector must not be blank");
+ }
+ this.operation = operation;
+ this.expert = expert;
+ this.state = state;
+ this.parameters = immutableMap(parameters == null ? Map.of() : parameters);
+ }
+ /** Convenience declaration for instruction-driven operations named boolean, choice or score. */
public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy uncertaintyPolicy) {
- this(type, instructions, state, criteria, levels, threshold, uncertainty, uncertaintyPolicy, null);
+ List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy) {
+ this(type, instructions, state, criteria, levels, threshold, uncertainty, policy, null);
}
public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy uncertaintyPolicy,
+ List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy,
String expert) {
- this.type = Objects.requireNonNull(type, "Question type is required");
- if (expert != null && expert.isBlank()) {
- throw new IllegalArgumentException("Question expert must not be blank");
+ this(type.name().toLowerCase(Locale.ROOT), expert, state,
+ instructionParameters(type, instructions, criteria, levels, threshold, uncertainty, policy));
+ }
+
+ private static Map<String, Object> instructionParameters(
+ Type type, String instructions, Map<String, String> criteria,
+ List<String> levels, double threshold, double uncertainty,
+ UncertaintyPolicy policy) {
+ Map<String, Object> values = new LinkedHashMap<>();
+ if (instructions != null) {
+ values.put("instructions", instructions);
}
- this.expert = expert;
- if (instructions != null && instructions.isBlank()) {
- throw new IllegalArgumentException("Question instructions must not be blank");
+ if (criteria != null && !criteria.isEmpty()) {
+ values.put("criteria", criteria);
}
- if (state != null && state.isBlank()) {
- throw new IllegalArgumentException("Question state selector must not be blank");
+ if (levels != null && !levels.isEmpty()) {
+ values.put("criteria", levels);
}
- this.instructions = instructions;
- this.state = state;
- this.criteria = criteria == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(criteria));
- this.levels = levels == null ? List.of() : List.copyOf(levels);
- this.criteria.forEach((key, value) -> {
- if (key == null || key.isBlank() || value == null || value.isBlank()) {
- throw new IllegalArgumentException("Question criteria require nonblank keys and descriptions");
+ if (type == Type.BOOLEAN) {
+ values.put("threshold", threshold);
+ values.put("uncertainty", uncertainty);
+ values.put("uncertaintyPolicy", policy == UncertaintyPolicy.NON_MATCH ? "non-match" : "fail");
+ }
+ return values;
+ }
+
+ static Map<String, Object> immutableMap(Map<String, ?> values) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ values.forEach((name, value) -> {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Parameter names must not be blank");
}
+ copy.put(name, immutableValue(value));
});
- if (this.levels.stream().anyMatch(String::isBlank)) {
- throw new IllegalArgumentException("Question score levels must not be blank");
- }
- if (type == Type.CHOICE && this.criteria.isEmpty() || type == Type.SCORE && this.levels.isEmpty()
- || type != Type.SCORE && !this.levels.isEmpty() || type == Type.SCORE && !this.criteria.isEmpty()) {
- throw new IllegalArgumentException(
- "Choice needs criteria, score needs ordered levels, boolean accepts true/false criteria");
+ return Collections.unmodifiableMap(copy);
+ }
+
+ private static Object immutableValue(Object value) {
+ if (value instanceof Map<?, ?> map) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ map.forEach((key, entry) -> {
+ if (!(key instanceof String name)) {
+ throw new IllegalArgumentException("Parameter maps require string keys");
+ }
+ copy.put(name, entry);
+ });
+ return immutableMap(copy);
}
- if (type == Type.BOOLEAN && this.criteria.keySet().stream().anyMatch(k -> !k.equals("true") && !k.equals("false"))) {
- throw new IllegalArgumentException("Boolean criteria keys must be true or false");
+ if (value instanceof List<?> list) {
+ List<Object> copy = new ArrayList<>();
+ list.forEach(entry -> copy.add(immutableValue(entry)));
+ return Collections.unmodifiableList(copy);
}
- if (!Double.isFinite(threshold) || !Double.isFinite(uncertainty) || uncertainty < 0
- || threshold - uncertainty < 0 || threshold + uncertainty > 1) {
- throw new IllegalArgumentException("Boolean threshold and uncertainty band must be within [0,1]");
+ if (value == null || value instanceof String || value instanceof Boolean
+ || NUMBER_TYPES.contains(value.getClass())) {
+ return value;
}
- this.threshold = threshold;
- this.uncertainty = uncertainty;
- this.uncertaintyPolicy = Objects.requireNonNull(uncertaintyPolicy, "Uncertainty policy is required");
+ throw new IllegalArgumentException("Parameters require immutable scalar, map or list values");
+ }
+
+ public String getOperation() {
+ return operation;
}
public String getExpert() {
return expert;
}
- public Type getType() {
- return type;
+ public String getState() {
+ return state;
}
- public String getInstructions() {
- return instructions;
+ public Map<String, Object> getParameters() {
+ return parameters;
}
- public String getState() {
- return state;
+ /**
+ * Instruction-driven operation kind; custom operations have their result type in the expert contract.
+ *
+ * @throws IllegalArgumentException if the operation is not a built-in instruction-driven kind
+ */
+ public Type getType() {
+ return Type.valueOf(operation.toUpperCase(Locale.ROOT));
}
+ public String getInstructions() {
+ return (String) parameters.get("instructions");
+ }
+
+ @SuppressWarnings("unchecked")
public Map<String, String> getCriteria() {
- return criteria;
+ return parameters.get("criteria") instanceof Map<?, ?> map ? (Map<String, String>) map : Map.of();
}
+ @SuppressWarnings("unchecked")
public List<String> getLevels() {
- return levels;
+ return parameters.get("criteria") instanceof List<?> list ? (List<String>) list : List.of();
}
- public double getThreshold() {
- return threshold;
+ /** Requested threshold, or null when omitted. Defaults belong to the expert. */
+ public Double getThreshold() {
+ return parameters.get("threshold") instanceof Number number ? number.doubleValue() : null;
}
- public double getUncertainty() {
- return uncertainty;
+ /** Requested uncertainty band, or null when omitted. Defaults belong to the expert. */
+ public Double getUncertainty() {
+ return parameters.get("uncertainty") instanceof Number number ? number.doubleValue() : null;
}
+ /** Requested instruction-driven policy, or null when omitted. Custom policies remain available in parameters. */
public UncertaintyPolicy getUncertaintyPolicy() {
- return uncertaintyPolicy;
+ Object policy = parameters.get("uncertaintyPolicy");
+ if (policy == null) {
+ return null;
+ }
+ return UncertaintyPolicy.valueOf(policy.toString().toUpperCase(Locale.ROOT).replace('-', '_'));
}
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java
index 164a6ec95d67..dc9db101c94a 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java
@@ -19,57 +19,75 @@ package org.apache.camel.semantic;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import org.apache.camel.CamelContext;
import org.apache.camel.util.StringHelper;
-/** Fluent definition of a named semantic question. */
+/** Fluent definition of an expert-owned evaluation. All DSLs build the same immutable declaration. */
public final class SemanticQuestionBuilder {
private final SemanticQuestionsBuilder parent;
+ private final Map<String, Object> parameters = new LinkedHashMap<>();
private final Map<String, String> criteria = new LinkedHashMap<>();
private final List<String> levels = new ArrayList<>();
- private String type;
- private String expert;
- private String instructions;
- private String state;
private String threshold;
private String uncertainty;
- private String uncertaintyPolicy;
+ private boolean normalizeUncertaintyPolicy;
+ private String operation;
+ private String expert;
+ private String state;
+
+ /** Create a standalone declaration, completed with {@link #build(CamelContext)}. */
+ public SemanticQuestionBuilder() {
+ this(null);
+ }
SemanticQuestionBuilder(SemanticQuestionsBuilder parent) {
this.parent = parent;
}
- /** The question type: boolean, choice or score. */
- public SemanticQuestionBuilder type(String type) {
- this.type = type;
+ public SemanticQuestionBuilder operation(String operation) {
+ this.operation = operation;
return this;
}
- /** Select a configured expert by registry bean name. */
+ /** Select an instruction-driven operation by type, such as boolean, choice or score. */
+ public SemanticQuestionBuilder type(String type) {
+ return operation(type.toLowerCase(Locale.ROOT));
+ }
+
public SemanticQuestionBuilder expert(String expert) {
this.expert = expert;
return this;
}
String getExpert() {
- return expert;
+ return expert != null ? expert : parent != null ? parent.getExpert() : null;
}
- /** The instructions sent to the provider, if supported. */
- public SemanticQuestionBuilder instructions(String instructions) {
- this.instructions = instructions;
+ public SemanticQuestionBuilder state(String state) {
+ this.state = state;
return this;
}
- /** A Simple expression selecting the state to evaluate; defaults to the message body. */
- public SemanticQuestionBuilder state(String state) {
- this.state = state;
+ public SemanticQuestionBuilder parameter(String name, Object value) {
+ if (parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic parameter: " + name);
+ }
+ parameters.put(name, value);
+ return this;
+ }
+
+ public SemanticQuestionBuilder parameters(Map<String, ?> values) {
+ values.forEach(this::parameter);
return this;
}
- /** Add a named choice, or a true/false criterion for a boolean question. */
+ public SemanticQuestionBuilder instructions(String instructions) {
+ return parameter("instructions", instructions);
+ }
+
public SemanticQuestionBuilder criterion(String name, String description) {
if (criteria.containsKey(name)) {
throw new IllegalArgumentException("Duplicate semantic criterion: " + name);
@@ -78,84 +96,89 @@ public final class SemanticQuestionBuilder {
return this;
}
- /** Add an ordered score level. */
public SemanticQuestionBuilder level(String level) {
levels.add(level);
return this;
}
- /** Boolean decision threshold; defaults to 0.5. */
public SemanticQuestionBuilder threshold(double threshold) {
- return threshold(Double.toString(threshold));
+ return parameter("threshold", threshold);
}
- /** Boolean decision threshold, optionally using property placeholders. */
public SemanticQuestionBuilder threshold(String threshold) {
this.threshold = threshold;
return this;
}
- /** Boolean uncertainty band; defaults to zero. */
public SemanticQuestionBuilder uncertainty(double uncertainty) {
- return uncertainty(Double.toString(uncertainty));
+ return parameter("uncertainty", uncertainty);
}
- /** Boolean uncertainty band, optionally using property placeholders. */
public SemanticQuestionBuilder uncertainty(String uncertainty) {
this.uncertainty = uncertainty;
return this;
}
- /** Boolean uncertainty policy: fail (default) or non-match. */
- public SemanticQuestionBuilder uncertaintyPolicy(String uncertaintyPolicy) {
- this.uncertaintyPolicy = uncertaintyPolicy;
+ public SemanticQuestionBuilder uncertaintyPolicy(String policy) {
+ parameter("uncertaintyPolicy", policy);
+ normalizeUncertaintyPolicy = true;
return this;
}
- /** Return to the group to add another question. */
public SemanticQuestionsBuilder end() {
+ if (parent == null) {
+ throw new IllegalStateException("Complete a standalone declaration with build(context)");
+ }
return parent;
}
- /** Validate and register the entire group. */
public void register() {
- parent.register();
+ end().register();
}
- SemanticQuestion build(CamelContext context) {
- if (type == null) {
- throw new IllegalArgumentException("Question type is required");
+ /** Build an immutable declaration, resolving placeholders in parameter values while retaining their types. */
+ public SemanticQuestion build(CamelContext context) {
+ Map<String, Object> values = new LinkedHashMap<>(parameters);
+ if (!criteria.isEmpty() || !levels.isEmpty()) {
+ if (values.containsKey("criteria") || !criteria.isEmpty() && !levels.isEmpty()) {
+ throw new IllegalArgumentException("Duplicate parameter 'criteria'");
+ }
+ values.put("criteria", !criteria.isEmpty() ? criteria : levels);
+ }
+ for (String numeric : List.of("threshold", "uncertainty")) {
+ String text = numeric.equals("threshold") ? threshold : uncertainty;
+ if (text != null) {
+ if (values.containsKey(numeric)) {
+ throw new IllegalArgumentException("Duplicate parameter '" + numeric + "'");
+ }
+ try {
+ values.put(numeric, Double.valueOf(context.resolvePropertyPlaceholders(text)));
+ } catch (NumberFormatException invalid) {
+ throw new IllegalArgumentException("Parameter '" + numeric + "' must be a valid number");
+ }
+ }
}
- SemanticQuestion.Type questionType = enumeration(type, SemanticQuestion.Type.class);
- if (questionType != SemanticQuestion.Type.BOOLEAN
- && (threshold != null || uncertainty != null || uncertaintyPolicy != null)) {
- throw new IllegalArgumentException("Threshold and uncertainty policy require a boolean question");
+ values.replaceAll((name, value) -> resolve(context, value));
+ if (normalizeUncertaintyPolicy && values.get("uncertaintyPolicy") instanceof String policy) {
+ values.put("uncertaintyPolicy",
+ StringHelper.asEnumConstantValue(policy).toLowerCase(Locale.ROOT).replace('_', '-'));
}
return new SemanticQuestion(
- questionType, instructions, state, criteria, levels,
- threshold == null ? 0.5 : parseDouble(context, threshold, "threshold"),
- uncertainty == null ? 0 : parseDouble(context, uncertainty, "uncertainty"),
- uncertaintyPolicy == null
- ? SemanticQuestion.UncertaintyPolicy.FAIL
- : enumeration(uncertaintyPolicy, SemanticQuestion.UncertaintyPolicy.class),
- expert);
- }
-
- private static double parseDouble(CamelContext context, String value, String field) {
- try {
- return Double.parseDouble(context.resolvePropertyPlaceholders(value));
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException(field + " must be a valid number: " + value, e);
- }
+ operation, getExpert(), state != null ? state : parent != null ? parent.getState() : null, values);
}
- private static <T extends Enum<T>> T enumeration(String value, Class<T> type) {
- String normalized = StringHelper.asEnumConstantValue(value);
- for (T constant : type.getEnumConstants()) {
- if (constant.name().equalsIgnoreCase(value) || constant.name().equalsIgnoreCase(normalized)) {
- return constant;
- }
+ private static Object resolve(CamelContext context, Object value) {
+ if (value instanceof String text) {
+ return context.resolvePropertyPlaceholders(text);
+ }
+ if (value instanceof Map<?, ?> map) {
+ Map<Object, Object> resolved = new LinkedHashMap<>();
+ map.forEach((key, item) -> resolved.put(key, resolve(context, item)));
+ return resolved;
+ }
+ if (value instanceof List<?> list) {
+ return list.stream().map(item -> resolve(context, item)).toList();
}
- throw new IllegalArgumentException("Invalid " + type.getSimpleName() + ": " + value);
+ return value;
}
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java
index 17c74e59f871..e92c910cacaa 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java
@@ -25,16 +25,30 @@ import java.util.WeakHashMap;
import java.util.function.Consumer;
import org.apache.camel.CamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
import org.apache.camel.spi.Resource;
/** Context-local named questions, replaced atomically per source when a route resource is reloaded. */
public final class SemanticQuestions {
private static final Object CREATION_LOCK = new Object();
+ private final CamelContext context;
private final Map<String, Map<String, SemanticQuestion>> sources = new HashMap<>();
private final Map<String, Resource> resources = new HashMap<>();
private volatile Map<String, SemanticQuestion> questions = Map.of();
+ private final ThreadLocal<Map<String, SemanticQuestion>> candidate = new ThreadLocal<>();
private final Map<Consumer<Map<String, SemanticQuestion>>, List<String>> validators = new WeakHashMap<>();
+ private SemanticQuestions(CamelContext context) {
+ this.context = context;
+ }
+
+ /** Validate every declaration, including evaluations not referenced by a route. */
+ public void validate() {
+ if (!questions.isEmpty()) {
+ ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(questions);
+ }
+ }
+
/** Validate current declarations and weakly track a callback owned by its initialized expression. */
public synchronized void setValidator(List<String> names, Consumer<Map<String, SemanticQuestion>> validator) {
// A replacement may have occurred between the expression's initial compilation and registration.
@@ -46,7 +60,7 @@ public final class SemanticQuestions {
synchronized (CREATION_LOCK) {
SemanticQuestions answer = context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class);
if (answer == null) {
- answer = new SemanticQuestions();
+ answer = new SemanticQuestions(context);
context.getCamelContextExtension().addContextPlugin(SemanticQuestions.class, answer);
}
return answer;
@@ -56,7 +70,9 @@ public final class SemanticQuestions {
/**
* Replace all definitions from one source; an empty map removes obsolete declarations. The Java fluent helper
* reserves {@code java:} followed by the resource location (or a generated key for embedded builders). XML and YAML
- * declarations use the resource location as their source key.
+ * declarations use the resource location as their source key. On a started context, new and changed declarations
+ * are validated before publication, including unused evaluations. A validation failure preserves the previous
+ * source.
*/
public synchronized void replace(String source, Map<String, SemanticQuestion> definitions) {
Map<String, SemanticQuestion> replacement = new HashMap<>();
@@ -73,19 +89,32 @@ public final class SemanticQuestions {
throw new IllegalArgumentException("Duplicate semantic question: " + name);
}
});
- validators.forEach((validator, names) -> {
- if (!Collections.disjoint(names, definitions.keySet())) {
- Map<String, SemanticQuestion> selected = new LinkedHashMap<>();
- names.forEach(name -> {
- SemanticQuestion question = replacement.get(name);
- // Removed declarations remain removable; their existing expressions fail if evaluated again.
- if (question != null) {
- selected.put(name, question);
- }
- });
- validator.accept(Collections.unmodifiableMap(selected));
+ Map<String, SemanticQuestion> previous = candidate.get();
+ candidate.set(Map.copyOf(replacement));
+ try {
+ if (context.isStarted() && !definitions.isEmpty()) {
+ ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(definitions);
}
- });
+ validators.forEach((validator, names) -> {
+ if (!Collections.disjoint(names, definitions.keySet())) {
+ Map<String, SemanticQuestion> selected = new LinkedHashMap<>();
+ names.forEach(name -> {
+ SemanticQuestion question = replacement.get(name);
+ // Removed declarations remain removable; their existing expressions fail if evaluated again.
+ if (question != null) {
+ selected.put(name, question);
+ }
+ });
+ validator.accept(Collections.unmodifiableMap(selected));
+ }
+ });
+ } finally {
+ if (previous == null) {
+ candidate.remove();
+ } else {
+ candidate.set(previous);
+ }
+ }
if (definitions.isEmpty()) {
sources.remove(source);
} else {
@@ -125,7 +154,7 @@ public final class SemanticQuestions {
}
public SemanticQuestion get(String name) {
- SemanticQuestion question = questions.get(name);
+ SemanticQuestion question = snapshot().get(name);
if (question == null) {
throw new IllegalArgumentException("Unknown semantic question: " + name);
}
@@ -134,7 +163,7 @@ public final class SemanticQuestions {
/** Resolve all requested names from one immutable snapshot, preserving reference order. */
public Map<String, SemanticQuestion> get(List<String> names) {
- Map<String, SemanticQuestion> snapshot = questions;
+ Map<String, SemanticQuestion> snapshot = snapshot();
Map<String, SemanticQuestion> selected = new LinkedHashMap<>();
for (String name : names) {
SemanticQuestion question = snapshot.get(name);
@@ -145,4 +174,10 @@ public final class SemanticQuestions {
}
return Collections.unmodifiableMap(selected);
}
+
+ private Map<String, SemanticQuestion> snapshot() {
+ Map<String, SemanticQuestion> validation = candidate.get();
+ return validation != null ? validation : questions;
+ }
+
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java
index 5eade0c90818..a9f2ce5f979b 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java
@@ -30,6 +30,8 @@ import org.apache.camel.spi.Resource;
/** Fluent declarations for use inside an ordinary {@link RouteBuilder#configure()}. */
public final class SemanticQuestionsBuilder {
private static final String LIFECYCLE = SemanticQuestionsBuilder.class.getName();
+ private String expert;
+ private String state;
private final CamelContext context;
private final RouteBuilder builder;
private final Resource resource;
@@ -55,6 +57,28 @@ public final class SemanticQuestionsBuilder {
return new SemanticQuestionsBuilder(builder);
}
+ public SemanticQuestionsBuilder expert(String expert) {
+ this.expert = expert;
+ return this;
+ }
+
+ public SemanticQuestionsBuilder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ String getExpert() {
+ return expert;
+ }
+
+ String getState() {
+ return state;
+ }
+
+ public SemanticQuestionBuilder evaluation(String name) {
+ return question(name);
+ }
+
/** Add a named question. Names must be unique across the context. */
public SemanticQuestionBuilder question(String name) {
if (name == null || name.isBlank()) {
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java
index 1b4374154712..658018d0fbfb 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java
@@ -33,6 +33,14 @@ public class SemanticReloadPlugin implements ContextServicePlugin {
public void load(CamelContext context) {
installXmlLoader(context);
lifecycle = new LifecycleStrategySupport() {
+ @Override
+ public void onContextStarting(CamelContext camelContext) {
+ SemanticQuestions questions = camelContext.getCamelContextExtension().getContextPlugin(SemanticQuestions.class);
+ if (questions != null) {
+ questions.validate();
+ }
+ }
+
@Override
public void onContextInitializing(CamelContext camelContext) {
// Applications can replace the registry after the context's eager build phase.
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticResult.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticResult.java
index ba44c27da0b2..76f2c406dcdb 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticResult.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticResult.java
@@ -17,11 +17,13 @@
package org.apache.camel.semantic;
import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
/**
- * A provider answer. Value is Boolean, a category String, or a numeric score. A boolean provider may supply only
- * probability; the question then defines the decision policy. Missing probabilities/confidence remain absent. Metadata
- * may contain provider/model identity, revision and usage. It must not contain credentials or input state.
+ * An expert answer after applying its policy. Value is Boolean, a category String, a numeric score, or a set of
+ * classification labels. Missing probabilities/confidence remain absent. Metadata may contain provider/model identity,
+ * revision and usage. It must not contain credentials or input state.
*/
public final class SemanticResult {
private final Object value;
@@ -32,7 +34,10 @@ public final class SemanticResult {
public SemanticResult(Object value, Double probability, Map<String, Double> probabilities,
Double confidence, Map<String, Object> metadata) {
- this.value = value;
+ if (value instanceof Set<?> labels && labels.stream().anyMatch(Objects::isNull)) {
+ throw new IllegalArgumentException("Semantic classification labels must not be null");
+ }
+ this.value = value instanceof Set<?> labels ? Set.copyOf(labels) : value;
this.probability = probability;
this.probabilities = probabilities == null ? Map.of() : Map.copyOf(probabilities);
this.confidence = confidence;
@@ -48,44 +53,6 @@ public final class SemanticResult {
}
}
- public Object decision(SemanticQuestion question) {
- switch (question.getType()) {
- case BOOLEAN:
- if (probability != null) {
- double threshold = question.getThreshold();
- double uncertainty = question.getUncertainty();
- if (uncertainty > 0 && probability >= threshold - uncertainty && probability <= threshold + uncertainty) {
- if (question.getUncertaintyPolicy() == SemanticQuestion.UncertaintyPolicy.FAIL) {
- throw new IllegalStateException("Semantic boolean decision is uncertain");
- }
- return false;
- }
- return probability >= threshold;
- }
- if (value instanceof Boolean && question.getUncertainty() == 0 && question.getThreshold() == 0.5) {
- return value;
- }
- break;
- case CHOICE:
- if (value instanceof String && question.getCriteria().containsKey(value)) {
- if (!probabilities.isEmpty() && !probabilities.keySet().equals(question.getCriteria().keySet())) {
- throw new IllegalArgumentException("Semantic choice probabilities must cover every criterion");
- }
- return value;
- }
- break;
- case SCORE:
- if (value instanceof Number number && Double.isFinite(number.doubleValue()) && number.doubleValue() >= 0
- && number.doubleValue() <= question.getLevels().size() - 1) {
- return number.doubleValue();
- }
- break;
- default:
- break;
- }
- throw new IllegalArgumentException("Semantic result does not support the question and its decision policy");
- }
-
public Object getValue() {
return value;
}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java
index 17bf179090ac..e5d7fae3d65b 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java
@@ -18,8 +18,11 @@ package org.apache.camel.semantic;
import java.io.IOException;
import java.io.InputStream;
+import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import javax.xml.XMLConstants;
@@ -137,15 +140,22 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
}
private static void declarations(Element semantic, SemanticQuestionsBuilder questions) {
- attributes(semantic, Set.of());
+ attributes(semantic, Set.of("expert", "state"));
+ if (semantic.hasAttribute("expert")) {
+ questions.expert(semantic.getAttribute("expert"));
+ }
+ if (semantic.hasAttribute("state")) {
+ questions.state(semantic.getAttribute("state"));
+ }
for (Element element : children(semantic)) {
- if (!"question".equals(element.getLocalName())) {
+ if (!"question".equals(element.getLocalName()) && !"evaluation".equals(element.getLocalName())) {
throw new IllegalArgumentException("Unexpected semantic element: " + element.getTagName());
}
try {
question(element, questions);
} catch (IllegalArgumentException e) {
- String expert = element.hasAttribute("expert") ? element.getAttribute("expert") : "default/automatic";
+ String expert = element.hasAttribute("expert") ? element.getAttribute("expert")
+ : questions.getExpert() != null ? questions.getExpert() : "default/automatic";
throw new IllegalArgumentException(
"Invalid semantic question '" + element.getAttribute("name") + "': " + e.getMessage()
+ " (expert '" + expert + "')",
@@ -155,8 +165,15 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
}
private static void question(Element element, SemanticQuestionsBuilder questions) {
- attributes(element, Set.of("name", "type", "expert", "state", "threshold", "uncertainty", "uncertaintyPolicy"));
+ attributes(element,
+ Set.of("name", "type", "operation", "expert", "state", "threshold", "uncertainty", "uncertaintyPolicy"));
SemanticQuestionBuilder question = questions.question(element.getAttribute("name"));
+ if (element.hasAttribute("type") && element.hasAttribute("operation")) {
+ throw new IllegalArgumentException("Specify exactly one operation or type");
+ }
+ if (element.hasAttribute("operation")) {
+ question.operation(element.getAttribute("operation"));
+ }
if (element.hasAttribute("type")) {
question.type(element.getAttribute("type"));
}
@@ -178,6 +195,16 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
boolean instructions = false;
for (Element child : children(element)) {
switch (child.getLocalName()) {
+ case "parameters" -> {
+ attributes(child, Set.of());
+ for (Element parameter : children(child)) {
+ if (!"parameter".equals(parameter.getLocalName())) {
+ throw new IllegalArgumentException("Expected parameter element");
+ }
+ attributes(parameter, Set.of("name"));
+ question.parameter(parameter.getAttribute("name"), parameterValue(parameter));
+ }
+ }
case "instructions" -> {
if (instructions) {
throw new IllegalArgumentException("Duplicate instructions for semantic question");
@@ -202,6 +229,58 @@ public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
}
}
+ private static Object parameterValue(Element parent) {
+ List<Element> values = children(parent);
+ if (values.size() != 1) {
+ throw new IllegalArgumentException("Parameter or map entry requires exactly one typed value");
+ }
+ return value(values.get(0));
+ }
+
+ private static Object value(Element element) {
+ attributes(element, Set.of());
+ return switch (element.getLocalName()) {
+ case "string" -> text(element);
+ case "null" -> {
+ if (!text(element).isBlank()) {
+ throw new IllegalArgumentException("Null parameter value must be empty");
+ }
+ yield null;
+ }
+ case "number" -> {
+ try {
+ yield new BigDecimal(text(element).strip());
+ } catch (NumberFormatException invalid) {
+ throw new IllegalArgumentException("Invalid numeric parameter");
+ }
+ }
+ case "boolean" -> {
+ String text = text(element).strip();
+ if (!"true".equals(text) && !"false".equals(text)) {
+ throw new IllegalArgumentException("Invalid boolean parameter");
+ }
+ yield Boolean.valueOf(text);
+ }
+ case "list" -> children(element).stream().map(SemanticXmlRoutesBuilderLoader::value).toList();
+ case "map" -> {
+ Map<String, Object> map = new LinkedHashMap<>();
+ for (Element entry : children(element)) {
+ if (!"entry".equals(entry.getLocalName())) {
+ throw new IllegalArgumentException("Expected map entry");
+ }
+ attributes(entry, Set.of("key"));
+ String key = entry.getAttribute("key");
+ if (map.containsKey(key)) {
+ throw new IllegalArgumentException("Duplicate parameter map key: " + key);
+ }
+ map.put(key, parameterValue(entry));
+ }
+ yield map;
+ }
+ default -> throw new IllegalArgumentException("Unknown parameter value type: " + element.getLocalName());
+ };
+ }
+
private static String text(Element element) {
for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child instanceof Element) {
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java
index c70c592866d9..cef45e978038 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java
@@ -16,40 +16,50 @@
*/
package org.apache.camel.semantic.yaml;
+import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import org.apache.camel.CamelContext;
import org.apache.camel.dsl.yaml.common.YamlDeserializationContext;
import org.apache.camel.dsl.yaml.common.YamlDeserializerResolver;
import org.apache.camel.dsl.yaml.common.YamlDeserializerSupport;
-import org.apache.camel.dsl.yaml.common.exception.InvalidEnumException;
import org.apache.camel.dsl.yaml.common.exception.InvalidNodeTypeException;
import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
import org.apache.camel.semantic.SemanticQuestion;
+import org.apache.camel.semantic.SemanticQuestionBuilder;
import org.apache.camel.semantic.SemanticQuestions;
import org.apache.camel.spi.CamelContextCustomizer;
import org.apache.camel.spi.annotations.YamlIn;
import org.apache.camel.spi.annotations.YamlProperty;
import org.apache.camel.spi.annotations.YamlType;
import org.snakeyaml.engine.v2.api.ConstructNode;
+import org.snakeyaml.engine.v2.api.LoadSettings;
+import org.snakeyaml.engine.v2.constructor.StandardConstructor;
import org.snakeyaml.engine.v2.nodes.MappingNode;
import org.snakeyaml.engine.v2.nodes.Node;
import org.snakeyaml.engine.v2.nodes.NodeTuple;
import org.snakeyaml.engine.v2.nodes.SequenceNode;
+import org.snakeyaml.engine.v2.nodes.Tag;
/** Named semantic declarations are installed in a resource-wide pass before route references are resolved. */
@YamlIn
@YamlType(nodes = "semantic", properties = {
- @YamlProperty(name = "question",
- type = "map:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionSchema",
- required = true)
+ @YamlProperty(name = "__oneOf",
+ type = "object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionBlockSchema",
+ oneOf = "declarations", required = true),
+ @YamlProperty(name = "__oneOf",
+ type = "object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationBlockSchema",
+ oneOf = "declarations", required = true)
})
public class SemanticDefinitionDeserializer extends YamlDeserializerSupport implements ConstructNode, YamlDeserializerResolver {
private static final Set<String> FIELDS
- = Set.of("type", "expert", "instructions", "state", "criteria", "threshold", "uncertainty", "uncertaintyPolicy");
+ = Set.of("type", "operation", "expert", "parameters", "instructions", "state", "criteria", "threshold",
+ "uncertainty", "uncertaintyPolicy");
@Override
public ConstructNode resolve(String id) {
@@ -101,17 +111,33 @@ public class SemanticDefinitionDeserializer extends YamlDeserializerSupport impl
private static Map<String, SemanticQuestion> read(CamelContext context, Node node) {
Map<String, Node> semantic = fields(node, "semantic declaration");
- if (!semantic.keySet().equals(Set.of("question"))) {
- throw new YamlDeserializationException(node, "Semantic declaration requires only question");
+ for (String field : semantic.keySet()) {
+ if (!Set.of("question", "evaluation", "expert", "state").contains(field)) {
+ throw new YamlDeserializationException(
+ semantic.get(field), "Unknown property '" + field + "' in semantic declaration");
+ }
+ }
+ if (semantic.containsKey("question") == semantic.containsKey("evaluation")) {
+ throw new YamlDeserializationException(node, "Semantic declaration requires exactly one of question or evaluation");
}
+ String declaration = semantic.containsKey("evaluation") ? "evaluation" : "question";
Map<String, SemanticQuestion> result = new LinkedHashMap<>();
- fields(semantic.get("question"), "semantic questions").forEach((name, definition) -> {
+ fields(semantic.get(declaration), "semantic evaluations").forEach((name, definition) -> {
if (name.isBlank()) {
throw new YamlDeserializationException(definition, "Semantic question requires a nonblank name");
}
Map<String, Node> values = fields(definition, "semantic question '" + name + "'");
- String expert = values.containsKey("expert") ? asText(values.get("expert")) : "default/automatic";
+ for (String common : List.of("expert", "state")) {
+ if (!values.containsKey(common) && semantic.containsKey(common)) {
+ values.put(common, semantic.get(common));
+ }
+ }
+ String expert = "default/automatic";
try {
+ if (values.containsKey("expert")) {
+ expert = "invalid expert reference";
+ expert = asText(values.get("expert"));
+ }
result.put(name, readQuestion(context, name, expert, definition, values));
} catch (IllegalArgumentException | InvalidNodeTypeException e) {
throw new YamlDeserializationException(
@@ -133,63 +159,71 @@ public class SemanticDefinitionDeserializer extends YamlDeserializerSupport impl
value, "Unknown property '" + field + "' in " + description + expertContext);
}
});
- if (!values.containsKey("type")) {
- throw new YamlDeserializationException(definition, "Semantic question type is required: " + name + expertContext);
+ if (values.containsKey("type") == values.containsKey("operation")) {
+ throw new YamlDeserializationException(
+ definition, "Specify exactly one operation or type: " + name + expertContext);
}
- SemanticQuestion.Type type = enumeration(values.get("type"), name, expert, "type", SemanticQuestion.Type.class);
- Map<String, String> criteria = new LinkedHashMap<>();
- List<String> levels = List.of();
- if (values.containsKey("criteria")) {
- if (type == SemanticQuestion.Type.SCORE) {
- levels = asSequenceNode(values.get("criteria")).getValue().stream().map(YamlDeserializerSupport::asText)
- .toList();
- } else {
- fields(values.get("criteria"), "criteria for " + description + expertContext)
- .forEach((key, value) -> criteria.put(key, asText(value)));
- }
+ String operation = values.containsKey("operation")
+ ? asText(values.get("operation"))
+ : asText(values.get("type")).toLowerCase(Locale.ROOT);
+ SemanticQuestionBuilder builder = new SemanticQuestionBuilder().operation(operation)
+ .expert(asText(values.get("expert"))).state(asText(values.get("state")));
+ if (values.containsKey("parameters")) {
+ fields(values.get("parameters"), "evaluation parameters")
+ .forEach((key, value) -> builder.parameter(key, value(value)));
}
- if (type != SemanticQuestion.Type.BOOLEAN && (values.containsKey("threshold") || values.containsKey("uncertainty")
- || values.containsKey("uncertaintyPolicy"))) {
- throw new YamlDeserializationException(
- definition, "Threshold and uncertainty policy require a boolean question: " + name + expertContext);
+ for (String field : List.of("instructions", "criteria", "threshold", "uncertainty", "uncertaintyPolicy")) {
+ Node node = values.get(field);
+ if (node == null) {
+ continue;
+ }
+ switch (field) {
+ case "instructions" -> builder.instructions(asText(node));
+ case "threshold", "uncertainty" -> {
+ try {
+ builder.parameter(field, Double.valueOf(context.resolvePropertyPlaceholders(asText(node))));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(
+ node,
+ "Invalid numeric value for '" + field + "' in " + description + expertContext);
+ }
+ }
+ case "uncertaintyPolicy" -> builder.uncertaintyPolicy(asText(node));
+ case "criteria" -> {
+ if (node instanceof MappingNode) {
+ Map<String, String> criteria = new LinkedHashMap<>();
+ fields(node, "criteria").forEach((key, child) -> criteria.put(key, asText(child)));
+ builder.parameter(field, criteria);
+ } else if (node instanceof SequenceNode sequence) {
+ builder.parameter(field,
+ sequence.getValue().stream().map(SemanticDefinitionDeserializer::asText).toList());
+ } else {
+ builder.parameter(field, value(node));
+ }
+ }
+ default -> throw new IllegalStateException(field);
+ }
}
- SemanticQuestion.UncertaintyPolicy policy = values.containsKey("uncertaintyPolicy")
- ? enumeration(values.get("uncertaintyPolicy"), name, expert, "uncertaintyPolicy",
- SemanticQuestion.UncertaintyPolicy.class)
- : SemanticQuestion.UncertaintyPolicy.FAIL;
- return new SemanticQuestion(
- type, asText(values.get("instructions")), asText(values.get("state")),
- criteria, levels, number(context, values, name, expert, "threshold", 0.5),
- number(context, values, name, expert, "uncertainty", 0), policy, asText(values.get("expert")));
+ return builder.build(context);
}
- private static <T extends Enum<T>> T enumeration(Node node, String question, String expert, String field, Class<T> type) {
- try {
- return asEnum(node, type);
- } catch (InvalidEnumException e) {
- throw new YamlDeserializationException(
- node, "Invalid value for '" + field + "' in semantic question '" + question + "': " + asText(node)
- + " (expert '" + expert + "')",
- e);
+ private static Object value(Node node) {
+ if (node instanceof MappingNode) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ fields(node, "parameter map").forEach((key, child) -> result.put(key, value(child)));
+ return result;
}
- }
-
- private static double number(
- CamelContext context, Map<String, Node> values, String question, String expert, String name, double fallback) {
- if (!values.containsKey(name)) {
- return fallback;
+ if (node instanceof SequenceNode sequence) {
+ return sequence.getValue().stream().map(SemanticDefinitionDeserializer::value).toList();
}
- Node node = values.get(name);
- String raw = asText(node);
- try {
- return Double.parseDouble(context.resolvePropertyPlaceholders(raw));
- } catch (NumberFormatException e) {
- throw new YamlDeserializationException(
- node,
- "Invalid numeric value for '" + name + "' in semantic question '" + question + "': " + raw
- + " (expert '" + expert + "')",
- e);
+ if (Tag.FLOAT.equals(node.getTag())) {
+ try {
+ return new BigDecimal(asText(node));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(node, "Invalid numeric parameter");
+ }
}
+ return new StandardConstructor(LoadSettings.builder().build()).constructSingleDocument(Optional.of(node));
}
private static Map<String, Node> fields(Node node, String description) {
@@ -203,50 +237,58 @@ public class SemanticDefinitionDeserializer extends YamlDeserializerSupport impl
return result;
}
+ @YamlType(properties = {
+ @YamlProperty(name = "expert", type = "string"),
+ @YamlProperty(name = "state", type = "string")
+ })
+ public static class BlockSchema {
+ }
+
+ @YamlType(properties = {
+ @YamlProperty(name = "question",
+ type = "map:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema",
+ required = true)
+ })
+ public static class QuestionBlockSchema extends BlockSchema {
+ }
+
+ @YamlType(properties = {
+ @YamlProperty(name = "evaluation",
+ type = "map:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema",
+ required = true)
+ })
+ public static class EvaluationBlockSchema extends BlockSchema {
+ }
+
@YamlType(properties = {
@YamlProperty(name = "__oneOf",
- type = "object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$BooleanSchema",
- oneOf = "kind", required = true),
- @YamlProperty(name = "__oneOf",
- type = "object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ChoiceSchema",
- oneOf = "kind", required = true),
+ type = "object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$MapCriteriaSchema",
+ oneOf = "criteria", required = true),
@YamlProperty(name = "__oneOf",
- type = "object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ScoreSchema",
- oneOf = "kind", required = true)
+ type = "object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ListCriteriaSchema",
+ oneOf = "criteria", required = true)
})
- public static class QuestionSchema {
+ public static class EvaluationSchema {
}
@YamlType(properties = {
- @YamlProperty(name = "type", type = "enum:boolean", required = true),
+ @YamlProperty(name = "operation", type = "string"),
+ @YamlProperty(name = "type", type = "string"),
@YamlProperty(name = "expert", type = "string"),
- @YamlProperty(name = "instructions", type = "string"),
@YamlProperty(name = "state", type = "string"),
+ @YamlProperty(name = "parameters", type = "object"),
+ @YamlProperty(name = "instructions", type = "string"),
@YamlProperty(name = "criteria", type = "map:string"),
@YamlProperty(name = "threshold", type = "number"),
@YamlProperty(name = "uncertainty", type = "number"),
- @YamlProperty(name = "uncertaintyPolicy", type = "enum:fail,non-match")
+ @YamlProperty(name = "uncertaintyPolicy", type = "string")
})
- public static class BooleanSchema {
+ public static class MapCriteriaSchema {
}
@YamlType(properties = {
- @YamlProperty(name = "type", type = "enum:choice", required = true),
- @YamlProperty(name = "expert", type = "string"),
- @YamlProperty(name = "instructions", type = "string"),
- @YamlProperty(name = "state", type = "string"),
- @YamlProperty(name = "criteria", type = "map:string", required = true)
- })
- public static class ChoiceSchema {
- }
-
- @YamlType(properties = {
- @YamlProperty(name = "type", type = "enum:score", required = true),
- @YamlProperty(name = "expert", type = "string"),
- @YamlProperty(name = "instructions", type = "string"),
- @YamlProperty(name = "state", type = "string"),
@YamlProperty(name = "criteria", type = "array:string", required = true)
})
- public static class ScoreSchema {
+ public static class ListCriteriaSchema extends MapCriteriaSchema {
}
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/FixedSemanticExpert.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/FixedSemanticExpert.java
new file mode 100644
index 000000000000..f44ee11a45d2
--- /dev/null
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/FixedSemanticExpert.java
@@ -0,0 +1,84 @@
+/*
+ * 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.semantic;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.semantic.SemanticExpert.InputType;
+import org.apache.camel.semantic.SemanticExpert.ResultType;
+
+@SemanticExpert(name = "fixed", description = "Fixed detection", provider = "test", artifactId = "test",
+ operations = @SemanticOperation(name = "boolean", description = "Detect injection",
+ inputTypes = InputType.TEXT, inputRequirements = "A text message",
+ resultType = ResultType.BOOLEAN,
+ resultMeaning = "True means injection detected",
+ probability = true, probabilityMeaning = "Probability of injection",
+ parameters = {
+ @SemanticParameter(name = "threshold",
+ description = "Detection threshold",
+ type = Number.class,
+ minimum = 0, maximum = 1, omission = "Use 0.5"),
+ @SemanticParameter(name = "uncertainty",
+ description = "Uncertainty band",
+ type = Number.class,
+ minimum = 0, maximum = 1, omission = "No band"),
+ @SemanticParameter(name = "uncertaintyPolicy",
+ description = "Uncertainty policy",
+ values = { "fail", "non-match" },
+ omission = "Fail") }))
+class FixedSemanticExpert implements SemanticAdapter {
+ double probability = 0.9;
+ boolean fail;
+ int calls;
+ final List<List<String>> batches = new ArrayList<>();
+ final List<Object> states = new ArrayList<>();
+
+ @Override
+ public void validate(SemanticQuestion question) {
+ SemanticCapabilities.from(getClass()).validate(question);
+ }
+
+ @Override
+ public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ calls++;
+ states.add(state);
+ if (fail) {
+ throw new IllegalStateException("provider unavailable");
+ }
+ return TestSemanticAdapter.applyPolicy(question,
+ new SemanticResult(null, probability, null, null, Map.of("provider", "fixed")));
+ }
+
+ @Override
+ public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticQuestion> questions, Object state)
+ throws Exception {
+ batches.add(List.copyOf(questions.keySet()));
+ return SemanticAdapter.super.evaluateBatch(questions, state);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof FixedSemanticExpert;
+ }
+
+ @Override
+ public int hashCode() {
+ return 1;
+ }
+}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticBatchTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticBatchTest.java
index 44b329d4f058..efb6e8412b7f 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticBatchTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticBatchTest.java
@@ -328,10 +328,11 @@ class SemanticBatchTest {
case CHOICE -> new SemanticResult(
"billing", null, Map.of("billing", 0.9, "technical", 0.1), 0.8, Map.of("provider", "fixture"));
case SCORE -> new SemanticResult(1.2, null, null, 0.7, Map.of("provider", "fixture"));
+ default -> throw new IllegalArgumentException("Unsupported fixture operation");
};
}
- private static class RecordingAdapter implements SemanticAdapter {
+ private static class RecordingAdapter extends TestSemanticAdapter {
final List<SemanticQuestion.Type> calls = new ArrayList<>();
final List<Object> states = new ArrayList<>();
@@ -343,7 +344,7 @@ class SemanticBatchTest {
public SemanticResult evaluate(SemanticQuestion question, Object state) throws Exception {
calls.add(question.getType());
states.add(state);
- return result(question.getType());
+ return applyPolicy(question, result(question.getType()));
}
}
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java
index 77b245a62228..6e00e44c369e 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java
@@ -16,11 +16,15 @@
*/
package org.apache.camel.semantic;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.camel.semantic.SemanticExpert.InputType;
-import org.apache.camel.semantic.SemanticExpert.Instructions;
import org.apache.camel.semantic.SemanticExpert.ResultType;
import org.junit.jupiter.api.Test;
@@ -30,93 +34,137 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
class SemanticCapabilitiesTest {
@Test
- void configuredCapabilitiesAreIndependentImmutableAndOrdered() {
- ResultType[] types = { ResultType.SCORE, ResultType.BOOLEAN, ResultType.CHOICE };
- var builder = SemanticCapabilities.builder().name("general").inputTypes(InputType.STRUCTURED, InputType.TEXT)
- .resultTypes(types).confidenceTypes(types).callerDefinedCriteria(true).maxChoices(10);
- var capabilities = builder.build();
- types[0] = ResultType.BOOLEAN;
- builder.resultTypes(ResultType.BOOLEAN).maxChoices(1);
- var narrowed = capabilities.toBuilder().resultTypes(ResultType.BOOLEAN).maxChoices(2).build();
- assertThat(capabilities.isKnown()).isTrue();
- assertThat(capabilities.getResultTypes()).containsExactly(ResultType.BOOLEAN, ResultType.CHOICE, ResultType.SCORE);
- assertThat(capabilities.getInputTypes()).containsExactly(InputType.TEXT, InputType.STRUCTURED);
- assertThat(capabilities.getConfidenceTypes()).containsExactly(ResultType.BOOLEAN, ResultType.CHOICE, ResultType.SCORE);
- assertThatThrownBy(() -> capabilities.getResultTypes().clear()).isInstanceOf(UnsupportedOperationException.class);
- assertThat(capabilities.getMaxChoices()).isEqualTo(10);
- assertThat(narrowed.getMaxChoices()).isEqualTo(2);
- assertThat(narrowed.getName()).isEqualTo("general");
- assertThat(narrowed.getResultTypes()).containsExactly(ResultType.BOOLEAN);
- assertThatThrownBy(() -> narrowed.validate(question(SemanticQuestion.Type.CHOICE, Map.of("a", "A"), List.of())))
- .hasMessageContaining("supports [BOOLEAN]");
+ void staticContractDoesNotConstructExpertAndIsImmutable() {
+ var capabilities = SemanticCapabilities.from(StaticExpert.class);
+ var operation = capabilities.operation("classify");
+ assertThat(capabilities.getName()).isEqualTo("static");
+ assertThat(SemanticCapabilities.from(StaticExpert.class)).isSameAs(capabilities);
+ assertThat(operation.getResultType()).isEqualTo(ResultType.CLASSIFICATION);
+ assertThat(operation.getInputTypes()).containsExactly(InputType.STRUCTURED);
+ assertThat(operation.getParameters().get("policy").getOmission()).isEqualTo("Service defaults");
+ assertThatThrownBy(() -> capabilities.getOperations().clear()).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(() -> operation.getParameters().clear()).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(() -> operation.getLabels().clear()).isInstanceOf(UnsupportedOperationException.class);
}
@Test
- void duplicateAnnotationValuesAreDeduplicated() {
- var capabilities = SemanticCapabilities.from(DuplicateExpert.class.getAnnotation(SemanticExpert.class));
- assertThat(capabilities.getInputTypes()).containsExactly(InputType.TEXT, InputType.STRUCTURED);
- assertThat(capabilities.getResultTypes()).containsExactly(ResultType.BOOLEAN, ResultType.SCORE);
- assertThat(capabilities.getConfidenceTypes()).containsExactly(ResultType.BOOLEAN, ResultType.SCORE);
+ void operationAndParameterValidationDoNotExposeValues() {
+ var capabilities = SemanticCapabilities.from(StaticExpert.class);
+ assertThatThrownBy(() -> capabilities.operation("missing")).hasMessageContaining("Unknown operation");
+ var operation = capabilities.operation("classify");
+ assertThatThrownBy(() -> operation.validate(Map.of())).hasMessageContaining("criterion")
+ .hasMessageContaining("required");
+ assertThatThrownBy(() -> operation.validate(Map.of("criterion", "SECRET")))
+ .hasMessageContaining("criterion").hasMessageNotContaining("SECRET");
+ assertThatThrownBy(() -> operation.validate(Map.of("criterion", "safety", "unknown", "SECRET")))
+ .hasMessageContaining("unknown").hasMessageNotContaining("SECRET");
+ assertThatThrownBy(() -> operation.validate(Map.of("criterion", "safety", "threshold", "0.5")))
+ .hasMessageContaining("threshold").hasMessageContaining("Number");
+ assertThatThrownBy(() -> operation.validate(Map.of("criterion", "safety", "threshold", Double.NaN)))
+ .hasMessageContaining("threshold");
+ assertThatCode(() -> operation.validate(Map.of("criterion", "safety", "threshold", 0.5)))
+ .doesNotThrowAnyException();
}
@Test
- void builderAcceptsEmptyEnumArrays() {
- var capabilities = SemanticCapabilities.builder().inputTypes().resultTypes().confidenceTypes().build();
- assertThat(capabilities.getInputTypes()).isEmpty();
- assertThat(capabilities.getResultTypes()).isEmpty();
- assertThat(capabilities.getConfidenceTypes()).isEmpty();
+ void nestedDeclarationParametersAreCopiedButOmittedValuesStayAbsent() {
+ Map<String, Object> policy = new LinkedHashMap<>();
+ List<String> labels = new ArrayList<>(List.of("privacy"));
+ policy.put("labels", labels);
+ SemanticQuestion evaluation = new SemanticQuestion(
+ "classify", "expert", null,
+ Map.of("criterion", "safety", "policy", policy));
+ labels.add("later");
+ policy.put("new", true);
+ assertThat(evaluation.getParameters()).doesNotContainKey("threshold");
+ assertThat(evaluation.getParameters().get("policy")).isEqualTo(Map.of("labels", List.of("privacy")));
+ assertThatThrownBy(() -> evaluation.getParameters().clear()).isInstanceOf(UnsupportedOperationException.class);
}
- @SemanticExpert(name = "duplicates", description = "Duplicate capability values", provider = "test", artifactId = "test",
- instructions = Instructions.OPTIONAL, callerDefinedCriteria = false,
- inputTypes = { InputType.STRUCTURED, InputType.TEXT, InputType.TEXT },
- resultTypes = { ResultType.SCORE, ResultType.BOOLEAN, ResultType.BOOLEAN },
- confidenceTypes = { ResultType.SCORE, ResultType.BOOLEAN, ResultType.BOOLEAN })
- private static class DuplicateExpert {
+ @Test
+ void resultTypesRangesAndOptionalInformationAreValidated() {
+ var operation = SemanticCapabilities.from(StaticExpert.class).operation("classify");
+ assertThat(operation.validateResult(result(Set.of()))).isEqualTo(Set.of());
+ assertThat(operation.validateResult(result(Set.of("privacy")))).isEqualTo(Set.of("privacy"));
+ assertThatThrownBy(() -> operation.validateResult(result(List.of("privacy")))).hasMessageContaining("CLASSIFICATION");
+ assertThatThrownBy(() -> operation.validateResult(result(Set.of("unknown")))).hasMessageContaining("CLASSIFICATION");
+ assertThatThrownBy(() -> operation.validateResult(new SemanticResult(Set.of(), null, null, 0.9, null)))
+ .hasMessageContaining("undeclared");
+ assertThat(operation.validateResult(new SemanticResult(Set.of("privacy"), null, Map.of("privacy", 0.8), null, null)))
+ .isEqualTo(Set.of("privacy"));
+ assertThatThrownBy(
+ () -> operation.validateResult(new SemanticResult(Set.of(), null, Map.of("unknown", 0.8), null, null)))
+ .hasMessageContaining("unknown labels");
+ var score = SemanticCapabilities.from(StaticExpert.class).operation("score");
+ assertThat(score.validateResult(result(42))).isEqualTo(42);
+ for (double value : new double[] { -1, 101, Double.NaN, Double.POSITIVE_INFINITY }) {
+ assertThatThrownBy(() -> score.validateResult(result(value))).hasMessageContaining("SCORE");
+ }
}
@Test
- void choiceLimitsDoNotApplyToBooleanCriteriaOrUnlimitedScores() {
- var capabilities = SemanticCapabilities.builder().resultTypes(ResultType.values())
- .callerDefinedCriteria(true).maxChoices(1).build();
- Map<String, String> criteria = Map.of("true", "Yes", "false", "No");
- assertThatCode(() -> capabilities.validate(question(SemanticQuestion.Type.BOOLEAN, criteria, List.of())))
- .doesNotThrowAnyException();
- assertThatCode(() -> capabilities.validate(question(SemanticQuestion.Type.CHOICE, Map.of("a", "A"), List.of())))
- .doesNotThrowAnyException();
- assertThatThrownBy(() -> capabilities.validate(question(SemanticQuestion.Type.CHOICE, criteria, List.of())))
- .hasMessage("Supports at most 1 choice criteria");
- assertThatCode(() -> capabilities.validate(question(SemanticQuestion.Type.SCORE, Map.of(), List.of("low", "high"))))
- .doesNotThrowAnyException();
+ void explicitPolicyValuesArePreservedAndMutableNumbersAreRejected() {
+ var evaluation = new SemanticQuestion(
+ SemanticQuestion.Type.BOOLEAN, null, null, null, null,
+ 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
+ assertThat(evaluation.getParameters()).containsEntry("threshold", 0.5).containsEntry("uncertainty", 0.0)
+ .containsEntry("uncertaintyPolicy", "fail");
+ assertThatThrownBy(() -> new SemanticQuestion("classify", "expert", null, Map.of("threshold", new AtomicInteger(1))))
+ .hasMessageContaining("immutable scalar");
+ var operation = SemanticCapabilities.from(StaticExpert.class).operation("classify");
+ assertThatCode(() -> operation.validate(Map.of("criterion", "safety", "limit", 2.0))).doesNotThrowAnyException();
+ assertThatCode(() -> operation.validate(Map.of("criterion", "safety", "limit", 2))).doesNotThrowAnyException();
+ assertThatThrownBy(() -> operation.validate(Map.of("criterion", "safety", "limit", 2.1)))
+ .hasMessageContaining("integer");
}
@Test
- void scoreLimitReportsOnlyTheLimitedDimension() {
- var capabilities = SemanticCapabilities.builder().resultTypes(ResultType.values())
- .callerDefinedCriteria(true).maxScoreLevels(1).build();
- assertThatCode(() -> capabilities.validate(question(SemanticQuestion.Type.SCORE, Map.of(), List.of("low"))))
- .doesNotThrowAnyException();
- assertThatThrownBy(() -> capabilities.validate(question(SemanticQuestion.Type.SCORE, Map.of(), List.of("low", "high"))))
- .hasMessage("Supports at most 1 score levels");
- assertThatCode(
- () -> capabilities.validate(question(SemanticQuestion.Type.CHOICE, Map.of("a", "A", "b", "B"), List.of())))
- .doesNotThrowAnyException();
+ void numericBoundsDoNotRoundInvalidValuesOntoBoundaries() {
+ var contract = SemanticCapabilities.from(StaticExpert.class);
+ for (String value : List.of("1.0000000000000000001", "-1e-400")) {
+ assertThatThrownBy(() -> contract.operation("classify").validate(
+ Map.of("criterion", "safety", "threshold", new BigDecimal(value))))
+ .hasMessageContaining("threshold").hasMessageContaining("numeric constraints");
+ }
+ for (String value : List.of("100.0000000000000000001", "-1e-400")) {
+ assertThatThrownBy(() -> contract.operation("score").validateResult(result(new BigDecimal(value))))
+ .hasMessageContaining("SCORE");
+ }
}
- @Test
- void explicitOptionalInstructionsDoNotClaimUnspecifiedResultSupport() {
- var capabilities = SemanticCapabilities.builder().instructions(Instructions.OPTIONAL).build();
- var question = new SemanticQuestion(
- SemanticQuestion.Type.BOOLEAN, null, null, null, null, 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL);
- assertThatThrownBy(() -> capabilities.validate(question)).hasMessageContaining("supports []");
- assertThatCode(() -> capabilities.toBuilder().resultTypes(ResultType.BOOLEAN).build().validate(question))
- .doesNotThrowAnyException();
+ private static SemanticResult result(Object value) {
+ return new SemanticResult(value, null, null, null, null);
}
- private SemanticQuestion question(SemanticQuestion.Type type, Map<String, String> criteria, List<String> levels) {
- return new SemanticQuestion(
- type, "Evaluate this", null, criteria, levels, 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL);
+ @SemanticExpert(name = "static", description = "Inspectable without construction", provider = "test", artifactId = "test",
+ operations = {
+ @SemanticOperation(name = "classify", description = "Classify content",
+ inputTypes = InputType.STRUCTURED,
+ inputRequirements = "Conversation map", resultType = ResultType.CLASSIFICATION,
+ resultMeaning = "Detected categories", labels = { "privacy", "unsafe" },
+ probabilities = true,
+ probabilityMeaning = "Independent probability for each category", parameters = {
+ @SemanticParameter(name = "limit", description = "Maximum labels",
+ type = Number.class, integer = true, minimum = 1,
+ omission = "All labels"),
+ @SemanticParameter(name = "criterion", description = "Criterion",
+ required = true, values = "safety"),
+ @SemanticParameter(name = "policy",
+ description = "Nested provider policy",
+ type = Map.class, omission = "Service defaults"),
+ @SemanticParameter(name = "threshold",
+ description = "Requested filtering threshold",
+ type = Number.class,
+ minimum = 0, maximum = 1,
+ omission = "Service defaults") }),
+ @SemanticOperation(name = "score", description = "Quality", inputTypes = InputType.TEXT,
+ inputRequirements = "Text", resultType = ResultType.SCORE,
+ resultMeaning = "Quality from 0 to 100",
+ minimum = 0, maximum = 100)
+ })
+ private static class StaticExpert {
+ StaticExpert() {
+ throw new AssertionError("Contract inspection must not construct the expert");
+ }
}
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
new file mode 100644
index 000000000000..1b29c3c1ce90
--- /dev/null
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
@@ -0,0 +1,536 @@
+/*
+ * 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.semantic;
+
+import java.math.BigDecimal;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.dsl.yaml.common.YamlDeserializationContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
+import org.apache.camel.semantic.SemanticExpert.InputType;
+import org.apache.camel.semantic.SemanticExpert.ResultType;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.ResourceHelper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.snakeyaml.engine.v2.api.LoadSettings;
+import org.snakeyaml.engine.v2.api.lowlevel.Compose;
+
+import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class SemanticContractTest {
+ @ParameterizedTest
+ @ValueSource(strings = { "java", "yaml", "xml" })
+ void equivalentDeclarationsPreserveNestedParametersAndStructuredState(String dsl) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var expert = new ContentExpert();
+ context.getRegistry().bind("content", expert);
+ declarations(context, dsl);
+ context.start();
+ var evaluation = SemanticQuestions.get(context).get("categories");
+ assertThat(evaluation.getExpert()).isEqualTo("content");
+ assertThat(evaluation.getState()).isEqualTo("${body}");
+ assertThat(evaluation.getParameters()).containsOnlyKeys("policy", "limit");
+ assertThat(evaluation.getParameters().get("policy")).isInstanceOfSatisfying(Map.class, policy -> {
+ assertThat(policy.get("allowed-tags")).isEqualTo(List.of("privacy", "unsafe"));
+ assertThat(policy.get("enabled")).isEqualTo(true);
+ });
+ assertThat(new BigDecimal(((Map<?, ?>) evaluation.getParameters().get("policy")).get("cutoff").toString()))
+ .isEqualByComparingTo("0.7");
+ var exchange = new DefaultExchange(context);
+ Map<String, Object> state = Map.of("prompt", "request", "response", "answer", "documents", List.of("evidence"));
+ exchange.getMessage().setBody(state);
+ var expression = context.resolveLanguage("simple").createExpression("${semantic('categories')}");
+ assertThat(expression.evaluate(exchange, Object.class)).isEqualTo(Set.of("privacy"));
+ assertThat(expert.state).isSameAs(state);
+ assertThat(exchange.getMessage().getBody()).isSameAs(state);
+ assertThat(expert.calls).isOne();
+ assertThat(expression.evaluate(exchange, Object.class)).isEqualTo(Set.of("privacy"));
+ assertThat(expert.calls).isEqualTo(2);
+ var detail = exchange.getProperty(SemanticLanguage.RESULT, SemanticResult.class);
+ assertThat(detail.getConfidence()).isNull();
+ assertThat(detail.getProbabilities()).containsEntry("privacy", 0.8);
+ assertThat(context.resolveLanguage("simple").createPredicate("${semantic('categories')} contains 'privacy'")
+ .matches(exchange)).isTrue();
+ assertThat(expert.calls).isEqualTo(3);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "${semantic()}", "${semantic(name)}", "${semantic('')}", "${semantic(' ')}",
+ "${semantic('first','second')}" })
+ void simpleFunctionRejectsInvalidArguments(String expression) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.start();
+ assertThatThrownBy(() -> context.resolveLanguage("simple").createExpression(expression))
+ .hasMessageContaining("Semantic requires one quoted evaluation name");
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "safety", "classify", "rank" })
+ void simplePredicatesRejectNonBooleanBeforeInference(String operation) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var expert = new ContentExpert();
+ context.getRegistry().bind("content", expert);
+ SemanticQuestions.get(context).replace("test",
+ Map.of("q", new SemanticQuestion(operation, "content", null, Map.of())));
+ context.start();
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody(Map.of("response", "answer"));
+ for (String syntax : List.of("${semantic('q')}", "${!semantic('q')}", "!${semantic('q')}")) {
+ assertThatThrownBy(() -> context.resolveLanguage("simple").createPredicate(syntax).matches(exchange))
+ .as(syntax).hasMessageContaining("requires a boolean");
+ }
+ assertThat(expert.calls).isZero();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void cachedNestedSelectorsValidateTheCompleteReplacement(boolean transitive) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("content", new ContentExpert());
+ var registry = SemanticQuestions.get(context);
+ Map<String, SemanticQuestion> declarations = new LinkedHashMap<>();
+ declarations.put("outer", new SemanticQuestion("detect", "content", "${semantic('inner')} ready", Map.of()));
+ declarations.put("inner", new SemanticQuestion(
+ "detect", "content",
+ transitive ? "${semantic('leaf')} ready" : null, Map.of()));
+ if (transitive) {
+ declarations.put("leaf", new SemanticQuestion("detect", "content", null, Map.of()));
+ }
+ registry.replace("test", declarations);
+ context.start();
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("content");
+ var expression = context.resolveLanguage("semantic").createExpression("ref:outer");
+ assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
+ Map<String, SemanticQuestion> replacement = new LinkedHashMap<>(declarations);
+ String removed = transitive ? "leaf" : "inner";
+ replacement.remove(removed);
+ assertThatThrownBy(() -> registry.replace("test", replacement))
+ .hasMessageContaining("Unknown semantic question: " + removed);
+ assertThat(registry.get(removed)).isSameAs(declarations.get(removed));
+ assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
+ }
+ }
+
+ @Test
+ void candidateDeclarationsRemainPrivateUntilValidationCompletes() throws Exception {
+ var callers = Executors.newSingleThreadExecutor();
+ var entered = new CountDownLatch(1);
+ var release = new CountDownLatch(1);
+ try (var context = new DefaultCamelContext()) {
+ var original = new SemanticQuestion("detect", "content", null, Map.of());
+ var candidate = new SemanticQuestion("detect", "content", "${header.candidate}", Map.of());
+ var registry = SemanticQuestions.get(context);
+ context.getRegistry().bind("content", new ContentExpert() {
+ @Override
+ public void validate(SemanticQuestion question) {
+ if (question == candidate) {
+ entered.countDown();
+ try {
+ assertThat(release.await(10, TimeUnit.SECONDS)).isTrue();
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(interrupted);
+ }
+ }
+ }
+ });
+ registry.replace("test", Map.of("q", original));
+ context.start();
+ var replacement = callers.submit(() -> registry.replace("test", Map.of("q", candidate)));
+ try {
+ assertThat(entered.await(10, TimeUnit.SECONDS)).isTrue();
+ assertThat(registry.get("q")).isSameAs(original);
+ } finally {
+ release.countDown();
+ }
+ replacement.get(10, TimeUnit.SECONDS);
+ assertThat(registry.get("q")).isSameAs(candidate);
+ } finally {
+ release.countDown();
+ callers.shutdownNow();
+ }
+ }
+
+ @Test
+ void invalidProgrammaticReplacementRetainsUnusedDeclarations() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("content", new ContentExpert());
+ var registry = SemanticQuestions.get(context);
+ var previous = new SemanticQuestion("detect", "content", null, Map.of());
+ registry.replace("test", Map.of("q", previous));
+ context.start();
+ assertThatThrownBy(() -> registry.replace("test", Map.of("q",
+ new SemanticQuestion("detect", "content", null, Map.of("threshold", "wrong")))))
+ .hasMessageContaining("q").hasMessageContaining("content").hasMessageContaining("threshold");
+ assertThat(registry.get("q")).isSameAs(previous);
+ assertThat(previous.getThreshold()).isNull();
+ assertThat(previous.getUncertainty()).isNull();
+ assertThat(previous.getUncertaintyPolicy()).isNull();
+ }
+ }
+
+ @Test
+ void classificationMembershipAndScalarSwitchWorkInRoutes() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("content", new ContentExpert());
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ semanticQuestions(this).expert("content")
+ .evaluation("categories").operation("classify").end()
+ .evaluation("safety").operation("safety").register();
+ from("direct:check").choice()
+ .when(simple("${semantic('categories')} contains 'privacy'"))
+ .setHeader("review", constant(true)).end()
+ .doSwitch(simple("${semantic('safety')}"))
+ .doCase("safe", "direct:deliver").otherwise("direct:review");
+ from("direct:deliver").setHeader("verdict", constant("safe"));
+ from("direct:review").setHeader("verdict", constant("unsafe"));
+ }
+ });
+ context.start();
+ try (var template = context.createProducerTemplate()) {
+ Map<String, Object> body = Map.of("prompt", "request", "response", "answer");
+ var exchange = template.request("direct:check", e -> e.getMessage().setBody(body));
+ assertThat(exchange.getException()).isNull();
+ assertThat(exchange.getMessage().getBody()).isSameAs(body);
+ assertThat(exchange.getMessage().getHeader("review")).isEqualTo(true);
+ assertThat(exchange.getMessage().getHeader("verdict")).isEqualTo("safe");
+ }
+ }
+ }
+
+ @Test
+ void providerPolicyIsAppliedOnceAndOmissionIsPreserved() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var expert = new ContentExpert();
+ context.getRegistry().bind("content", expert);
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ semanticQuestions(this).expert("content")
+ .evaluation("injection").operation("detect").parameter("threshold", 0.9).end()
+ .evaluation("defaultPolicy").operation("detect").register();
+ }
+ });
+ context.start();
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("content");
+ assertThat(context.resolveLanguage("simple").createPredicate("${semantic('injection')}").matches(exchange))
+ .isFalse();
+ assertThat(exchange.getProperty(SemanticLanguage.RESULT, SemanticResult.class).getProbability()).isEqualTo(0.8);
+ assertThat(expert.parameters).containsEntry("threshold", 0.9);
+ assertThat(context.resolveLanguage("simple").createPredicate("${!semantic('injection')}").matches(exchange))
+ .isTrue();
+ assertThat(context.resolveLanguage("simple").createPredicate("!${semantic('injection')}").matches(exchange))
+ .isTrue();
+ expert.verdict = true;
+ assertThat(context.resolveLanguage("simple").createPredicate("${semantic('injection')}").matches(exchange))
+ .isTrue();
+ expert.verdict = false;
+ assertThat(context.resolveLanguage("semantic").createPredicate("ref:defaultPolicy").matches(exchange)).isFalse();
+ expert.verdict = null;
+ assertThat(context.resolveLanguage("semantic").createPredicate("ref:defaultPolicy").matches(exchange)).isTrue();
+ assertThat(expert.parameters).isEmpty();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "failure", "malformed", "missingInput" })
+ void failuresNeverBecomeDecisionsAndClearBothResultProperties(String failure) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var expert = new ContentExpert();
+ context.getRegistry().bind("content", expert);
+ SemanticQuestions.get(context).replace("test",
+ Map.of("categories", new SemanticQuestion("classify", "content", null, Map.of())));
+ context.start();
+ var expression = context.resolveLanguage("simple").createExpression("${semantic('categories')}");
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody(Map.of("response", "answer"));
+ expression.evaluate(exchange, Object.class);
+ exchange.setProperty(SemanticLanguage.RESULTS, "old");
+ expert.failure = failure.equals("failure");
+ expert.malformed = failure.equals("malformed");
+ if (failure.equals("missingInput")) {
+ exchange.getMessage().setBody(Map.of("prompt", "private-content"));
+ }
+ assertThatThrownBy(() -> expression.evaluate(exchange, Object.class)).hasMessageNotContaining("private-content");
+ assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
+ assertThat(exchange.getProperty(SemanticLanguage.RESULTS)).isNull();
+ assertThat(expert.calls).isEqualTo(failure.equals("missingInput") ? 1 : 2);
+ }
+ }
+
+ @Test
+ void instanceCannotReplaceItsStaticContract() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("content", new ContentExpert() {
+ public SemanticCapabilities capabilities() {
+ throw new AssertionError("Runtime must read the class contract directly");
+ }
+ });
+ SemanticQuestions.get(context).replace("test",
+ Map.of("detect", new SemanticQuestion("detect", "content", null, Map.of())));
+ context.start();
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("content");
+ assertThat(context.resolveLanguage("semantic").createPredicate("ref:detect").matches(exchange)).isTrue();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void nestedSelectorsDoNotActivateOwnedProvidersDuringValidation(boolean started) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ SemanticExpertTest.ManagedExpert.initialized = 0;
+ SemanticExpertTest.ManagedExpert.started = 0;
+ var language = (SemanticLanguage) context.resolveLanguage("semantic");
+ language.setAdapter(SemanticExpertTest.ManagedExpert.class.getName());
+ context.getRegistry().bind("content", new ContentExpert());
+ if (started) {
+ context.start();
+ }
+ SemanticQuestions.get(context).replace("test", Map.of(
+ "inner", new SemanticQuestion("boolean", null, null, Map.of()),
+ "outer", new SemanticQuestion("detect", "content", "${semantic('inner')} ready", Map.of())));
+ context.start();
+ assertThat(SemanticExpertTest.ManagedExpert.initialized).isZero();
+ assertThat(SemanticExpertTest.ManagedExpert.started).isZero();
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("content");
+ assertThat(language.createExpression("ref:outer").evaluate(exchange, Boolean.class)).isTrue();
+ assertThat(SemanticExpertTest.ManagedExpert.initialized).isOne();
+ assertThat(SemanticExpertTest.ManagedExpert.started).isOne();
+ }
+ }
+
+ @Test
+ void unusedInvalidDeclarationFailsAtStartupWithoutInference() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var expert = new ContentExpert();
+ context.getRegistry().bind("content", expert);
+ SemanticQuestions.get(context).replace("test", Map.of("bad", new SemanticQuestion(
+ "detect", "content", null,
+ Map.of("threshold", "private-value"))));
+ assertThatThrownBy(context::start).hasMessageContaining("bad").hasMessageContaining("content")
+ .hasMessageContaining("threshold").hasMessageNotContaining("private-value");
+ assertThat(expert.calls).isZero();
+ }
+ }
+
+ @Test
+ void nestedStateEvaluationCannotLeaveResultsWhenOuterEvaluationFails() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("content", new ContentExpert());
+ SemanticQuestions.get(context).replace("test", Map.of(
+ "inner", new SemanticQuestion("safety", "content", null, Map.of()),
+ "outer", new SemanticQuestion("classify", "content", "${semantic('inner')}", Map.of())));
+ context.start();
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody(Map.of("response", "answer"));
+ assertThatThrownBy(() -> context.resolveLanguage("semantic").createExpression("ref:outer")
+ .evaluate(exchange, Object.class)).hasMessageContaining("outer");
+ assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
+ assertThat(exchange.getProperty(SemanticLanguage.RESULTS)).isNull();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "java", "yaml", "xml" })
+ void numericParametersPreservePrecisionAndRejectFractionalIntegers(String dsl) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("content", new ContentExpert());
+ numericDeclaration(context, dsl, "9007199254740993");
+ context.start();
+ Object value = SemanticQuestions.get(context).get("numeric").getParameters().get("limit");
+ assertThat(new BigDecimal(value.toString())).isEqualByComparingTo("9007199254740993");
+ }
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("content", new ContentExpert());
+ numericDeclaration(context, dsl, "2.0000000000000000001");
+ assertThatThrownBy(context::start).hasMessageContaining("limit").hasMessageContaining("integer");
+ }
+ }
+
+ private static void numericDeclaration(DefaultCamelContext context, String dsl, String value) throws Exception {
+ if (dsl.equals("java")) {
+ SemanticQuestions.get(context).replace("test", Map.of("numeric",
+ new SemanticQuestion("classify", "content", null, Map.of("limit", new BigDecimal(value)))));
+ } else if (dsl.equals("yaml")) {
+ yamlDeclaration(context, """
+ - semantic:
+ expert: content
+ evaluation:
+ numeric:
+ operation: classify
+ parameters:
+ limit: %s
+ """.formatted(value));
+ } else {
+ PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("numeric.xml", """
+ <semantic expert="content">
+ <evaluation name="numeric" operation="classify">
+ <parameters><parameter name="limit"><number>%s</number></parameter></parameters>
+ </evaluation>
+ </semantic>
+ """.formatted(value)));
+ }
+ }
+
+ private static void yamlDeclaration(DefaultCamelContext context, String yaml) throws Exception {
+ var settings = LoadSettings.builder().build();
+ try (var dc = new YamlDeserializationContext(settings)) {
+ dc.setCamelContext(context);
+ dc.setResource(ResourceHelper.fromString("contract.yaml", yaml));
+ dc.start();
+ dc.preParse(new Compose(settings).composeString(yaml).orElseThrow());
+ }
+ }
+
+ private static void declarations(DefaultCamelContext context, String dsl) throws Exception {
+ if (dsl.equals("java")) {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ semanticQuestions(this).expert("content").state("${body}")
+ .evaluation("categories").operation("classify")
+ .parameter("limit", 2).parameter("policy",
+ Map.of("allowed-tags", List.of("{{label:privacy}}", "unsafe"), "enabled", true, "cutoff",
+ 0.7))
+ .register();
+ }
+ });
+ } else if (dsl.equals("yaml")) {
+ String yaml = """
+ - semantic:
+ expert: content
+ state: "${body}"
+ evaluation:
+ categories:
+ operation: classify
+ parameters:
+ limit: 2
+ policy:
+ allowed-tags: ["{{label:privacy}}", unsafe]
+ enabled: true
+ cutoff: 0.7
+ """;
+ yamlDeclaration(context, yaml);
+ } else {
+ PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("contract.xml",
+ """
+ <semantic expert="content" state="${body}">
+ <evaluation name="categories" operation="classify">
+ <parameters>
+ <parameter name="limit"><number>2</number></parameter>
+ <parameter name="policy"><map>
+ <entry key="allowed-tags"><list><string>{{label:privacy}}</string><string>unsafe</string></list></entry>
+ <entry key="enabled"><boolean>true</boolean></entry>
+ <entry key="cutoff"><number>0.7</number></entry>
+ </map></parameter>
+ </parameters>
+ </evaluation>
+ </semantic>
+ """));
+ }
+ }
+
+ @SemanticExpert(name = "content", description = "Content fixture", provider = "test", artifactId = "test", operations = {
+ @SemanticOperation(name = "classify", description = "Content classification", inputTypes = InputType.STRUCTURED,
+ inputRequirements = "A map containing the assistant response",
+ resultType = ResultType.CLASSIFICATION,
+ resultMeaning = "Detected categories", labels = { "privacy", "unsafe" }, probabilities = true,
+ probabilityMeaning = "Independent label probabilities", parameters = {
+ @SemanticParameter(name = "limit", description = "Maximum labels", type = Number.class,
+ integer = true, minimum = 1, omission = "All labels"),
+ @SemanticParameter(name = "policy", description = "Provider policy", type = Map.class,
+ omission = "Service defaults") }),
+ @SemanticOperation(name = "safety", description = "Response safety", inputTypes = InputType.STRUCTURED,
+ inputRequirements = "A map containing the assistant response", resultType = ResultType.CHOICE,
+ resultMeaning = "Response safety verdict", labels = { "safe", "unsafe" }),
+ @SemanticOperation(name = "rank", description = "Response score", inputTypes = InputType.STRUCTURED,
+ inputRequirements = "A map containing the assistant response", resultType = ResultType.SCORE,
+ resultMeaning = "Response quality", minimum = 0, maximum = 1),
+ @SemanticOperation(name = "detect", description = "Injection detection", inputTypes = InputType.TEXT,
+ inputRequirements = "Text", resultType = ResultType.BOOLEAN,
+ resultMeaning = "True means injection detected",
+ probability = true, probabilityMeaning = "Probability of injection", parameters = {
+ @SemanticParameter(name = "threshold", description = "Requested threshold",
+ type = Number.class,
+ minimum = 0, maximum = 1, omission = "Use provider threshold 0.5") })
+ })
+ static class ContentExpert implements SemanticAdapter {
+ int calls;
+ Object state;
+ Map<String, Object> parameters;
+ Boolean verdict;
+ boolean failure;
+ boolean malformed;
+
+ @Override
+ public void validate(SemanticQuestion evaluation) {
+ }
+
+ @Override
+ public void validateInput(SemanticQuestion evaluation, Object selected) {
+ if (!evaluation.getOperation().equals("detect")
+ && (!(selected instanceof Map<?, ?> map) || !map.containsKey("response"))) {
+ throw new IllegalArgumentException("Selected state requires a response");
+ }
+ }
+
+ @Override
+ public SemanticResult evaluate(SemanticQuestion evaluation, Object selected) {
+ calls++;
+ state = selected;
+ parameters = evaluation.getParameters();
+ if (failure) {
+ throw new IllegalStateException("Expert unavailable");
+ }
+ if (evaluation.getOperation().equals("detect")) {
+ double threshold = ((Number) parameters.getOrDefault("threshold", 0.5)).doubleValue();
+ return new SemanticResult(verdict != null ? verdict : 0.8 >= threshold, 0.8, null, null, null);
+ }
+ if (evaluation.getOperation().equals("rank")) {
+ return new SemanticResult(0.7, null, null, null, null);
+ }
+ if (evaluation.getOperation().equals("safety")) {
+ return new SemanticResult("safe", null, null, null, null);
+ }
+ return new SemanticResult(
+ malformed ? Set.of("unknown") : Set.of("privacy"), null, Map.of("privacy", 0.8), null, null);
+ }
+ }
+}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java
index 9d56c50ff1d2..0dace6f1cda9 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java
@@ -54,7 +54,7 @@ class SemanticDeclarationDslTest {
@BeforeEach
void setup() throws Exception {
context = new DefaultCamelContext();
- context.getRegistry().bind("adapter", new SemanticAdapter() {
+ context.getRegistry().bind("adapter", new TestSemanticAdapter() {
@Override
public void validate(SemanticQuestion question) {
}
@@ -62,11 +62,12 @@ class SemanticDeclarationDslTest {
@Override
public SemanticResult evaluate(SemanticQuestion question, Object state) {
states.add(state);
- return switch (question.getType()) {
+ return applyPolicy(question, switch (question.getType()) {
case BOOLEAN -> new SemanticResult(null, 0.82, null, null, null);
case CHOICE -> new SemanticResult("billing", null, null, null, null);
case SCORE -> new SemanticResult(1.2, null, null, null, null);
- };
+ default -> throw new IllegalArgumentException("Unsupported fixture operation");
+ });
}
});
((SemanticLanguage) context.resolveLanguage("semantic")).setAdapter("adapter");
@@ -199,20 +200,21 @@ class SemanticDeclarationDslTest {
return Stream.of(
Arguments.of(question + question, "Duplicate semantic question"),
Arguments.of(question.replace("name=\"q\"", "name=\" \""), "nonblank name"),
- Arguments.of(question.replace("type=\"boolean\"", ""), "type is required"),
- Arguments.of(question.replace("boolean", "unknown"), "Invalid semantic question"),
- Arguments.of(question.replace("Valid?", " "), "instructions must not be blank"),
- Arguments.of(question.replace("type=\"boolean\"", "type=\"choice\" threshold=\"0.8\""), "require a boolean"),
+ Arguments.of(question.replace("type=\"boolean\"", ""), "operation is required"),
+ Arguments.of(question.replace("boolean", "unknown"), "Unknown operation"),
+ Arguments.of(question.replace("Valid?", " "), "Parameter 'instructions' is outside its size constraints"),
+ Arguments.of(question.replace("type=\"boolean\"", "type=\"choice\" threshold=\"0.8\""),
+ "Unknown parameter 'threshold'"),
Arguments.of(
question.replace("</question>",
"<criterion key=\"true\" value=\"A\"/><criterion key=\"true\" value=\"B\"/></question>"),
"Duplicate semantic criterion"),
- Arguments.of(question.replace("type=\"boolean\"", "type=\"score\""), "score needs ordered levels"),
- Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" threshold=\"NaN\""), "within [0,1]"),
+ Arguments.of(question.replace("type=\"boolean\"", "type=\"score\""), "Parameter 'criteria' is required"),
+ Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" threshold=\"NaN\""), "numeric constraints"),
Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" threshold=\"abc\""),
- "Invalid semantic question 'q': threshold must be a valid number: abc"),
+ "Invalid semantic question 'q': Parameter 'threshold' must be a valid number"),
Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" uncertainty=\"abc\""),
- "Invalid semantic question 'q': uncertainty must be a valid number: abc"),
+ "Invalid semantic question 'q': Parameter 'uncertainty' must be a valid number"),
Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" state=\" \""),
"state selector must not be blank"),
Arguments.of(question.replace("name=\"q\"", "unknown=\"q\""), "Unexpected attribute"));
@@ -318,8 +320,8 @@ class SemanticDeclarationDslTest {
<instructions>Urgent?</instructions>
</question></semantic>
""")))
- .hasMessageContaining("Invalid semantic question 'invalid': threshold must be a valid number")
- .hasRootCauseInstanceOf(NumberFormatException.class);
+ .hasMessageContaining("Invalid semantic question 'invalid': Parameter 'threshold' must be a valid number")
+ .hasRootCauseInstanceOf(IllegalArgumentException.class);
}
@Test
@@ -330,7 +332,7 @@ class SemanticDeclarationDslTest {
assertThatThrownBy(() -> loader.loadRoutes(List.of(
ResourceHelper.fromString("first.xml", document("routes", xmlQuestions())),
ResourceHelper.fromString("second.xml", document("routes", second.replace("boolean", "unknown"))))))
- .hasMessageContaining("Invalid semantic question");
+ .hasMessageContaining("Unknown operation");
loader.loadRoutes(List.of(
ResourceHelper.fromString("first.xml",
document("routes", xmlQuestions().replace("header.myState", "header.new"))),
@@ -429,7 +431,8 @@ class SemanticDeclarationDslTest {
}
context.addRoutes(new Questions("Urgent?"));
var previous = SemanticQuestions.get(context).get("urgent");
- assertThatThrownBy(() -> context.addRoutes(new Questions(" "))).hasMessageContaining("instructions must not be blank");
+ assertThatThrownBy(() -> context.addRoutes(new Questions(" ")))
+ .hasMessageContaining("Parameter 'instructions' is outside its size constraints");
assertThat(SemanticQuestions.get(context).get("urgent")).isSameAs(previous);
assertThat(SemanticQuestions.get(context).get("other")).isNotNull();
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEipTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEipTest.java
index 868f5e68d3b6..9b3693e329fc 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEipTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEipTest.java
@@ -43,7 +43,7 @@ class SemanticEipTest extends CamelTestSupport {
protected RouteBuilder createRouteBuilder() {
return new RouteBuilder() {
public void configure() {
- context.getRegistry().bind("classifier", new SemanticAdapter() {
+ context.getRegistry().bind("classifier", new TestSemanticAdapter() {
public void validate(SemanticQuestion question) {
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertTest.java
index 74a695671c27..7ce0bae85b0e 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertTest.java
@@ -27,7 +27,6 @@ import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.language.semantic.SemanticLanguage;
import org.apache.camel.semantic.SemanticExpert.InputType;
-import org.apache.camel.semantic.SemanticExpert.Instructions;
import org.apache.camel.semantic.SemanticExpert.ResultType;
import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.PluginHelper;
@@ -46,15 +45,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
class SemanticExpertTest {
private DefaultCamelContext context;
private SemanticLanguage language;
- private FixedExpert security;
- private FixedExpert other;
+ private FixedSemanticExpert security;
+ private FixedSemanticExpert other;
private DefaultExchange exchange;
@BeforeEach
void setup() throws Exception {
context = new DefaultCamelContext();
- security = new FixedExpert();
- other = new FixedExpert();
+ security = new FixedSemanticExpert();
+ other = new FixedSemanticExpert();
other.probability = 0.1;
context.getRegistry().bind("security", security);
context.getRegistry().bind("other", other);
@@ -92,7 +91,7 @@ class SemanticExpertTest {
@ValueSource(booleans = { false, true })
void automaticExpertErrorsRetainAStableRegistryName(boolean aliases) {
context.getRegistry().unbind("other");
- var registered = new FixedExpert() {
+ var registered = new FixedSemanticExpert() {
};
context.getRegistry().bind("security", registered);
if (aliases) {
@@ -100,11 +99,10 @@ class SemanticExpertTest {
context.getRegistry().bind("a-security", registered);
}
String name = aliases ? "a-security" : "security";
- SemanticQuestions.get(context).replace("test", Map.of("first",
- question(null, "Unsupported", Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5)));
- assertThatThrownBy(() -> language.createExpression("ref:first"))
+ assertThatThrownBy(() -> SemanticQuestions.get(context).replace("test", Map.of("first",
+ question(null, "Unsupported", Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5))))
.hasMessageContaining("evaluation 'first', expert '" + name + "'")
- .hasMessageContaining("Instructions are unsupported");
+ .hasMessageContaining("Unknown parameter 'instructions'");
define(null, null, 0.5);
var expression = language.createExpression("ref:first");
exchange.getMessage().setBody(Map.of("text", "input"));
@@ -117,9 +115,8 @@ class SemanticExpertTest {
@Test
void unknownExplicitExpertNeverFallsBackToDefaultOrClassLoading() {
language.setDefaultExpert("security");
- for (String name : List.of("missing", FixedExpert.class.getName(), "#security")) {
- define(name, "security", 0.5);
- assertThatThrownBy(() -> language.createExpression("ref:first"))
+ for (String name : List.of("missing", FixedSemanticExpert.class.getName(), "#security")) {
+ assertThatThrownBy(() -> define(name, "security", 0.5))
.hasMessageContaining("first").hasMessageContaining(name);
}
assertThat(security.calls).isZero();
@@ -128,7 +125,7 @@ class SemanticExpertTest {
@ParameterizedTest
@ValueSource(booleans = { false, true })
- void legacyAdaptersReportUnknownRatherThanUniversalCapabilities(boolean automatic) {
+ void adaptersMustDeclareTheirStaticContract(boolean automatic) {
SemanticAdapter legacy = new SemanticAdapter() {
@Override
public void validate(SemanticQuestion question) {
@@ -139,21 +136,17 @@ class SemanticExpertTest {
return new SemanticResult(true, null, null, null, null);
}
};
- assertThat(legacy.capabilities().isKnown()).isFalse();
- assertThat(legacy.capabilities().getResultTypes()).isEmpty();
+ assertThatThrownBy(() -> SemanticCapabilities.from(legacy.getClass())).hasMessageContaining("@SemanticExpert");
context.getRegistry().bind("legacy", legacy);
if (automatic) {
context.getRegistry().unbind("security");
context.getRegistry().unbind("other");
}
String reference = automatic ? null : "legacy";
- define(reference, "security", 0.5);
- assertThatThrownBy(() -> language.createExpression("ref:first"))
+ assertThatThrownBy(() -> SemanticQuestions.get(context).replace("test",
+ Map.of("first", question(reference, null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5))))
.hasMessageContaining("first").hasMessageContaining("expert 'legacy'")
- .hasMessageContaining("instructions are required");
- SemanticQuestions.get(context).replace("test", Map.of("first",
- question(reference, "Is this valid?", Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5)));
- assertThat(language.createExpression("ref:first").evaluate(exchange, Boolean.class)).isTrue();
+ .hasMessageContaining("@SemanticExpert");
}
@Test
@@ -163,8 +156,7 @@ class SemanticExpertTest {
question("security", null, Map.of("true", "valid"), SemanticQuestion.Type.BOOLEAN, 0.5),
question("security", null, Map.of("billing", "invoices"), SemanticQuestion.Type.CHOICE, 0.5));
for (SemanticQuestion question : invalid) {
- SemanticQuestions.get(context).replace("test", Map.of("first", question));
- assertThatThrownBy(() -> language.createExpression("ref:first"))
+ assertThatThrownBy(() -> SemanticQuestions.get(context).replace("test", Map.of("first", question)))
.hasMessageContaining("first").hasMessageContaining("security");
}
assertThat(security.calls).isZero();
@@ -232,7 +224,7 @@ class SemanticExpertTest {
var expression = language.createExpression("ref:first");
assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
- var replacement = new FixedExpert();
+ var replacement = new FixedSemanticExpert();
replacement.probability = 0.1;
context.getRegistry().unbind("security");
context.getRegistry().bind("security", replacement);
@@ -257,22 +249,18 @@ class SemanticExpertTest {
}
@Test
- void reloadValidatesAllInitializedReferencesAndLeavesUnusedDraftsAlone() {
+ void reloadValidatesBothInitializedReferencesAndUnusedDeclarations() {
Map<String, SemanticQuestion> definitions = new LinkedHashMap<>();
for (String name : List.of("first", "second", "third")) {
definitions.put(name, question("security", null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5));
}
- definitions.put("draft", question(null, null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5));
+ definitions.put("draft", question("security", null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5));
SemanticQuestions questions = SemanticQuestions.get(context);
questions.replace("test", definitions);
var single = language.createExpression("ref:first");
var batch = language.createExpression("refs:second,third");
questions.replace("test", definitions);
- assertThatThrownBy(() -> language.createExpression("ref:draft"))
- .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("exactly one eligible expert");
- // A failed expression must not make an unused draft mandatory on the next reload.
- questions.replace("test", definitions);
- for (String name : List.of("first", "second", "third")) {
+ for (String name : List.of("first", "second", "third", "draft")) {
Map<String, SemanticQuestion> invalid = new LinkedHashMap<>(definitions);
invalid.put(name, question("missing", null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5));
assertThatThrownBy(() -> questions.replace("test", invalid))
@@ -351,7 +339,7 @@ class SemanticExpertTest {
</semantic>
""".formatted(threshold)));
}
- }).hasStackTraceContaining("semantic question 'invalid'").hasStackTraceContaining("expert 'security'");
+ }).hasStackTraceContaining("'invalid'").hasStackTraceContaining("expert 'security'");
assertThat(security.calls).isZero();
}
}
@@ -364,7 +352,7 @@ class SemanticExpertTest {
<question name="invalid" type="boolean" expert="security" thresholdd="0.9"/>
</semantic>
""")))
- .hasStackTraceContaining("semantic question 'invalid'").hasStackTraceContaining("expert 'security'")
+ .hasStackTraceContaining("'invalid'").hasStackTraceContaining("expert 'security'")
.hasStackTraceContaining("thresholdd");
}
@@ -373,9 +361,9 @@ class SemanticExpertTest {
ManagedExpert.initialized = 0;
ManagedExpert.started = 0;
language.setAdapter(ManagedExpert.class.getName());
- SemanticQuestions.get(context).replace("test", Map.of("first",
- question(null, "Unsupported", Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5)));
- assertThatThrownBy(() -> language.createExpression("ref:first")).hasMessageContaining("Instructions are unsupported");
+ assertThatThrownBy(() -> SemanticQuestions.get(context).replace("test", Map.of("first",
+ question(null, "Unsupported", Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5))))
+ .hasMessageContaining("Unknown parameter 'instructions'");
assertThat(ManagedExpert.initialized).isZero();
assertThat(ManagedExpert.started).isZero();
define(null, null, 0.5);
@@ -384,9 +372,11 @@ class SemanticExpertTest {
assertThat(ManagedExpert.started).isOne();
}
- @SemanticExpert(name = "managed", description = "Fixed detection", provider = "test", artifactId = "test",
- inputTypes = InputType.TEXT, resultTypes = ResultType.BOOLEAN, instructions = Instructions.UNSUPPORTED,
- callerDefinedCriteria = false)
+ @SemanticExpert(name = "fixed", description = "Fixed detection", provider = "test", artifactId = "test",
+ operations = @SemanticOperation(name = "boolean", description = "Detect injection",
+ inputTypes = InputType.TEXT, inputRequirements = "A text message",
+ resultType = ResultType.BOOLEAN,
+ resultMeaning = "True means injection detected"))
public static class ManagedExpert extends ServiceSupport implements SemanticAdapter {
static int initialized;
static int started;
@@ -421,7 +411,7 @@ class SemanticExpertTest {
assertThat(DirectService.events).containsExactly("init", "start");
}
- public static class DirectService extends FixedExpert implements Service {
+ public static class DirectService extends FixedSemanticExpert implements Service {
static final List<String> events = new ArrayList<>();
@Override
@@ -443,13 +433,13 @@ class SemanticExpertTest {
@Test
void booleanOnlyExpertRejectsProbabilityPoliciesBeforeStarting() {
context.getRegistry().bind("label", new ManagedExpert());
- define("label", "security", 0.8);
- assertThatThrownBy(() -> language.createExpression("ref:first"))
- .hasMessageContaining("first").hasMessageContaining("label").hasMessageContaining("probability support");
+ assertThatThrownBy(() -> define("label", "security", 0.8))
+ .hasMessageContaining("first").hasMessageContaining("label")
+ .hasMessageContaining("Unknown parameter 'threshold'");
}
@Test
- void instructionDrivenAndFixedExpertsShareOneBatch() {
+ void instructionDrivenAndFixedSemanticExpertsShareOneBatch() {
context.getRegistry().bind("general", new SemanticLanguageTest.LabelAdapter());
SemanticQuestions.get(context).replace("test", Map.of(
"injection", question("security", null, Map.of(), SemanticQuestion.Type.BOOLEAN, 0.5),
@@ -469,52 +459,12 @@ class SemanticExpertTest {
private static SemanticQuestion question(
String expert, String instructions, Map<String, String> criteria,
SemanticQuestion.Type type, double threshold) {
+ if (type == SemanticQuestion.Type.BOOLEAN && instructions == null && criteria.isEmpty() && threshold == 0.5) {
+ return new SemanticQuestion("boolean", expert, null, Map.of());
+ }
return new SemanticQuestion(
type, instructions, null, criteria, List.of(), threshold, 0,
SemanticQuestion.UncertaintyPolicy.FAIL, expert);
}
- @SemanticExpert(name = "fixed", description = "Prompt-injection detection", provider = "test", artifactId = "test",
- inputTypes = InputType.TEXT, resultTypes = ResultType.BOOLEAN, instructions = Instructions.UNSUPPORTED,
- callerDefinedCriteria = false, booleanProbability = true, trueMeaning = "Injection detected",
- probabilityMeaning = "Probability of INJECTION, even when BENIGN wins")
- public static class FixedExpert implements SemanticAdapter {
- double probability = 0.9;
- boolean fail;
- int calls;
- final List<List<String>> batches = new ArrayList<>();
- final List<Object> states = new ArrayList<>();
-
- @Override
- public void validate(SemanticQuestion question) {
- capabilities().validate(question);
- }
-
- @Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
- calls++;
- states.add(state);
- if (fail) {
- throw new IllegalStateException("provider unavailable");
- }
- return new SemanticResult(null, probability, null, null, Map.of("provider", "fixed"));
- }
-
- @Override
- public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticQuestion> questions, Object state)
- throws Exception {
- batches.add(List.copyOf(questions.keySet()));
- return SemanticAdapter.super.evaluateBatch(questions, state);
- }
-
- @Override
- public boolean equals(Object other) {
- return other instanceof FixedExpert;
- }
-
- @Override
- public int hashCode() {
- return 1;
- }
- }
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertYamlTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertYamlTest.java
index f5923f097912..48c2357e0f0a 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertYamlTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertYamlTest.java
@@ -19,9 +19,6 @@ package org.apache.camel.semantic;
import org.apache.camel.dsl.yaml.common.YamlDeserializationContext;
import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
import org.apache.camel.impl.DefaultCamelContext;
-import org.apache.camel.semantic.SemanticExpert.InputType;
-import org.apache.camel.semantic.SemanticExpert.Instructions;
-import org.apache.camel.semantic.SemanticExpert.ResultType;
import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.ResourceHelper;
import org.junit.jupiter.api.Test;
@@ -74,7 +71,7 @@ class SemanticExpertYamlTest {
DECLARATIONS.replace("type: boolean", "type: boolean\n instructions: unsupported")))
.isInstanceOfSatisfying(YamlDeserializationException.class, error -> {
assertThat(error).hasMessageContaining("injection").hasMessageContaining("security")
- .hasMessageContaining("Instructions are unsupported");
+ .hasMessageContaining("Unknown parameter 'instructions'");
assertThat(error.getProblemMark()).hasValueSatisfying(mark -> {
assertThat(mark.getName()).isEqualTo("questions.yaml");
assertThat(mark.getLine()).isZero();
@@ -101,9 +98,11 @@ class SemanticExpertYamlTest {
yaml = yaml.replace(" type: boolean\n", "");
}
String declaration = yaml;
+ context.getRegistry().bind("security", new FixedExpert());
+ context.start();
assertThatThrownBy(() -> preParse(context, declaration))
.isInstanceOf(YamlDeserializationException.class)
- .hasMessageContaining("semantic question 'injection'").hasMessageContaining("expert 'security'");
+ .hasMessageContaining("injection").hasMessageContaining("expert 'security'");
}
}
@@ -128,16 +127,27 @@ class SemanticExpertYamlTest {
}
@Test
- void unusedDeclarationsDoNotMakeAnUnchangedReloadFail() throws Exception {
+ void malformedBlockExpertAndUnknownKeysHaveUsefulContext() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ assertThatThrownBy(() -> preParse(context,
+ "- semantic: {expert: [invalid], evaluation: {q: {operation: detect}}}"))
+ .isInstanceOf(YamlDeserializationException.class).hasMessageContaining("'q'")
+ .hasMessageContaining("invalid expert reference");
+ assertThatThrownBy(() -> preParse(context,
+ "- semantic: {expertt: security, evaluation: {q: {operation: detect}}}"))
+ .isInstanceOf(YamlDeserializationException.class).hasMessageContaining("Unknown property 'expertt'");
+ }
+ }
+
+ @Test
+ void unusedAmbiguousDeclarationsFailAtStartup() throws Exception {
try (var context = new DefaultCamelContext()) {
context.getRegistry().bind("security", new FixedExpert());
context.getRegistry().bind("other", new FixedExpert());
String declarations = DECLARATIONS + " draft: {type: boolean}\n";
preParse(context, declarations);
- context.start();
- context.resolveLanguage("semantic").createExpression("ref:injection");
- preParse(context, declarations);
- assertThat(SemanticQuestions.get(context).get("draft").getExpert()).isNull();
+ assertThatThrownBy(context::start).hasMessageContaining("draft")
+ .hasMessageContaining("exactly one eligible expert");
}
}
@@ -151,20 +161,9 @@ class SemanticExpertYamlTest {
}
}
- @SemanticExpert(name = "fixed", description = "Fixed test expert", provider = "test", artifactId = "test",
- inputTypes = InputType.TEXT, resultTypes = ResultType.BOOLEAN,
- instructions = Instructions.UNSUPPORTED, callerDefinedCriteria = false, booleanProbability = true)
- private static class FixedExpert implements SemanticAdapter {
- private int calls;
-
- @Override
- public void validate(SemanticQuestion question) {
- }
-
- @Override
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
- calls++;
- return new SemanticResult(null, 0.8, null, null, null);
+ private static class FixedExpert extends FixedSemanticExpert {
+ private FixedExpert() {
+ probability = 0.8;
}
}
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
index eccd55d9fe4a..65cb98cfc01a 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
@@ -29,7 +29,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.language.semantic.SemanticLanguage;
-import org.apache.camel.support.service.ServiceSupport;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -39,7 +38,7 @@ class SemanticInitializationTest {
void initializationDoesNotAcquirePublicContextOrRegistryMonitors() throws Exception {
ExecutorService callers = Executors.newSingleThreadExecutor();
try (var context = new DefaultCamelContext()) {
- context.start();
+ context.init();
synchronized (context) {
var questions = callers.submit(() -> SemanticQuestions.get(context));
assertThat(questions.get(10, TimeUnit.SECONDS)).isSameAs(SemanticQuestions.get(context));
@@ -85,7 +84,7 @@ class SemanticInitializationTest {
CountingAdapter.started.set(0);
CountingAdapter.stopped.set(0);
try (var context = new DefaultCamelContext()) {
- context.start();
+ context.init();
var first = language(context, CountingAdapter.class);
var second = language(context, CountingAdapter.class);
List<Future<String>> results = new ArrayList<>();
@@ -120,8 +119,8 @@ class SemanticInitializationTest {
BlockingAdapter.entered = new CountDownLatch(1);
BlockingAdapter.release = new CountDownLatch(1);
try (var first = new DefaultCamelContext(); var second = new DefaultCamelContext()) {
- first.start();
- second.start();
+ first.init();
+ second.init();
var blocked = language(first, BlockingAdapter.class);
var independent = language(second, Adapter.class);
Future<?> creation = callers.submit(() -> blocked.createExpression("ref:q"));
@@ -150,7 +149,7 @@ class SemanticInitializationTest {
return language;
}
- public static class Adapter extends ServiceSupport implements SemanticAdapter {
+ public static class Adapter extends TestSemanticAdapter {
@Override
public void validate(SemanticQuestion question) {
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java
index 4bfbc9467f15..88671131668c 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java
@@ -42,7 +42,6 @@ import org.apache.camel.impl.engine.DefaultInjector;
import org.apache.camel.language.semantic.SemanticLanguage;
import org.apache.camel.spi.FactoryFinder;
import org.apache.camel.support.DefaultExchange;
-import org.apache.camel.support.service.ServiceSupport;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -69,7 +68,7 @@ class SemanticLanguageTest {
language.setCamelContext(context);
language.setAdapter(CountingAdapter.class.getName());
context.getRegistry().bind("semantic", language);
- context.start();
+ context.init();
questions(question(SemanticQuestion.Type.BOOLEAN, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL));
}
@@ -424,7 +423,8 @@ class SemanticLanguageTest {
@ParameterizedTest
@ValueSource(strings = { "${header.broken", "{{missing.selector}}" })
- void reloadRejectsInvalidStateSelectorsBeforePublication(String selector) {
+ void reloadRejectsInvalidStateSelectorsBeforePublication(String selector) throws Exception {
+ context.start();
Expression expression = language.createExpression("ref:q");
SemanticQuestion previous = SemanticQuestions.get(context).get("q");
assertThatThrownBy(() -> questions(
@@ -495,7 +495,7 @@ class SemanticLanguageTest {
assertThat(expression.evaluate(exchange, String.class)).isEqualTo("billing");
}
- public static class CountingAdapter extends ServiceSupport implements SemanticAdapter {
+ public static class CountingAdapter extends TestSemanticAdapter {
static final AtomicInteger constructed = new AtomicInteger();
static final AtomicInteger started = new AtomicInteger();
static final AtomicInteger stopped = new AtomicInteger();
@@ -517,7 +517,7 @@ class SemanticLanguageTest {
if (failure != null) {
throw failure;
}
- return answer;
+ return applyPolicy(question, answer);
}
@Override
@@ -554,7 +554,7 @@ class SemanticLanguageTest {
}
}
- public static class LabelAdapter implements SemanticAdapter {
+ public static class LabelAdapter extends TestSemanticAdapter {
@Override
public void validate(SemanticQuestion question) {
if (question.getType() != SemanticQuestion.Type.CHOICE) {
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticQuestionBuilderTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticQuestionBuilderTest.java
new file mode 100644
index 000000000000..60b9c985c19e
--- /dev/null
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticQuestionBuilderTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.semantic;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.ResourceHelper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class SemanticQuestionBuilderTest {
+ static Stream<Consumer<SemanticQuestionBuilder>> conflicts() {
+ return Stream.of(
+ b -> b.criterion("a", "A").level("B"),
+ b -> b.level("B").criterion("a", "A"),
+ b -> b.parameter("criteria", Map.of("a", "A")).criterion("b", "B"),
+ b -> b.criterion("b", "B").parameter("criteria", Map.of("a", "A")),
+ b -> b.parameter("criteria", Map.of("a", "A")).level("B"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("conflicts")
+ void criteriaConflictsHaveDeclarationContext(Consumer<SemanticQuestionBuilder> configure) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ assertThatThrownBy(() -> context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ var question = semanticQuestions(this).expert("selected").evaluation("q").operation("choice");
+ configure.accept(question);
+ question.register();
+ }
+ })).hasMessageContaining("'q'").hasMessageContaining("expert 'selected'")
+ .hasMessageContaining("Duplicate parameter 'criteria'");
+ }
+ }
+
+ @Test
+ void callerCriteriaAreNeverMutated() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ Map<String, String> supplied = new LinkedHashMap<>(Map.of("a", "A"));
+ var builder = new SemanticQuestionBuilder().operation("choice").parameter("criteria", supplied);
+ builder.criterion("b", "B");
+ assertThatThrownBy(() -> builder.build(context)).hasMessageContaining("Duplicate parameter 'criteria'");
+ assertThat(supplied).containsExactlyEntriesOf(Map.of("a", "A"));
+ }
+ }
+
+ @Test
+ void xmlCriteriaConflictsAndStructuralErrorsNameTheBlockExpert() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ for (String children : new String[] {
+ "<criterion key=\"a\" value=\"A\"/><level>B</level>",
+ "<parameters><parameter name=\"criteria\"><list><string>A</string></list></parameter></parameters><level>B</level>",
+ "<unknown/>" }) {
+ assertThatThrownBy(() -> PluginHelper.getRoutesLoader(context)
+ .loadRoutes(ResourceHelper.fromString("invalid.xml",
+ "<semantic expert=\"selected\"><evaluation name=\"q\" operation=\"choice\">" + children
+ + "</evaluation></semantic>")))
+ .hasStackTraceContaining("'q'").hasStackTraceContaining("expert 'selected'");
+ }
+ }
+ }
+
+ @Test
+ void policyPlaceholdersResolveBeforeShorthandNormalization() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var properties = new Properties();
+ properties.setProperty("myPolicy", "NON_MATCH");
+ context.getPropertiesComponent().setInitialProperties(properties);
+ var shorthand = new SemanticQuestionBuilder().operation("boolean").uncertaintyPolicy("{{myPolicy}}")
+ .build(context);
+ var generic = new SemanticQuestionBuilder().operation("custom").parameter("uncertaintyPolicy", "{{myPolicy}}")
+ .build(context);
+ assertThat(shorthand.getParameters()).containsEntry("uncertaintyPolicy", "non-match");
+ assertThat(generic.getParameters()).containsEntry("uncertaintyPolicy", "NON_MATCH");
+ }
+ }
+
+ @Test
+ void nullPolicyIsRejectedWithContractContext() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("fixed", new FixedSemanticExpert());
+ context.start();
+ assertThatThrownBy(() -> context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ semanticQuestions(this).expert("fixed").evaluation("q").operation("boolean")
+ .uncertaintyPolicy(null).register();
+ }
+ })).hasMessageContaining("'q'").hasMessageContaining("'fixed'")
+ .hasMessageContaining("uncertaintyPolicy").hasMessageContaining("must not be null");
+ }
+ }
+}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticResultTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticResultTest.java
index 8d1536aae935..24b31a74ce2d 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticResultTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticResultTest.java
@@ -16,7 +16,9 @@
*/
package org.apache.camel.semantic;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
import org.junit.jupiter.api.Test;
@@ -25,29 +27,34 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
class SemanticResultTest {
@Test
- void booleanOnlyProviderCannotSilentlyIgnoreProbabilityPolicy() {
- SemanticResult result = new SemanticResult(true, null, null, null, null);
- SemanticQuestion defaults = SemanticLanguageTest.question(SemanticQuestion.Type.BOOLEAN, null, 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL);
- assertThat(result.decision(defaults)).isEqualTo(true);
- SemanticQuestion threshold = SemanticLanguageTest.question(SemanticQuestion.Type.BOOLEAN, null, 0.7, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL);
- assertThatThrownBy(() -> result.decision(threshold)).isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("decision policy");
- SemanticQuestion uncertain = SemanticLanguageTest.question(SemanticQuestion.Type.BOOLEAN, null, 0.5, 0.1,
- SemanticQuestion.UncertaintyPolicy.NON_MATCH);
- assertThatThrownBy(() -> result.decision(uncertain)).isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("decision policy");
+ void classificationLabelsAreImmutableAndConfidenceIsNotInvented() {
+ Set<String> labels = new HashSet<>(Set.of("privacy"));
+ SemanticResult result = new SemanticResult(labels, null, null, null, null);
+ labels.clear();
+ assertThat(result.getValue()).isEqualTo(Set.of("privacy"));
+ assertThatThrownBy(() -> ((Set<?>) result.getValue()).clear()).isInstanceOf(UnsupportedOperationException.class);
+ assertThat(result.getProbability()).isNull();
+ assertThat(result.getProbabilities()).isEmpty();
+ assertThat(result.getConfidence()).isNull();
}
@Test
- void choiceProbabilitiesMustCoverExactlyTheDeclaredCriteria() {
- SemanticQuestion question = SemanticLanguageTest.question(SemanticQuestion.Type.CHOICE, null, 0.5, 0,
- SemanticQuestion.UncertaintyPolicy.FAIL);
- SemanticResult incomplete = new SemanticResult("billing", null, Map.of("billing", 0.8), null, null);
- assertThatThrownBy(() -> incomplete.decision(question)).isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("cover every criterion");
- SemanticResult complete = new SemanticResult("billing", null, Map.of("billing", 0.8, "technical", 0.2), null, null);
- assertThat(complete.decision(question)).isEqualTo("billing");
+ void nullClassificationLabelHasAnActionableError() {
+ Set<String> labels = new HashSet<>();
+ labels.add(null);
+ assertThatThrownBy(() -> new SemanticResult(labels, null, null, null, null))
+ .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("labels must not be null");
+ }
+
+ @Test
+ void malformedProbabilityAndConfidenceAreErrors() {
+ for (double invalid : new double[] { -0.1, 1.1, Double.NaN, Double.POSITIVE_INFINITY }) {
+ assertThatThrownBy(() -> new SemanticResult(true, invalid, null, null, null))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> new SemanticResult(true, null, null, invalid, null))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> new SemanticResult(Set.of(), null, Map.of("privacy", invalid), null, null))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
}
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
index a5abe7d6bd71..aa9f57f60989 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
@@ -52,7 +52,7 @@ class SemanticRetryAndValidationTest extends CamelTestSupport {
return new RouteBuilder() {
@Override
public void configure() {
- context.getRegistry().bind("evaluator", new SemanticAdapter() {
+ context.getRegistry().bind("evaluator", new TestSemanticAdapter() {
@Override
public void validate(SemanticQuestion question) {
}
@@ -65,7 +65,7 @@ class SemanticRetryAndValidationTest extends CamelTestSupport {
}
return malformedResult
? new SemanticResult("unexpected category", null, null, null, null)
- : new SemanticResult(null, probability, null, null, null);
+ : applyPolicy(question, new SemanticResult(null, probability, null, null, null));
}
});
SemanticLanguage language = (SemanticLanguage) context.resolveLanguage("semantic");
@@ -274,7 +274,7 @@ class SemanticRetryAndValidationTest extends CamelTestSupport {
case "timeout" -> assertThat(ObjectHelper.getException(TimeoutException.class, exception))
.hasMessage("Evaluation timed out");
case "malformed" -> assertThat(ObjectHelper.getException(IllegalArgumentException.class, exception))
- .hasMessage("Semantic result does not support the question and its decision policy");
+ .hasMessage("Semantic result does not match BOOLEAN contract");
case "uncertain" -> assertThat(ObjectHelper.getException(IllegalStateException.class, exception))
.hasMessage("Semantic boolean decision is uncertain");
default -> throw new IllegalArgumentException(failure);
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java
index d8601e2b562d..d39915fe5d9d 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java
@@ -169,6 +169,7 @@ class SemanticXmlAutoDiscoveryTest {
delegate.set(loader);
}
});
+ context.getRegistry().bind("fixture", new SemanticLanguageTest.CountingAdapter());
context.start();
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("ordinary.xml", """
<camel xmlns="http://camel.apache.org/schema/xml-io">
@@ -228,6 +229,7 @@ class SemanticXmlAutoDiscoveryTest {
@Test
void contextWithApplicationRegistryDiscoversXmlDeclarations() throws Exception {
try (var context = new DefaultCamelContext(new SimpleRegistry())) {
+ context.getRegistry().bind("fixture", new SemanticLanguageTest.CountingAdapter());
context.start();
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("questions.xml", """
<semantic><question name="urgent" type="boolean"><instructions>Urgent?</instructions></question></semantic>
@@ -240,6 +242,7 @@ class SemanticXmlAutoDiscoveryTest {
void contextRestartReinstallsAutomaticLoader() throws Exception {
try (var context = new DefaultCamelContext()) {
for (int i = 0; i < 2; i++) {
+ context.getRegistry().bind("fixture", new SemanticLanguageTest.CountingAdapter());
context.start();
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("questions.xml",
"""
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java
index 2829377894d7..84cdde3408b7 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java
@@ -46,6 +46,7 @@ class SemanticXmlLoaderTest {
}
};
try (var context = new DefaultCamelContext(registry)) {
+ context.getRegistry().bind("fixture", new SemanticLanguageTest.CountingAdapter());
context.start();
SemanticXmlLoader loader = registry.lookupByNameAndType(SemanticXmlLoader.REGISTRY_KEY, SemanticXmlLoader.class);
lookups.set(0);
@@ -79,6 +80,7 @@ class SemanticXmlLoaderTest {
XmlRoutesBuilderLoader custom = new XmlRoutesBuilderLoader();
context.getRegistry().bind("customXml", custom);
+ context.getRegistry().bind("fixture", new SemanticLanguageTest.CountingAdapter());
context.start();
assertThat(loader.isSupportedExtension("xml")).isFalse();
@@ -109,6 +111,7 @@ class SemanticXmlLoaderTest {
""" : "");
try (var context = new DefaultCamelContext()) {
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("nested.routes.xml", routes));
+ context.getRegistry().bind("fixture", new SemanticLanguageTest.CountingAdapter());
context.start();
assertThat(context.getRouteDefinitions()).hasSize(2);
if (declarations) {
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/TestSemanticAdapter.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/TestSemanticAdapter.java
new file mode 100644
index 000000000000..910b589519f8
--- /dev/null
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/TestSemanticAdapter.java
@@ -0,0 +1,110 @@
+/*
+ * 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.semantic;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.semantic.SemanticExpert.InputType;
+import org.apache.camel.semantic.SemanticExpert.ResultType;
+import org.apache.camel.support.service.ServiceSupport;
+
+@SemanticExpert(name = "fixture", provider = "test", artifactId = "test",
+ description = "Deterministic instruction-driven test expert",
+ operations = {
+ @SemanticOperation(name = "boolean", description = "Instruction-driven boolean evaluation",
+ inputTypes = { InputType.TEXT, InputType.STRUCTURED },
+ inputRequirements = "Text or structured application state",
+ resultType = ResultType.BOOLEAN,
+ resultMeaning = "True means the supplied instructions and criteria hold",
+ probability = true,
+ probabilityMeaning = "Probability that the supplied instructions and criteria hold",
+ confidence = true,
+ confidenceMeaning = "Optional confidence reported by the provider",
+ parameters = {
+ @SemanticParameter(name = "instructions",
+ description = "Instructions to evaluate against the state",
+ required = true, minSize = 1),
+ @SemanticParameter(name = "criteria",
+ description = "Descriptions of true and false",
+ type = Map.class, itemType = String.class,
+ omission = "Use the instructions"),
+ @SemanticParameter(name = "threshold",
+ description = "Inclusive positive probability threshold",
+ type = Number.class, minimum = 0, maximum = 1,
+ omission = "Use 0.5"),
+ @SemanticParameter(name = "uncertainty",
+ description = "Half-width of the uncertainty band around the threshold",
+ type = Number.class, minimum = 0, maximum = 1,
+ omission = "No uncertainty band"),
+ @SemanticParameter(name = "uncertaintyPolicy",
+ description = "Behaviour inside the uncertainty band",
+ values = { "fail", "non-match" },
+ omission = "Fail the evaluation") }),
+ @SemanticOperation(name = "choice", description = "Instruction-driven choice evaluation",
+ inputTypes = { InputType.TEXT, InputType.STRUCTURED },
+ inputRequirements = "Text or structured application state",
+ resultType = ResultType.CHOICE, resultMeaning = "One of the supplied category keys",
+ probabilities = true, probabilityMeaning = "Probability of each supplied category",
+ confidence = true,
+ confidenceMeaning = "Optional confidence reported by the provider",
+ parameters = {
+ @SemanticParameter(name = "instructions",
+ description = "Instructions to evaluate against the state",
+ required = true, minSize = 1),
+ @SemanticParameter(name = "criteria", description = "Named categories",
+ type = Map.class, itemType = String.class,
+ required = true, minSize = 1, maxSize = 255) }),
+ @SemanticOperation(name = "score", description = "Instruction-driven score evaluation",
+ inputTypes = { InputType.TEXT, InputType.STRUCTURED },
+ inputRequirements = "Text or structured application state",
+ resultType = ResultType.SCORE,
+ resultMeaning = "A score from zero to the number of supplied levels minus one",
+ minimum = 0, maximum = 9,
+ confidence = true,
+ confidenceMeaning = "Optional confidence reported by the provider",
+ parameters = {
+ @SemanticParameter(name = "instructions",
+ description = "Instructions to evaluate against the state",
+ required = true, minSize = 1),
+ @SemanticParameter(name = "criteria", description = "Ordered score levels",
+ type = List.class, itemType = String.class,
+ required = true, minSize = 1, maxSize = 10) }) })
+abstract class TestSemanticAdapter extends ServiceSupport implements SemanticAdapter {
+ @Override
+ public void validate(SemanticQuestion question) {
+ SemanticCapabilities.from(getClass()).validate(question);
+ }
+
+ // This fixture models an expert that owns its probability policy.
+ static SemanticResult applyPolicy(SemanticQuestion question, SemanticResult result) {
+ if (question.getType() != SemanticQuestion.Type.BOOLEAN || result.getProbability() == null) {
+ return result;
+ }
+ double probability = result.getProbability();
+ double threshold = ((Number) question.getParameters().getOrDefault("threshold", 0.5)).doubleValue();
+ double uncertainty = ((Number) question.getParameters().getOrDefault("uncertainty", 0.0)).doubleValue();
+ boolean value = probability >= threshold;
+ if (uncertainty > 0 && probability >= threshold - uncertainty && probability <= threshold + uncertainty) {
+ if (!"non-match".equals(question.getParameters().get("uncertaintyPolicy"))) {
+ throw new IllegalStateException("Semantic boolean decision is uncertain");
+ }
+ value = false;
+ }
+ return new SemanticResult(value, probability, result.getProbabilities(), result.getConfidence(), result.getMetadata());
+ }
+}
diff --git a/components/camel-ai/camel-typesafe-ai/src/main/docs/typesafe-ai-component.adoc b/components/camel-ai/camel-typesafe-ai/src/main/docs/typesafe-ai-component.adoc
index 942b64630924..9504107aebe2 100644
--- a/components/camel-ai/camel-typesafe-ai/src/main/docs/typesafe-ai-component.adoc
+++ b/components/camel-ai/camel-typesafe-ai/src/main/docs/typesafe-ai-component.adoc
@@ -21,6 +21,12 @@ It calls the service directly using the JDK HTTP client and Camel JSON utilities
For semantic conditions in Java, XML and YAML, use the xref:languages:typesafe-ai-language.adoc[TypeSafe AI language] supplied by this component.
This component also advertises an adapter for the xref:languages:semantic-language.adoc[Semantic language].
+Its static expert contract exposes `boolean`, `choice` and `score` operations. The adapter requires
+instructions and validates the supplied criteria or score levels before transport is initialized.
+For Boolean evaluations it applies the requested `threshold`, `uncertainty` and `uncertaintyPolicy`
+once and returns both the typed verdict and the provider probability. Omitted policy parameters
+use this adapter's defaults: threshold `0.5`, no uncertainty band, and failure for uncertain results.
+The semantic runtime validates the result and does not apply a second policy.
Named boolean, choice and score questions use the common contract and inherit this component's
credentials, model and transport settings without requiring a TypeSafe AI endpoint in the route.
diff --git a/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java b/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java
index 67c3df0e8cf0..f7df36b14063 100644
--- a/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java
+++ b/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java
@@ -18,15 +18,18 @@ package org.apache.camel.component.typesafeai;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.semantic.SemanticAdapter;
+import org.apache.camel.semantic.SemanticCapabilities;
import org.apache.camel.semantic.SemanticExpert;
import org.apache.camel.semantic.SemanticExpert.InputType;
-import org.apache.camel.semantic.SemanticExpert.Instructions;
import org.apache.camel.semantic.SemanticExpert.ResultType;
+import org.apache.camel.semantic.SemanticOperation;
+import org.apache.camel.semantic.SemanticParameter;
import org.apache.camel.semantic.SemanticQuestion;
import org.apache.camel.semantic.SemanticResult;
import org.apache.camel.spi.annotations.JdkService;
@@ -35,16 +38,65 @@ import org.apache.camel.util.json.JsonObject;
/** Maps common questions to TypeSafe AI using the component's configured, managed transport. */
@JdkService("semantic-adapter")
@SemanticExpert(name = "typesafe-ai", provider = "typesafe-ai", artifactId = "camel-typesafe-ai",
- description = "Instruction-driven boolean decisions, choices and rubric scores using TypeSafe AI",
- inputTypes = { InputType.TEXT, InputType.STRUCTURED },
- resultTypes = { ResultType.BOOLEAN, ResultType.CHOICE, ResultType.SCORE },
- instructions = Instructions.REQUIRED, callerDefinedCriteria = true,
- booleanProbability = true, choiceProbabilities = true,
- confidenceTypes = { ResultType.BOOLEAN, ResultType.CHOICE, ResultType.SCORE },
- probabilityMeaning = "Boolean: probability of true; choice: probability of each supplied category",
- confidenceMeaning = "Optional provider-reported confidence; distinct from the normalized decision",
- trueMeaning = "The supplied boolean instructions and criteria hold for the selected state",
- maxChoices = 255, maxScoreLevels = 10)
+ description = "Instruction-driven decisions using TypeSafe AI",
+ operations = {
+ @SemanticOperation(name = "boolean", description = "Instruction-driven boolean evaluation",
+ inputTypes = { InputType.TEXT, InputType.STRUCTURED },
+ inputRequirements = "Text or structured application state",
+ resultType = ResultType.BOOLEAN,
+ resultMeaning = "True means the supplied instructions and criteria hold",
+ probability = true,
+ probabilityMeaning = "Probability that the supplied instructions and criteria hold",
+ parameters = {
+ @SemanticParameter(name = "instructions",
+ description = "Instructions to evaluate against the state",
+ required = true, minSize = 1),
+ @SemanticParameter(name = "criteria",
+ description = "Descriptions of true and false",
+ type = Map.class, itemType = String.class,
+ omission = "Use the instructions"),
+ @SemanticParameter(name = "threshold",
+ description = "Inclusive positive probability threshold",
+ type = Number.class, minimum = 0, maximum = 1,
+ omission = "Use 0.5"),
+ @SemanticParameter(name = "uncertainty",
+ description = "Half-width of the uncertainty band around the threshold",
+ type = Number.class, minimum = 0, maximum = 1,
+ omission = "No uncertainty band"),
+ @SemanticParameter(name = "uncertaintyPolicy",
+ description = "Behaviour inside the uncertainty band",
+ values = { "fail", "non-match" },
+ omission = "Fail the evaluation") }),
+ @SemanticOperation(name = "choice", description = "Instruction-driven choice evaluation",
+ inputTypes = { InputType.TEXT, InputType.STRUCTURED },
+ inputRequirements = "Text or structured application state",
+ resultType = ResultType.CHOICE, resultMeaning = "One of the supplied category keys",
+ probabilities = true, probabilityMeaning = "Probability of each supplied category",
+ confidence = true,
+ confidenceMeaning = "Optional confidence reported by the provider",
+ parameters = {
+ @SemanticParameter(name = "instructions",
+ description = "Instructions to evaluate against the state",
+ required = true, minSize = 1),
+ @SemanticParameter(name = "criteria", description = "Named categories",
+ type = Map.class, itemType = String.class,
+ required = true, minSize = 1, maxSize = 255) }),
+ @SemanticOperation(name = "score", description = "Instruction-driven score evaluation",
+ inputTypes = { InputType.TEXT, InputType.STRUCTURED },
+ inputRequirements = "Text or structured application state",
+ resultType = ResultType.SCORE,
+ resultMeaning = "A score from zero to the number of supplied levels minus one",
+ minimum = 0, maximum = 9, probabilities = true,
+ probabilityMeaning = "Probability of each supplied score level",
+ confidence = true,
+ confidenceMeaning = "Optional confidence reported by the provider",
+ parameters = {
+ @SemanticParameter(name = "instructions",
+ description = "Instructions to evaluate against the state",
+ required = true, minSize = 1),
+ @SemanticParameter(name = "criteria", description = "Ordered score levels",
+ type = List.class, itemType = String.class,
+ required = true, minSize = 1, maxSize = 10) }) })
public class TypeSafeAiSemanticAdapter implements SemanticAdapter, CamelContextAware {
private CamelContext camelContext;
private volatile TypeSafeAiEndpoint endpoint;
@@ -61,7 +113,21 @@ public class TypeSafeAiSemanticAdapter implements SemanticAdapter, CamelContextA
@Override
public void validate(SemanticQuestion question) {
- capabilities().validate(question);
+ SemanticCapabilities.from(getClass()).validate(question);
+ if (question.getCriteria().entrySet().stream().anyMatch(entry -> entry.getKey().isBlank() || entry.getValue().isBlank())
+ || question.getLevels().stream().anyMatch(String::isBlank)) {
+ throw new IllegalArgumentException("Parameter 'criteria' requires nonblank names and descriptions");
+ }
+ if (question.getType() == SemanticQuestion.Type.BOOLEAN) {
+ if (question.getCriteria().keySet().stream().anyMatch(key -> !key.equals("true") && !key.equals("false"))) {
+ throw new IllegalArgumentException("Parameter 'criteria' requires true or false keys");
+ }
+ double threshold = threshold(question);
+ double uncertainty = uncertainty(question);
+ if (threshold - uncertainty < 0 || threshold + uncertainty > 1) {
+ throw new IllegalArgumentException("Parameters 'threshold' and 'uncertainty' must keep the band within [0,1]");
+ }
+ }
}
private TypeSafeAiEndpoint endpoint() {
@@ -112,6 +178,7 @@ public class TypeSafeAiSemanticAdapter implements SemanticAdapter, CamelContextA
case BOOLEAN -> "noul";
case CHOICE -> "choice";
case SCORE -> "score";
+ default -> throw new IllegalArgumentException("Unsupported TypeSafe AI operation");
};
}
@@ -120,10 +187,47 @@ public class TypeSafeAiSemanticAdapter implements SemanticAdapter, CamelContextA
if (answer.get("probabilities") instanceof Map<?, ?> values) {
values.forEach((key, value) -> probabilities.put((String) key, ((Number) value).doubleValue()));
}
- return new SemanticResult(
- question.getType() == SemanticQuestion.Type.BOOLEAN ? null : answer.get(type(question)),
- question.getType() == SemanticQuestion.Type.BOOLEAN ? answer.getDouble("noul") : null,
+ Double probability = question.getType() == SemanticQuestion.Type.BOOLEAN ? answer.getDouble("noul") : null;
+ Object value = switch (question.getType()) {
+ case BOOLEAN -> booleanDecision(question, probability);
+ case SCORE -> answer.getDouble("score");
+ default -> answer.get(type(question));
+ };
+ SemanticResult result = new SemanticResult(
+ value,
+ probability,
probabilities, answer.get("confidence") == null ? null : answer.getDouble("confidence"),
Map.of("provider", "typesafe-ai", "model", response.get("model"), "usage", response.get("usage")));
+ SemanticCapabilities.from(getClass()).operation(question.getOperation()).validateResult(result);
+ if (question.getType() == SemanticQuestion.Type.CHOICE
+ && (!question.getCriteria().containsKey(value)
+ || !probabilities.isEmpty() && !probabilities.keySet().equals(question.getCriteria().keySet()))) {
+ throw new IllegalArgumentException("TypeSafe AI choice result must match the supplied criteria");
+ }
+ if (question.getType() == SemanticQuestion.Type.SCORE
+ && ((Number) value).doubleValue() > question.getLevels().size() - 1) {
+ throw new IllegalArgumentException("TypeSafe AI score result must be within the supplied levels");
+ }
+ return result;
+ }
+
+ private static double threshold(SemanticQuestion question) {
+ return ((Number) question.getParameters().getOrDefault("threshold", 0.5)).doubleValue();
+ }
+
+ private static double uncertainty(SemanticQuestion question) {
+ return ((Number) question.getParameters().getOrDefault("uncertainty", 0.0)).doubleValue();
+ }
+
+ private static boolean booleanDecision(SemanticQuestion question, double probability) {
+ double threshold = threshold(question);
+ double uncertainty = uncertainty(question);
+ if (uncertainty > 0 && probability >= threshold - uncertainty && probability <= threshold + uncertainty) {
+ if (!"non-match".equals(question.getParameters().get("uncertaintyPolicy"))) {
+ throw new IllegalStateException("Semantic boolean decision is uncertain");
+ }
+ return false;
+ }
+ return probability >= threshold;
}
}
diff --git a/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java b/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
index 739a7b1a0b1f..e72334809c7a 100644
--- a/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
+++ b/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
@@ -24,7 +24,7 @@ import java.util.stream.IntStream;
import org.apache.camel.Exchange;
import org.apache.camel.Expression;
import org.apache.camel.language.semantic.SemanticLanguage;
-import org.apache.camel.semantic.SemanticExpert.ResultType;
+import org.apache.camel.semantic.SemanticCapabilities;
import org.apache.camel.semantic.SemanticQuestion;
import org.apache.camel.semantic.SemanticQuestions;
import org.apache.camel.semantic.SemanticResult;
@@ -34,27 +34,82 @@ import org.apache.camel.support.ResourceHelper;
import org.apache.camel.util.json.JsonObject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
+ @ParameterizedTest
+ @CsvSource({ "0.25, fail", "0.5, fail", "0.75, fail", "0.25, non-match", "0.5, non-match", "0.75, non-match" })
+ void inclusiveUncertaintyBandIsAppliedByTheAdapter(double probability, String policy) throws Exception {
+ respond = request -> result(Map.of("question", Map.of("type", "noul", "noul", probability)));
+ var adapter = new TypeSafeAiSemanticAdapter();
+ adapter.setCamelContext(context);
+ var question = new SemanticQuestion(
+ "boolean", null, null, Map.of(
+ "instructions", "Classify", "threshold", 0.5, "uncertainty", 0.25, "uncertaintyPolicy", policy));
+ if (policy.equals("fail")) {
+ assertThatThrownBy(() -> adapter.evaluate(question, "text"))
+ .isInstanceOf(IllegalStateException.class).hasMessageContaining("uncertain");
+ } else {
+ var result = adapter.evaluate(question, "text");
+ assertThat(result.getValue()).isEqualTo(false);
+ assertThat(result.getProbability()).isEqualTo(probability);
+ }
+ assertThat(requests).hasSize(1);
+ }
+
+ @ParameterizedTest
+ @CsvSource({ "0.249, false", "0.751, true" })
+ void outsideUncertaintyBandUsesThreshold(double probability, boolean expected) throws Exception {
+ respond = request -> result(Map.of("question", Map.of("type", "noul", "noul", probability)));
+ var adapter = new TypeSafeAiSemanticAdapter();
+ adapter.setCamelContext(context);
+ var question = new SemanticQuestion(
+ "boolean", null, null,
+ Map.of("instructions", "Classify", "threshold", 0.5, "uncertainty", 0.25));
+ assertThat(adapter.evaluate(question, "text").getValue()).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @ValueSource(doubles = { 0.1, 0.9 })
+ void invalidUncertaintyBandFailsWithoutTransport(double threshold) {
+ var adapter = new TypeSafeAiSemanticAdapter();
+ var question = new SemanticQuestion(
+ "boolean", null, null,
+ Map.of("instructions", "Classify", "threshold", threshold, "uncertainty", 0.25));
+ assertThatThrownBy(() -> adapter.validate(question)).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("band within [0,1]");
+ assertThat(requests).isEmpty();
+ }
+
+ @Test
+ void omittedPolicyUsesAdapterDefaults() throws Exception {
+ respond = request -> result(Map.of("question", Map.of("type", "noul", "noul", 0.5)));
+ var adapter = new TypeSafeAiSemanticAdapter();
+ adapter.setCamelContext(context);
+ var question = new SemanticQuestion("boolean", null, null, Map.of("instructions", "Classify"));
+ assertThat(adapter.evaluate(question, "text").getValue()).isEqualTo(true);
+ assertThat(question.getParameters()).containsOnlyKeys("instructions");
+ }
+
@Test
void advertisesCapabilitiesAndRequiresInstructionsWithoutTransport() {
TypeSafeAiSemanticAdapter adapter = new TypeSafeAiSemanticAdapter();
- var capabilities = adapter.capabilities();
- assertThat(capabilities.isKnown()).isTrue();
+ var capabilities = SemanticCapabilities.from(adapter.getClass());
assertThat(capabilities.getName()).isEqualTo("typesafe-ai");
assertThat(capabilities.getArtifactId()).isEqualTo("camel-typesafe-ai");
- assertThat(capabilities.getMaxChoices()).isEqualTo(255);
- assertThat(capabilities.getMaxScoreLevels()).isEqualTo(10);
- assertThat(capabilities.getResultTypes()).containsExactly(ResultType.BOOLEAN, ResultType.CHOICE, ResultType.SCORE);
- assertThat(capabilities.isBooleanProbability()).isTrue();
+ assertThat(capabilities.operation("choice").getParameters().get("criteria").getMaxSize()).isEqualTo(255);
+ assertThat(capabilities.operation("score").getParameters().get("criteria").getMaxSize()).isEqualTo(10);
+ assertThat(capabilities.getOperations()).containsOnlyKeys("boolean", "choice", "score");
+ assertThat(capabilities.operation("boolean").isProbability()).isTrue();
+ assertThat(capabilities.operation("boolean").isConfidence()).isFalse();
SemanticQuestion fixed = new SemanticQuestion(
SemanticQuestion.Type.BOOLEAN, null, null,
null, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
- assertThatThrownBy(() -> adapter.validate(fixed)).hasMessageContaining("instructions are required");
+ assertThatThrownBy(() -> adapter.validate(fixed)).hasMessageContaining("Parameter 'instructions' is required");
assertThat(requests).isEmpty();
}
@@ -62,16 +117,17 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
void subclassesRetainCapabilitiesAndProviderValidation() {
TypeSafeAiSemanticAdapter subclass = new TypeSafeAiSemanticAdapter() {
};
- assertThat(subclass.capabilities()).usingRecursiveComparison()
- .isEqualTo(new TypeSafeAiSemanticAdapter().capabilities());
+ assertThat(SemanticCapabilities.from(subclass.getClass())).usingRecursiveComparison()
+ .isEqualTo(SemanticCapabilities.from(TypeSafeAiSemanticAdapter.class));
SemanticQuestion missing = new SemanticQuestion(
SemanticQuestion.Type.BOOLEAN, null, null,
null, null, 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
- assertThatThrownBy(() -> subclass.validate(missing)).hasMessageContaining("instructions are required");
+ assertThatThrownBy(() -> subclass.validate(missing)).hasMessageContaining("Parameter 'instructions' is required");
SemanticQuestion score = new SemanticQuestion(
SemanticQuestion.Type.SCORE, "Score", null, null,
IntStream.range(0, 11).mapToObj(i -> "Level " + i).toList(), 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
- assertThatThrownBy(() -> subclass.validate(score)).hasMessageContaining("10 score levels");
+ assertThatThrownBy(() -> subclass.validate(score))
+ .hasMessageContaining("Parameter 'criteria' is outside its size constraints");
}
@Test
@@ -82,7 +138,7 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
SemanticQuestion question = new SemanticQuestion(
SemanticQuestion.Type.BOOLEAN, "Classify", null,
Map.of(), List.of(), 0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL);
- assertThat(adapter.evaluate(question, "original").decision(question)).isEqualTo(true);
+ assertThat(adapter.evaluate(question, "original").getValue()).isEqualTo(true);
assertThat(requests).hasSize(1);
assertThat(authorization).containsExactly("Bearer test-key");
}
@@ -100,9 +156,9 @@ class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
Map.of(), IntStream.range(0, 11).mapToObj(i -> "Level " + i).toList(), 0.5, 0,
SemanticQuestion.UncertaintyPolicy.FAIL);
assertThatThrownBy(() -> adapter.validate(choice)).isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("255 choice criteria");
+ .hasMessageContaining("Parameter 'criteria' is outside its size constraints");
assertThatThrownBy(() -> adapter.evaluate(score, "original")).isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("10 score levels");
+ .hasMessageContaining("Parameter 'criteria' is outside its size constraints");
assertThat(requests).isEmpty();
}
diff --git a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
index d7286003a364..f11ea5d6b4bd 100644
--- a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
+++ b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
@@ -110,76 +110,77 @@
"isEmpty(exp)": { "index": 81, "kind": "function", "displayName": "Is Empty", "group": "condition", "label": "condition", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Whether the message body (or expression) is null or empty (list\/map types are tested if they have 0 elements).", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": fals [...]
"isNumeric(exp)": { "index": 82, "kind": "function", "displayName": "Is Numeric Value", "group": "condition", "label": "condition", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Whether the message body (or expression) is numeric value (0..9). For more advanced checks use the `regex` operator.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", " [...]
"jq(input,exp)": { "index": 83, "kind": "function", "displayName": "JQ", "group": "json", "label": "json", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with JSon data, then this allows using the JQ language, for example, to extract data from the message body (in JSon format). This requires having camel-jq JAR on the classpath. For input (optional), you can choose [...]
- "jsonpath(input,exp)": { "index": 84, "kind": "function", "displayName": "JSonPath", "group": "json", "label": "json", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with JSon data, then this allows using the JSonPath language, for example, to extract data from the message body (in JSon format). This requires having camel-jsonpath JAR on the classpath. For input (op [...]
- "join(separator,prefix,exp)": { "index": 85, "kind": "function", "displayName": "Join", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The join function iterates the message body\/expression and joins the data into a string. The separator is by default a comma. The prefix is optional. The join uses the message body as source by default. It i [...]
- "length(exp)": { "index": 86, "kind": "function", "displayName": "Length", "group": "core", "label": "core", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The payload length (number of bytes) of the message body (or expression).", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressi [...]
- "list(val...)": { "index": 87, "kind": "function", "displayName": "Create List of values", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.ArrayList", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The list function creates an ArrayList with the given set of values.", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": true, "description": [...]
- "listAdd(source,exp)": { "index": 88, "kind": "function", "displayName": "List Add", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Adds the result of the expression to the message body (or expression) which is a list", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaultValue" [...]
- "listRemove(source,exp)": { "index": 89, "kind": "function", "displayName": "List Remove", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Removes the result of the expression from the message body (or expression) which is a list", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "de [...]
- "load(file)": { "index": 90, "kind": "function", "displayName": "Load", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Loads the content of the resource from classpath (cannot load from file-system to avoid dangerous situations).", "ognl": false, "suffix": "}", "params": [ { "name": "file", "javaType": "String", "required": true, "description": [...]
- "lowercase(exp)": { "index": 91, "kind": "function", "displayName": "Lowercase", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Lowercases the message body (or expression)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitted [...]
- "mandatoryBodyAs(type)": { "index": 92, "kind": "function", "displayName": "Mandatory Body As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the message body to the given type (classname). If the body is null then the function will fail with an exception", "ognl": true, "suffix": "}", "params": [ { "name": "type", "javaType": "String" [...]
- "map(key1,value1,...)": { "index": 93, "kind": "function", "displayName": "Create Map of pairs", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.LinkedHashMap", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The map function creates a LinkedHashMap with the given set of pairs.", "ognl": false, "suffix": "}", "params": [ { "name": "key1", "javaType": "Object", "required": true, "d [...]
- "mapAdd(source,key,exp)": { "index": 94, "kind": "function", "displayName": "Map Add", "group": "collection", "label": "collection", "required": false, "javaType": "Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Adds the result of the expression to the message body (or expression) which is a map", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaultValue" [...]
- "mapRemove(source,key)": { "index": 95, "kind": "function", "displayName": "Map Remove", "group": "collection", "label": "collection", "required": false, "javaType": "Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Removes the result of the expression from the message body (or expression) which is a map", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaul [...]
- "max(val...)": { "index": 96, "kind": "function", "displayName": "Maximum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the maximum number from all the values (integral numbers only).", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "required": false, "defaultValue": "body", "description": "Val [...]
- "messageAs(type)": { "index": 97, "kind": "function", "displayName": "Message As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the message to the given type (classname).", "ognl": true, "suffix": "}", "params": [ { "name": "type", "javaType": "String", "required": true, "description": "The target type classname" } ] },
- "messageHistory(boolean)": { "index": 98, "kind": "function", "displayName": "Print Message History", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The message history of the current exchange (how it has been routed). This is similar to the route stack-trace message history the error handler logs in case of an unhandled exception. The boolean c [...]
- "messageTimestamp": { "index": 99, "kind": "function", "displayName": "Message Timestamp", "group": "core", "label": "core", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The message timestamp (millis since epoc) that this message originates from. Some systems like JMS, Kafka, AWS have a timestamp on the event\/message that Camel received. This method returns the timestamp if a t [...]
- "min(val...)": { "index": 100, "kind": "function", "displayName": "Minimum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the minimum number from all the values (integral numbers only).", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "required": false, "defaultValue": "body", "description": "Va [...]
- "newEmpty(type)": { "index": 101, "kind": "function", "displayName": "Create Empty Object", "group": "collection", "label": "collection", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Creates a new empty object (decided by type). Use `string` to create an empty String. Use `list` to create an empty `java.util.ArrayList`. Use `map` to create an empty `java.util.LinkedHashMap`. U [...]
- "normalizeWhitespace(exp)": { "index": 102, "kind": "function", "displayName": "Normalize Whitespace", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Normalizes the whitespace in the message body (or expression) by cleaning up excess whitespaces.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": f [...]
- "not(exp)": { "index": 103, "kind": "function", "displayName": "Not", "group": "condition", "label": "condition", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Evaluates the predicate and returns the opposite.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The predicate. When omitted [...]
- "null": { "index": 104, "kind": "function", "displayName": "Null", "group": "other", "label": "other", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a null value", "ognl": false, "suffix": "}", "examples": [ "${null} -> null" ] },
- "originalBody": { "index": 105, "kind": "function", "displayName": "Original Body", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The original incoming body (only available if allowUseOriginalMessage=true).", "ognl": false, "suffix": "}" },
- "pad(exp,width,separator)": { "index": 106, "kind": "function", "displayName": "Pad String", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Pads the expression with extra padding if necessary, according to the total width. The separator is by default a space. A positive width pads to the right (after the value), and a negative width pads to [...]
- "pretty(exp)": { "index": 107, "kind": "function", "displayName": "Pretty Print", "group": "xml", "label": "json,xml", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to a String, and attempts to pretty print if JSon or XML, otherwise the expression is returned as the String value.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "O [...]
- "prettyBody": { "index": 108, "kind": "function", "displayName": "Pretty Body", "group": "xml", "label": "json,xml", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to a String, and attempts to pretty print if JSon or XML; otherwise the body is returned as the String value.", "ognl": false, "suffix": "}" },
- "sort(exp,reverse)": { "index": 109, "kind": "function", "displayName": "Sort", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sorts the message body or expression in natural order", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression [...]
- "toPrettyJson(exp)": { "index": 110, "kind": "function", "displayName": "To Pretty JSon", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to JSon String representation.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressio [...]
- "toPrettyJsonBody": { "index": 111, "kind": "function", "displayName": "To Pretty JSon Body", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to JSon String representation.", "ognl": false, "suffix": "}" },
- "toJson(exp)": { "index": 112, "kind": "function", "displayName": "To JSon", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to JSon String representation.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitt [...]
- "toJsonBody": { "index": 113, "kind": "function", "displayName": "To JSon Body", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to JSon String representation.", "ognl": false, "suffix": "}" },
- "properties:key:default": { "index": 114, "kind": "function", "displayName": "Property Placeholder", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Lookup a property placeholder with the given key. If the key does not exist nor has a value, then an optional default value can be specified.", "ognl": false, "suffix": "}", "params": [ { "name": "ke [...]
- "propertiesExist:key": { "index": 115, "kind": "function", "displayName": "Property Placeholder Exists", "group": "core", "label": "core", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Checks whether a property placeholder with the given key exists or not. The result can be negated by prefixing the key with !", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaTy [...]
- "quote(exp)": { "index": 116, "kind": "function", "displayName": "Quote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) as a double quoted string", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressio [...]
- "random(min,max)": { "index": 117, "kind": "function", "displayName": "Random", "group": "number", "label": "number", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a random number between min and max (exclusive)", "ognl": false, "suffix": "}", "params": [ { "name": "min", "javaType": "int", "required": true, "description": "The minimum value (inclusive)" }, { "name": "max" [...]
- "range(min,max)": { "index": 118, "kind": "function", "displayName": "Range", "group": "number", "label": "number", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of increasing integers between the given interval (exclusive)", "ognl": false, "suffix": "}", "params": [ { "name": "min", "javaType": "int", "required": true, "description": "The start value (inclusive)" [...]
- "ref:name": { "index": 119, "kind": "function", "displayName": "Bean By Id", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "To look up a bean from the Registry with the given name.", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The bean name in the registry" } ], "examples": [...]
- "replace(from,to,exp)": { "index": 120, "kind": "function", "displayName": "Replace String Values", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Replace all the string values in the message body\/expression. To make it easier to replace single and double quotes, then you can use XML escaped values `\\"` as double quote, `\\'` as [...]
- "reverse(val...)": { "index": 121, "kind": "function", "displayName": "Reverse Values", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of all the values, but in reverse order", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": false, "defaultValue": "body", "description": "Value [...]
- "routeGroup": { "index": 122, "kind": "function", "displayName": "Route Group", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The route group of the current route the Exchange is being routed. Not all routes have a group assigned, so this may be null.", "ognl": false, "suffix": "}" },
- "routeId": { "index": 123, "kind": "function", "displayName": "Route Id", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The route id of the current route the Exchange is being routed", "ognl": false, "suffix": "}", "examples": [ "${routeId} -> myRoute" ] },
- "safeQuote(exp)": { "index": 124, "kind": "function", "displayName": "Safe Quote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) safely quoted if needed", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The ex [...]
- "setAttachment(key,exp)": { "index": 125, "kind": "function", "displayName": "Set Attachment", "group": "attachment", "label": "attachment", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets an attachment with payload from the message body\/expression.", "ognl": false, "suffix": "}" },
- "setHeader(name,type,exp)": { "index": 126, "kind": "function", "displayName": "Set Header", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets a message header with the given expression (optional converting to the given type)", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "T [...]
- "setVariable(name,type,exp)": { "index": 127, "kind": "function", "displayName": "Set Variable", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets a variable with the given expression (optional converting to the given type)", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The [...]
- "shuffle(val...)": { "index": 128, "kind": "function", "displayName": "Shuffle Values", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of all the values shuffled in random order", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": false, "defaultValue": "body", "description": "Va [...]
- "simpleJsonpath(input,exp)": { "index": 129, "kind": "function", "displayName": "Simple JSonPath", "group": "json", "label": "json", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with JSon data, then this allows using the Simple JSonPath language, for example, to extract data from the message body (in JSon format). For input (optional), you can choose `header:key`, [...]
- "size(exp)": { "index": 130, "kind": "function", "displayName": "Size", "group": "collection", "label": "collection", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the number of elements in collection or array based payloads. If the value is null then 0 is returned, otherwise 1.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": [...]
- "skip(num)": { "index": 131, "kind": "function", "displayName": "Skip First Items from the Message Body", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.Iterator", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The skip function iterates the message body and skips the first number of items. This can be used with the Splitter EIP to split a message body and skip the first N numbe [...]
- "split(exp,separator)": { "index": 132, "kind": "function", "displayName": "Split String Values", "group": "collection", "label": "collection", "required": false, "javaType": "String[]", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Splits the message body\/expression as a String value using the separator into a String array", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": [...]
- "stepId": { "index": 133, "kind": "function", "displayName": "Step Id", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the id of the current step the Exchange is being routed.", "ognl": false, "suffix": "}" },
- "substring(head,tail)": { "index": 134, "kind": "function", "displayName": "Substring", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression by clipping characters off its ends (not by positions). With one number, a positive number clips that many characters from the beginning, and a negative num [...]
- "substringAfter(exp,before)": { "index": 135, "kind": "function", "displayName": "Substring After", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that comes after. Returns null if nothing comes after.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "requi [...]
- "substringBefore(exp,before)": { "index": 136, "kind": "function", "displayName": "Substring Before", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that comes before. Returns null if nothing comes before.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "r [...]
- "substringBetween(exp,after,before)": { "index": 137, "kind": "function", "displayName": "Substring Between", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that are between after and before. Returns null if nothing comes between.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", [...]
- "sum(val...)": { "index": 138, "kind": "function", "displayName": "Calculate Sum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sums together all the values as integral numbers. This function can also be used to subtract by using negative numbers.", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "requir [...]
- "sys.name": { "index": 139, "kind": "function", "displayName": "JVM System Property", "group": "other", "label": "other", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The JVM system property with the given name", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The system property name" } ], "examples": [ "${sy [...]
- "threadId": { "index": 140, "kind": "function", "displayName": "Thread Id", "group": "other", "label": "other", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the id of the current thread. Can be used for logging.", "ognl": false, "suffix": "}" },
- "threadName": { "index": 141, "kind": "function", "displayName": "Thread Name", "group": "other", "label": "other", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the name of the current thread. Can be used for logging.", "ognl": false, "suffix": "}" },
- "throwException(msg,type)": { "index": 142, "kind": "function", "displayName": "Throw Exception", "group": "core", "label": "core", "required": false, "javaType": "java.lang.Exception", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Deliberately throws an error. Uses IllegalArgumentException by default if no type is specified (use fully qualified classname).", "ognl": false, "suffix": "}", "params": [ { "name": "msg", [...]
- "trim(exp)": { "index": 143, "kind": "function", "displayName": "Trim", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The trim function trims the message body (or expression) by removing all leading and trailing white spaces.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue" [...]
- "type:name.field": { "index": 144, "kind": "function", "displayName": "Java Field Value", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "To refer to a type or field by its classname. To refer to a field, you can append .FIELD_NAME. For example, you can refer to the constant field from Exchange as: `org.apache.camel.Exchange.FILE_NAME`", "ognl": [...]
- "kindOfType(exp)": { "index": 145, "kind": "function", "displayName": "Kind of Type", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "What kind of type is the value (null,number,string,boolean,array,object)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": [...]
- "unquote(exp)": { "index": 146, "kind": "function", "displayName": "Unquote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) with any leading\/ending quotes removed", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "descriptio [...]
- "uppercase(exp)": { "index": 147, "kind": "function", "displayName": "Uppercase", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Uppercases the message body (or expression)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitted [...]
- "uuid(type)": { "index": 148, "kind": "function", "displayName": "Generate UUID", "group": "other", "label": "other", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a UUID using the Camel `UuidGenerator`. You can choose between `default`, `classic`, `short`, `simple` and `random` as the type. If no type is given, the default is used. It is also possible to use a custom ` [...]
- "val(exp)": { "index": 149, "kind": "function", "displayName": "Value", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the expression as a constant value", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": true, "description": "The constant value expression" } ], "examples": [ "${val(42)} -> 42" [...]
- "variable.name": { "index": 150, "kind": "function", "displayName": "Variable", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The variable with the given name", "ognl": true, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The variable name" } ], "examples": [ "${variable.myVar} -> variable v [...]
- "variableAs(key,type)": { "index": 151, "kind": "function", "displayName": "Variable As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaType": "String", "required": true, "description": "The variable name" }, { "name": "type", [...]
- "variables": { "index": 152, "kind": "function", "displayName": "Variables", "group": "core", "label": "core", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}", "examples": [ "${variables} -> {myVar=value1, count=5}" ] },
- "xpath(input,exp)": { "index": 153, "kind": "function", "displayName": "XPath", "group": "xml", "label": "xml", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optional), you ca [...]
+ "semantic(name)": { "index": 84, "kind": "function", "displayName": "Semantic Evaluation", "group": "ai", "label": "ai", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Invoke a named semantic evaluation and return its typed value. Preserves the body and publishes CamelSemanticResult. Each occurrence invokes the expert. Requires camel-semantic.", "ognl": false, "suffix": "}", "pa [...]
+ "jsonpath(input,exp)": { "index": 85, "kind": "function", "displayName": "JSonPath", "group": "json", "label": "json", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with JSon data, then this allows using the JSonPath language, for example, to extract data from the message body (in JSon format). This requires having camel-jsonpath JAR on the classpath. For input (op [...]
+ "join(separator,prefix,exp)": { "index": 86, "kind": "function", "displayName": "Join", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The join function iterates the message body\/expression and joins the data into a string. The separator is by default a comma. The prefix is optional. The join uses the message body as source by default. It i [...]
+ "length(exp)": { "index": 87, "kind": "function", "displayName": "Length", "group": "core", "label": "core", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The payload length (number of bytes) of the message body (or expression).", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressi [...]
+ "list(val...)": { "index": 88, "kind": "function", "displayName": "Create List of values", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.ArrayList", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The list function creates an ArrayList with the given set of values.", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": true, "description": [...]
+ "listAdd(source,exp)": { "index": 89, "kind": "function", "displayName": "List Add", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Adds the result of the expression to the message body (or expression) which is a list", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaultValue" [...]
+ "listRemove(source,exp)": { "index": 90, "kind": "function", "displayName": "List Remove", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Removes the result of the expression from the message body (or expression) which is a list", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "de [...]
+ "load(file)": { "index": 91, "kind": "function", "displayName": "Load", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Loads the content of the resource from classpath (cannot load from file-system to avoid dangerous situations).", "ognl": false, "suffix": "}", "params": [ { "name": "file", "javaType": "String", "required": true, "description": [...]
+ "lowercase(exp)": { "index": 92, "kind": "function", "displayName": "Lowercase", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Lowercases the message body (or expression)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitted [...]
+ "mandatoryBodyAs(type)": { "index": 93, "kind": "function", "displayName": "Mandatory Body As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the message body to the given type (classname). If the body is null then the function will fail with an exception", "ognl": true, "suffix": "}", "params": [ { "name": "type", "javaType": "String" [...]
+ "map(key1,value1,...)": { "index": 94, "kind": "function", "displayName": "Create Map of pairs", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.LinkedHashMap", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The map function creates a LinkedHashMap with the given set of pairs.", "ognl": false, "suffix": "}", "params": [ { "name": "key1", "javaType": "Object", "required": true, "d [...]
+ "mapAdd(source,key,exp)": { "index": 95, "kind": "function", "displayName": "Map Add", "group": "collection", "label": "collection", "required": false, "javaType": "Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Adds the result of the expression to the message body (or expression) which is a map", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaultValue" [...]
+ "mapRemove(source,key)": { "index": 96, "kind": "function", "displayName": "Map Remove", "group": "collection", "label": "collection", "required": false, "javaType": "Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Removes the result of the expression from the message body (or expression) which is a map", "ognl": false, "suffix": "}", "params": [ { "name": "source", "javaType": "Object", "required": false, "defaul [...]
+ "max(val...)": { "index": 97, "kind": "function", "displayName": "Maximum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the maximum number from all the values (integral numbers only).", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "required": false, "defaultValue": "body", "description": "Val [...]
+ "messageAs(type)": { "index": 98, "kind": "function", "displayName": "Message As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the message to the given type (classname).", "ognl": true, "suffix": "}", "params": [ { "name": "type", "javaType": "String", "required": true, "description": "The target type classname" } ] },
+ "messageHistory(boolean)": { "index": 99, "kind": "function", "displayName": "Print Message History", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The message history of the current exchange (how it has been routed). This is similar to the route stack-trace message history the error handler logs in case of an unhandled exception. The boolean c [...]
+ "messageTimestamp": { "index": 100, "kind": "function", "displayName": "Message Timestamp", "group": "core", "label": "core", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The message timestamp (millis since epoc) that this message originates from. Some systems like JMS, Kafka, AWS have a timestamp on the event\/message that Camel received. This method returns the timestamp if a [...]
+ "min(val...)": { "index": 101, "kind": "function", "displayName": "Minimum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the minimum number from all the values (integral numbers only).", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "required": false, "defaultValue": "body", "description": "Va [...]
+ "newEmpty(type)": { "index": 102, "kind": "function", "displayName": "Create Empty Object", "group": "collection", "label": "collection", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Creates a new empty object (decided by type). Use `string` to create an empty String. Use `list` to create an empty `java.util.ArrayList`. Use `map` to create an empty `java.util.LinkedHashMap`. U [...]
+ "normalizeWhitespace(exp)": { "index": 103, "kind": "function", "displayName": "Normalize Whitespace", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Normalizes the whitespace in the message body (or expression) by cleaning up excess whitespaces.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": f [...]
+ "not(exp)": { "index": 104, "kind": "function", "displayName": "Not", "group": "condition", "label": "condition", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Evaluates the predicate and returns the opposite.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The predicate. When omitted [...]
+ "null": { "index": 105, "kind": "function", "displayName": "Null", "group": "other", "label": "other", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a null value", "ognl": false, "suffix": "}", "examples": [ "${null} -> null" ] },
+ "originalBody": { "index": 106, "kind": "function", "displayName": "Original Body", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The original incoming body (only available if allowUseOriginalMessage=true).", "ognl": false, "suffix": "}" },
+ "pad(exp,width,separator)": { "index": 107, "kind": "function", "displayName": "Pad String", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Pads the expression with extra padding if necessary, according to the total width. The separator is by default a space. A positive width pads to the right (after the value), and a negative width pads to [...]
+ "pretty(exp)": { "index": 108, "kind": "function", "displayName": "Pretty Print", "group": "xml", "label": "json,xml", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to a String, and attempts to pretty print if JSon or XML, otherwise the expression is returned as the String value.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "O [...]
+ "prettyBody": { "index": 109, "kind": "function", "displayName": "Pretty Body", "group": "xml", "label": "json,xml", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to a String, and attempts to pretty print if JSon or XML; otherwise the body is returned as the String value.", "ognl": false, "suffix": "}" },
+ "sort(exp,reverse)": { "index": 110, "kind": "function", "displayName": "Sort", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sorts the message body or expression in natural order", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression [...]
+ "toPrettyJson(exp)": { "index": 111, "kind": "function", "displayName": "To Pretty JSon", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to JSon String representation.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressio [...]
+ "toPrettyJsonBody": { "index": 112, "kind": "function", "displayName": "To Pretty JSon Body", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to JSon String representation.", "ognl": false, "suffix": "}" },
+ "toJson(exp)": { "index": 113, "kind": "function", "displayName": "To JSon", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the expression to JSon String representation.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitt [...]
+ "toJsonBody": { "index": 114, "kind": "function", "displayName": "To JSon Body", "group": "json", "label": "json", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the body to JSon String representation.", "ognl": false, "suffix": "}" },
+ "properties:key:default": { "index": 115, "kind": "function", "displayName": "Property Placeholder", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Lookup a property placeholder with the given key. If the key does not exist nor has a value, then an optional default value can be specified.", "ognl": false, "suffix": "}", "params": [ { "name": "ke [...]
+ "propertiesExist:key": { "index": 116, "kind": "function", "displayName": "Property Placeholder Exists", "group": "core", "label": "core", "required": false, "javaType": "boolean", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Checks whether a property placeholder with the given key exists or not. The result can be negated by prefixing the key with !", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaTy [...]
+ "quote(exp)": { "index": 117, "kind": "function", "displayName": "Quote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) as a double quoted string", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expressio [...]
+ "random(min,max)": { "index": 118, "kind": "function", "displayName": "Random", "group": "number", "label": "number", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a random number between min and max (exclusive)", "ognl": false, "suffix": "}", "params": [ { "name": "min", "javaType": "int", "required": true, "description": "The minimum value (inclusive)" }, { "name": "max" [...]
+ "range(min,max)": { "index": 119, "kind": "function", "displayName": "Range", "group": "number", "label": "number", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of increasing integers between the given interval (exclusive)", "ognl": false, "suffix": "}", "params": [ { "name": "min", "javaType": "int", "required": true, "description": "The start value (inclusive)" [...]
+ "ref:name": { "index": 120, "kind": "function", "displayName": "Bean By Id", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "To look up a bean from the Registry with the given name.", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The bean name in the registry" } ], "examples": [...]
+ "replace(from,to,exp)": { "index": 121, "kind": "function", "displayName": "Replace String Values", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Replace all the string values in the message body\/expression. To make it easier to replace single and double quotes, then you can use XML escaped values `\\"` as double quote, `\\'` as [...]
+ "reverse(val...)": { "index": 122, "kind": "function", "displayName": "Reverse Values", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of all the values, but in reverse order", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": false, "defaultValue": "body", "description": "Value [...]
+ "routeGroup": { "index": 123, "kind": "function", "displayName": "Route Group", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The route group of the current route the Exchange is being routed. Not all routes have a group assigned, so this may be null.", "ognl": false, "suffix": "}" },
+ "routeId": { "index": 124, "kind": "function", "displayName": "Route Id", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The route id of the current route the Exchange is being routed", "ognl": false, "suffix": "}", "examples": [ "${routeId} -> myRoute" ] },
+ "safeQuote(exp)": { "index": 125, "kind": "function", "displayName": "Safe Quote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) safely quoted if needed", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The ex [...]
+ "setAttachment(key,exp)": { "index": 126, "kind": "function", "displayName": "Set Attachment", "group": "attachment", "label": "attachment", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets an attachment with payload from the message body\/expression.", "ognl": false, "suffix": "}" },
+ "setHeader(name,type,exp)": { "index": 127, "kind": "function", "displayName": "Set Header", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets a message header with the given expression (optional converting to the given type)", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "T [...]
+ "setVariable(name,type,exp)": { "index": 128, "kind": "function", "displayName": "Set Variable", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets a variable with the given expression (optional converting to the given type)", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The [...]
+ "shuffle(val...)": { "index": 129, "kind": "function", "displayName": "Shuffle Values", "group": "collection", "label": "collection", "required": false, "javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a list of all the values shuffled in random order", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "Object", "required": false, "defaultValue": "body", "description": "Va [...]
+ "simpleJsonpath(input,exp)": { "index": 130, "kind": "function", "displayName": "Simple JSonPath", "group": "json", "label": "json", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with JSon data, then this allows using the Simple JSonPath language, for example, to extract data from the message body (in JSon format). For input (optional), you can choose `header:key`, [...]
+ "size(exp)": { "index": 131, "kind": "function", "displayName": "Size", "group": "collection", "label": "collection", "required": false, "javaType": "int", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the number of elements in collection or array based payloads. If the value is null then 0 is returned, otherwise 1.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": [...]
+ "skip(num)": { "index": 132, "kind": "function", "displayName": "Skip First Items from the Message Body", "group": "collection", "label": "collection", "required": false, "javaType": "java.util.Iterator", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The skip function iterates the message body and skips the first number of items. This can be used with the Splitter EIP to split a message body and skip the first N numbe [...]
+ "split(exp,separator)": { "index": 133, "kind": "function", "displayName": "Split String Values", "group": "collection", "label": "collection", "required": false, "javaType": "String[]", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Splits the message body\/expression as a String value using the separator into a String array", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": [...]
+ "stepId": { "index": 134, "kind": "function", "displayName": "Step Id", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the id of the current step the Exchange is being routed.", "ognl": false, "suffix": "}" },
+ "substring(head,tail)": { "index": 135, "kind": "function", "displayName": "Substring", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression by clipping characters off its ends (not by positions). With one number, a positive number clips that many characters from the beginning, and a negative num [...]
+ "substringAfter(exp,before)": { "index": 136, "kind": "function", "displayName": "Substring After", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that comes after. Returns null if nothing comes after.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "requi [...]
+ "substringBefore(exp,before)": { "index": 137, "kind": "function", "displayName": "Substring Before", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that comes before. Returns null if nothing comes before.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "r [...]
+ "substringBetween(exp,after,before)": { "index": 138, "kind": "function", "displayName": "Substring Between", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a substring of the message body\/expression that are between after and before. Returns null if nothing comes between.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", [...]
+ "sum(val...)": { "index": 139, "kind": "function", "displayName": "Calculate Sum Number", "group": "number", "label": "number", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sums together all the values as integral numbers. This function can also be used to subtract by using negative numbers.", "ognl": false, "suffix": "}", "params": [ { "name": "val", "javaType": "long", "requir [...]
+ "sys.name": { "index": 140, "kind": "function", "displayName": "JVM System Property", "group": "other", "label": "other", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The JVM system property with the given name", "ognl": false, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The system property name" } ], "examples": [ "${sy [...]
+ "threadId": { "index": 141, "kind": "function", "displayName": "Thread Id", "group": "other", "label": "other", "required": false, "javaType": "long", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the id of the current thread. Can be used for logging.", "ognl": false, "suffix": "}" },
+ "threadName": { "index": 142, "kind": "function", "displayName": "Thread Name", "group": "other", "label": "other", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the name of the current thread. Can be used for logging.", "ognl": false, "suffix": "}" },
+ "throwException(msg,type)": { "index": 143, "kind": "function", "displayName": "Throw Exception", "group": "core", "label": "core", "required": false, "javaType": "java.lang.Exception", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Deliberately throws an error. Uses IllegalArgumentException by default if no type is specified (use fully qualified classname).", "ognl": false, "suffix": "}", "params": [ { "name": "msg", [...]
+ "trim(exp)": { "index": 144, "kind": "function", "displayName": "Trim", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The trim function trims the message body (or expression) by removing all leading and trailing white spaces.", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue" [...]
+ "type:name.field": { "index": 145, "kind": "function", "displayName": "Java Field Value", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "To refer to a type or field by its classname. To refer to a field, you can append .FIELD_NAME. For example, you can refer to the constant field from Exchange as: `org.apache.camel.Exchange.FILE_NAME`", "ognl": [...]
+ "kindOfType(exp)": { "index": 146, "kind": "function", "displayName": "Kind of Type", "group": "core", "label": "core", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "What kind of type is the value (null,number,string,boolean,array,object)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": [...]
+ "unquote(exp)": { "index": 147, "kind": "function", "displayName": "Unquote", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) with any leading\/ending quotes removed", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "descriptio [...]
+ "uppercase(exp)": { "index": 148, "kind": "function", "displayName": "Uppercase", "group": "string", "label": "string", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Uppercases the message body (or expression)", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", "description": "The expression. When omitted [...]
+ "uuid(type)": { "index": 149, "kind": "function", "displayName": "Generate UUID", "group": "other", "label": "other", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a UUID using the Camel `UuidGenerator`. You can choose between `default`, `classic`, `short`, `simple` and `random` as the type. If no type is given, the default is used. It is also possible to use a custom ` [...]
+ "val(exp)": { "index": 150, "kind": "function", "displayName": "Value", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the expression as a constant value", "ognl": false, "suffix": "}", "params": [ { "name": "exp", "javaType": "Object", "required": true, "description": "The constant value expression" } ], "examples": [ "${val(42)} -> 42" [...]
+ "variable.name": { "index": 151, "kind": "function", "displayName": "Variable", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The variable with the given name", "ognl": true, "suffix": "}", "params": [ { "name": "name", "javaType": "String", "required": true, "description": "The variable name" } ], "examples": [ "${variable.myVar} -> variable v [...]
+ "variableAs(key,type)": { "index": 152, "kind": "function", "displayName": "Variable As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaType": "String", "required": true, "description": "The variable name" }, { "name": "type", [...]
+ "variables": { "index": 153, "kind": "function", "displayName": "Variables", "group": "core", "label": "core", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}", "examples": [ "${variables} -> {myVar=value1, count=5}" ] },
+ "xpath(input,exp)": { "index": 154, "kind": "function", "displayName": "XPath", "group": "xml", "label": "xml", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optional), you ca [...]
},
"operators": {
"==": { "index": 0, "kind": "operator", "displayName": "Eq", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests equality between left and right operand values. Camel will coerce the right operand type to match the left.", "operatorKind": "binary", "operatorSyntax": "LHS == RHS", "precedence": 10, "examples": [ "${header.foo} == 'bar'", "${header.count} == 5" ] },
diff --git a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
index a6a26ee3573a..eb35a1bb283f 100644
--- a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
+++ b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
@@ -660,6 +660,14 @@ to call the `safeQuote` function as shown below:
----
+=== Semantic Functions
+
+[width="100%",cols="2,1,7",options="header"]
+|===
+|Function |Type |Description
+|`semantic(name)` | `Object` | Invokes a named xref:semantic-language.adoc[Semantic evaluation], for example `${semantic('injection')}`. Returns its typed result, preserves the body, and publishes `CamelSemanticResult`. Each occurrence invokes the expert. Requires camel-semantic.
+|===
+
=== XML & JSON Functions
Functions to work with XML and JSON payloads.
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
index a48050560700..6712db0cdecb 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
@@ -517,6 +517,12 @@ public final class SimpleConstants {
"param=exp:String:required::The JQ expression" })
public static final String JQ = "jq(input,exp)";
+ @Metadata(description = "Invoke a named semantic evaluation and return its typed value. Preserves the body and publishes CamelSemanticResult. Each occurrence invokes the expert. Requires camel-semantic.",
+ label = "ai", javaType = "Object", displayName = "Semantic Evaluation",
+ examples = { "${semantic('injection')} -> true // the expert's Boolean verdict" },
+ annotations = { "param=name:String:required::The declared evaluation name" })
+ public static final String SEMANTIC = "semantic(name)";
+
@Metadata(description = "When working with JSon data, then this allows using the JSonPath language, for example, to extract data from the message body (in JSon format). This requires having camel-jsonpath JAR on the classpath. For input (optional), you can choose `header:key`, `exchangeProperty:key` or `variable:key` to use as input for the JSon payload instead of the message body.",
label = "json", javaType = "Object", displayName = "JSonPath",
examples = {
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
index 5003ac2f52d2..1e8a344a38fc 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
@@ -44,7 +44,7 @@ public final class SimpleSyntaxHints {
"substringBetween", "contains", "pad", "concat", "val", "length", "size", "empty", "newEmpty", "iif",
"hash", "convertTo", "throwException", "assert", "load", "uuid", "env", "sys", "sysenv", "ref", "bean",
"properties", "propertiesExist", "type", "messageAs", "messageHistory", "logExchange", "pretty",
- "toJson", "toPrettyJson", "jq", "jsonpath", "xpath", "simpleJsonpath", "function", "list", "map",
+ "toJson", "toPrettyJson", "jq", "jsonpath", "xpath", "simpleJsonpath", "semantic", "function", "list", "map",
"range", "split", "sort", "distinct", "reverse", "shuffle", "forEach", "filter", "listAdd", "listRemove",
"mapAdd", "mapRemove", "setHeader", "setVariable", "uppercase", "lowercase", "trim", "capitalize",
"normalizeWhitespace", "quote", "unquote", "safeQuote", "escape", "isEmpty", "isAlpha", "isAlphaNumeric",
@@ -57,7 +57,7 @@ public final class SimpleSyntaxHints {
* Functions that delegate to another language, all of them written {@code ${name(exp)}}. Unlike {@code bean:} or
* {@code date:} they take no colon form, which is the mistake CAMEL-24845 is about.
*/
- static final Set<String> QUERY_FUNCTIONS = Set.of("jq", "jsonpath", "xpath", "simpleJsonpath");
+ static final Set<String> QUERY_FUNCTIONS = Set.of("jq", "jsonpath", "xpath", "simpleJsonpath", "semantic");
/** Names from older Camel versions or other languages that a model still writes. */
static final Map<String, String> ALIASES = Map.ofEntries(
@@ -373,7 +373,7 @@ public final class SimpleSyntaxHints {
return bare + " is a language, not a simple function: another language cannot be nested inside ${...}; write"
+ " the expression with its own key, for example " + bare + ": \"...\"";
}
- // ${jsonpath:$.status}: written the way bean: and date: are, but these four take parentheses (CAMEL-24845)
+ // ${jsonpath:$.status}: written the way bean: and date: are, but these functions take parentheses (CAMEL-24845)
int colon = function.indexOf(':');
if (colon > 0 && QUERY_FUNCTIONS.contains(function.substring(0, colon))) {
return parentheses(function.substring(0, colon), function.substring(colon + 1));
@@ -421,6 +421,9 @@ public final class SimpleSyntaxHints {
/** The did-you-mean for a function whose argument was written after a colon instead of in parentheses. */
private static String parentheses(String name, String argument) {
+ if ("semantic".equals(name)) {
+ argument = "'" + ("exp".equals(argument) ? "evaluationName" : argument) + "'";
+ }
return "the argument goes in parentheses: did you mean ${" + name + "(" + argument + ")}?";
}
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
index ba0c801ed081..b4bc64a647ab 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
@@ -30,6 +30,7 @@ import org.apache.camel.language.simple.SimpleSyntaxHints;
import org.apache.camel.language.simple.functions.DirectFunctionFactory;
import org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.language.simple.types.SimpleToken;
+import org.apache.camel.support.ExpressionToPredicateAdapter;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.builder.ExpressionBuilder;
import org.apache.camel.util.ObjectHelper;
@@ -148,9 +149,8 @@ public class SimpleFunctionExpression extends LiteralExpression {
@Override
public <T> T evaluate(Exchange exchange, Class<T> type) {
- Object value = exp.evaluate(exchange, Object.class);
- // the same rule the language uses for a predicate on its own (CAMEL-24984)
- boolean matches = ObjectHelper.evaluateValuePredicate(value);
+ // Preserve a function's predicate contract, including semantic's boolean-only guard.
+ boolean matches = ExpressionToPredicateAdapter.toPredicate(exp).matches(exchange);
return exchange.getContext().getTypeConverter().convertTo(type, exchange, !matches);
}
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java
index dd5d39158715..e9eeeeb66b6b 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java
@@ -28,6 +28,7 @@ import org.apache.camel.RuntimeCamelException;
import org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.language.simple.types.SimpleToken;
import org.apache.camel.language.simple.types.UnaryOperatorType;
+import org.apache.camel.support.ExpressionToPredicateAdapter;
import org.apache.camel.util.ObjectHelper;
/**
@@ -102,10 +103,7 @@ public class UnaryExpression extends BaseSimpleNode {
@Override
public <T> T evaluate(Exchange exchange, Class<T> type) {
- Object value = exp.evaluate(exchange, Object.class);
- // the same rule the language uses for a predicate on its own, where ${body} is true and a missing
- // header is false, so !${body} and !${header.foo} answer the opposite of those (CAMEL-24984)
- boolean matches = ObjectHelper.evaluateValuePredicate(value);
+ boolean matches = ExpressionToPredicateAdapter.toPredicate(exp).matches(exchange);
return camelContext.getTypeConverter().convertTo(type, exchange, !matches);
}
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/QueryLanguageFunctionFactory.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/QueryLanguageFunctionFactory.java
index bbf0d15c2f1d..91619d557840 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/QueryLanguageFunctionFactory.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/QueryLanguageFunctionFactory.java
@@ -18,8 +18,10 @@ package org.apache.camel.language.simple.functions;
import org.apache.camel.CamelContext;
import org.apache.camel.Expression;
+import org.apache.camel.NoSuchLanguageException;
import org.apache.camel.language.simple.MiscExpressionBuilder;
import org.apache.camel.language.simple.types.SimpleParserException;
+import org.apache.camel.spi.Language;
import org.apache.camel.spi.SimpleLanguageFunctionFactory;
import org.apache.camel.support.builder.ExpressionBuilder;
import org.apache.camel.util.StringHelper;
@@ -33,6 +35,7 @@ import static org.apache.camel.language.simple.SimpleFunctionHelper.ifStartsWith
* All four functions support an optional input-source qualifier so the language operates on a header, exchange
* property, or variable rather than the body: e.g. {@code ${jq(header:myHeader,.name)}}.
* <p>
+ * {@code ${semantic('name')}} delegates to a named evaluation whose declaration selects the input.
*/
public final class QueryLanguageFunctionFactory implements SimpleLanguageFunctionFactory {
@@ -40,6 +43,30 @@ public final class QueryLanguageFunctionFactory implements SimpleLanguageFunctio
public Expression createFunction(CamelContext camelContext, String function, int index) {
String remainder;
+ remainder = ifStartsWithReturnRemainder("semantic(", function);
+ if (remainder != null) {
+ if (!remainder.endsWith(")")) {
+ throw new SimpleParserException("Valid syntax: ${semantic('evaluationName')}", index);
+ }
+ String argument = remainder.substring(0, remainder.length() - 1).strip();
+ if (argument.length() < 3 || !(argument.startsWith("'") && argument.endsWith("'")
+ || argument.startsWith("\"") && argument.endsWith("\""))) {
+ throw new SimpleParserException("Semantic requires one quoted evaluation name", index);
+ }
+ String name = argument.substring(1, argument.length() - 1);
+ if (name.isBlank() || name.indexOf(argument.charAt(0)) >= 0) {
+ throw new SimpleParserException("Semantic requires one quoted evaluation name", index);
+ }
+ Language semantic;
+ try {
+ semantic = camelContext.resolveLanguage("semantic");
+ } catch (NoSuchLanguageException missing) {
+ throw new SimpleParserException(
+ "The semantic function requires camel-semantic on the classpath", index);
+ }
+ return semantic.createExpression("ref:" + name);
+ }
+
remainder = ifStartsWithReturnRemainder("jq(", function);
if (remainder != null) {
return delegateLanguageExpression("jq", remainder, function, index);
diff --git a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
index 63f8520cc954..03d34ff1f601 100644
--- a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
+++ b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
@@ -40,6 +40,14 @@ public class SimpleSyntaxHintsTest extends ExchangeTestSupport {
return assertThrows(SimpleIllegalSyntaxException.class, parser::parseExpression).getMessage();
}
+ @Test
+ public void testSemanticFunctionHints() {
+ assertThat(expressionError("${sematic('q')}")).contains("${semantic('q')}");
+ assertThat(expressionError("${semantic:q}")).contains("${semantic('q')}");
+ assertThat(expressionError("${semantic}")).contains("${semantic('evaluationName')}");
+ assertThat(expressionError("${semantic('q')}")).contains("requires camel-semantic");
+ }
+
@Test
public void testFunctionWithoutDollarBraces() {
assertThat(predicateError("body == 'Hello World'"))
diff --git a/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java b/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
index de006f3addb2..3f15ea560b10 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
@@ -124,6 +124,7 @@ public class SimpleUtils {
"routegroup",
"routeid",
"safequote",
+ "semantic",
"setattachment",
"setheader",
"setvariable",
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
index cc28b5f68715..54babbdcafc0 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
@@ -56,37 +56,54 @@ class SemanticSchemaTest {
String question = booleanQuestion("\"{{limit:0.1}}\"");
for (String invalid : new String[] {
question.replace("threshold:", "thresholdd:"),
- question.replace("type: boolean", "type: choice\n criteria: {billing: Payments}"),
- question.replace("type: boolean", "type: score\n criteria: [Low, High]"),
question.replace("threshold: \"{{limit:0.1}}\"", "threshold: not-a-number"),
- question.replace("threshold: \"{{limit:0.1}}\"", "threshold: {value: 0.1}"),
- question.replace("type: boolean", "type: unknown") }) {
+ question.replace("threshold: \"{{limit:0.1}}\"", "threshold: {value: 0.1}") }) {
assertThat(validator.validate(invalid)).as("invalid question: %s", invalid).isNotEmpty();
}
}
@ParameterizedTest
@ValueSource(booleans = { false, true })
- void scalarConversionsDoNotHideMissingRequiredFields(boolean canonical) throws Exception {
+ void expertContractsOwnOperationNamesAndRequiredParameters(boolean canonical) throws Exception {
var validator = new YamlValidator(canonical);
- for (String fields : new String[] {
- "instructions: 123",
- "instructions: Assess this\n threshold: \"{{limit}}\"",
- "type: choice\n instructions: 123",
- "type: score\n instructions: 123" }) {
- String invalid = """
+ for (String declaration : new String[] { "question", "evaluation" }) {
+ String yaml = """
- semantic:
- question:
- invalid:
- %s
- """.formatted(fields);
- String valid = booleanQuestion("\"{{limit:0.1}}\"");
- for (String yaml : new String[] { invalid, valid + invalid, invalid + valid }) {
- assertThat(validator.validate(yaml)).as("missing required field: %s", yaml).isNotEmpty();
- }
+ expert: content
+ state: "${body}"
+ %s:
+ safety:
+ operation: classify
+ parameters:
+ policy:
+ labels: [privacy, unsafe]
+ limit: 2
+ enabled: true
+ department:
+ type: choice
+ instructions: Department?
+ criteria: {billing: Invoices, support: Questions}
+ score:
+ operation: rank
+ criteria: [Low, High]
+ """.formatted(declaration);
+ assertThat(validator.validate(yaml)).isEmpty();
+ assertThat(validator.validate(yaml.replace("parameters:", "paramters:"))).isNotEmpty();
+ assertThat(validator.validate(yaml.replace("parameters:", "parameters: []\n ignored:")))
+ .isNotEmpty();
}
}
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void semanticRequiresExactlyOneDeclarationBlock(boolean canonical) throws Exception {
+ var validator = new YamlValidator(canonical);
+ assertThat(validator.validate("- semantic: {expert: content}")).isNotEmpty();
+ assertThat(validator.validate("- semantic: {question: {}, evaluation: {}}")).isNotEmpty();
+ assertThat(validator.validate("- semantic: {evaluation: {q: {operation: detect, uncertaintyPolicy: expert-policy}}}"))
+ .isEmpty();
+ }
+
private static String booleanQuestion(String value) {
return """
- semantic:
@@ -101,7 +118,7 @@ class SemanticSchemaTest {
@ParameterizedTest
@ValueSource(booleans = { false, true })
- void questionTypesHaveDistinctClosedShapes(boolean canonical) throws Exception {
+ void questionSyntaxIsClosedAndExpertConstraintsAreValidatedAtRuntime(boolean canonical) throws Exception {
YamlValidator validator = new YamlValidator(canonical);
String choice = """
- semantic:
@@ -117,8 +134,6 @@ class SemanticSchemaTest {
assertThat(validator.validate(choice.replace("type: choice", "type: boolean")))
.isEmpty(); // criterion key constraints are checked by the runtime
for (String invalid : new String[] {
- choice.replace("type: choice", "type: score"),
- choice.replace("instructions:", "threshold: 0.5\n instructions:"),
choice.replace("instructions:", "typo:"),
choice.replace("instructions:", "uncertainty-policy: fail\n instructions:") }) {
assertThat(validator.validate(invalid)).isNotEmpty().allSatisfy(
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
index 0a77ca002da4..3477e3878693 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
@@ -13580,25 +13580,63 @@
}
},
"org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer" : {
+ "type" : "object",
+ "anyOf" : [ {
+ "oneOf" : [ {
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionBlockSchema"
+ }, {
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationBlockSchema"
+ } ]
+ } ]
+ },
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$BlockSchema" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
- "question" : {
+ "expert" : {
+ "type" : "string"
+ },
+ "state" : {
+ "type" : "string"
+ }
+ }
+ },
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationBlockSchema" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "properties" : {
+ "evaluation" : {
"type" : "object",
"additionalProperties" : {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionSchema"
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema"
}
+ },
+ "expert" : {
+ "type" : "string"
+ },
+ "state" : {
+ "type" : "string"
}
},
- "required" : [ "question" ]
+ "required" : [ "evaluation" ]
+ },
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema" : {
+ "type" : "object",
+ "anyOf" : [ {
+ "oneOf" : [ {
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$MapCriteriaSchema"
+ }, {
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ListCriteriaSchema"
+ } ]
+ } ]
},
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$BooleanSchema" : {
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ListCriteriaSchema" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"criteria" : {
- "type" : "object",
- "additionalProperties" : {
+ "type" : "array",
+ "items" : {
"type" : "string"
}
},
@@ -13608,6 +13646,12 @@
"instructions" : {
"type" : "string"
},
+ "operation" : {
+ "type" : "string"
+ },
+ "parameters" : {
+ "type" : "object"
+ },
"state" : {
"type" : "string"
},
@@ -13615,20 +13659,18 @@
"type" : "number"
},
"type" : {
- "type" : "string",
- "enum" : [ "boolean" ]
+ "type" : "string"
},
"uncertainty" : {
"type" : "number"
},
"uncertaintyPolicy" : {
- "type" : "string",
- "enum" : [ "fail", "non-match" ]
+ "type" : "string"
}
},
- "required" : [ "type" ]
+ "required" : [ "criteria" ]
},
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ChoiceSchema" : {
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$MapCriteriaSchema" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
@@ -13644,53 +13686,47 @@
"instructions" : {
"type" : "string"
},
+ "operation" : {
+ "type" : "string"
+ },
+ "parameters" : {
+ "type" : "object"
+ },
"state" : {
"type" : "string"
},
+ "threshold" : {
+ "type" : "number"
+ },
"type" : {
- "type" : "string",
- "enum" : [ "choice" ]
+ "type" : "string"
+ },
+ "uncertainty" : {
+ "type" : "number"
+ },
+ "uncertaintyPolicy" : {
+ "type" : "string"
}
- },
- "required" : [ "criteria", "type" ]
- },
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionSchema" : {
- "type" : "object",
- "anyOf" : [ {
- "oneOf" : [ {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$BooleanSchema"
- }, {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ChoiceSchema"
- }, {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ScoreSchema"
- } ]
- } ]
+ }
},
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ScoreSchema" : {
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionBlockSchema" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
- "criteria" : {
- "type" : "array",
- "items" : {
- "type" : "string"
- }
- },
"expert" : {
"type" : "string"
},
- "instructions" : {
- "type" : "string"
+ "question" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema"
+ }
},
"state" : {
"type" : "string"
- },
- "type" : {
- "type" : "string",
- "enum" : [ "score" ]
}
},
- "required" : [ "criteria", "type" ]
+ "required" : [ "question" ]
}
},
"properties" : {
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
index d6fc0c2c1c4c..6a67d3001303 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
@@ -20526,22 +20526,62 @@
"semantic" : {
"isListItem" : true,
"children" : [ {
+ "name" : "expert",
+ "type" : "string"
+ }, {
"name" : "question",
"type" : "map",
- "ref" : "semanticDefinitionDeserializer$QuestionSchema",
+ "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
+ "required" : true
+ }, {
+ "name" : "state",
+ "type" : "string"
+ }, {
+ "name" : "evaluation",
+ "type" : "map",
+ "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
+ "required" : true
+ } ]
+ },
+ "semanticDefinitionDeserializer$BlockSchema" : {
+ "children" : [ {
+ "name" : "expert",
+ "type" : "string"
+ }, {
+ "name" : "state",
+ "type" : "string"
+ } ]
+ },
+ "semanticDefinitionDeserializer$EvaluationBlockSchema" : {
+ "children" : [ {
+ "name" : "evaluation",
+ "type" : "map",
+ "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
"required" : true
+ }, {
+ "name" : "expert",
+ "type" : "string"
+ }, {
+ "name" : "state",
+ "type" : "string"
} ]
},
- "semanticDefinitionDeserializer$BooleanSchema" : {
+ "semanticDefinitionDeserializer$EvaluationSchema" : {
"children" : [ {
"name" : "criteria",
- "type" : "object"
+ "type" : "any"
}, {
"name" : "expert",
"type" : "string"
}, {
"name" : "instructions",
"type" : "string"
+ }, {
+ "name" : "operation",
+ "type" : "string"
+ }, {
+ "name" : "parameters",
+ "type" : "object"
}, {
"name" : "state",
"type" : "string"
@@ -20550,22 +20590,19 @@
"type" : "number"
}, {
"name" : "type",
- "type" : "enum",
- "enum" : [ "boolean" ],
- "required" : true
+ "type" : "string"
}, {
"name" : "uncertainty",
"type" : "number"
}, {
"name" : "uncertaintyPolicy",
- "type" : "enum",
- "enum" : [ "fail", "non-match" ]
+ "type" : "string"
} ]
},
- "semanticDefinitionDeserializer$ChoiceSchema" : {
+ "semanticDefinitionDeserializer$ListCriteriaSchema" : {
"children" : [ {
"name" : "criteria",
- "type" : "object",
+ "type" : "array",
"required" : true
}, {
"name" : "expert",
@@ -20573,26 +20610,45 @@
}, {
"name" : "instructions",
"type" : "string"
+ }, {
+ "name" : "operation",
+ "type" : "string"
+ }, {
+ "name" : "parameters",
+ "type" : "object"
}, {
"name" : "state",
"type" : "string"
+ }, {
+ "name" : "threshold",
+ "type" : "number"
}, {
"name" : "type",
- "type" : "enum",
- "enum" : [ "choice" ],
- "required" : true
+ "type" : "string"
+ }, {
+ "name" : "uncertainty",
+ "type" : "number"
+ }, {
+ "name" : "uncertaintyPolicy",
+ "type" : "string"
} ]
},
- "semanticDefinitionDeserializer$QuestionSchema" : {
+ "semanticDefinitionDeserializer$MapCriteriaSchema" : {
"children" : [ {
"name" : "criteria",
- "type" : "any"
+ "type" : "object"
}, {
"name" : "expert",
"type" : "string"
}, {
"name" : "instructions",
"type" : "string"
+ }, {
+ "name" : "operation",
+ "type" : "string"
+ }, {
+ "name" : "parameters",
+ "type" : "object"
}, {
"name" : "state",
"type" : "string"
@@ -20601,37 +20657,27 @@
"type" : "number"
}, {
"name" : "type",
- "type" : "enum",
- "enum" : [ "boolean", "choice", "score" ],
- "required" : true
+ "type" : "string"
}, {
"name" : "uncertainty",
"type" : "number"
}, {
"name" : "uncertaintyPolicy",
- "type" : "enum",
- "enum" : [ "fail", "non-match" ]
+ "type" : "string"
} ]
},
- "semanticDefinitionDeserializer$ScoreSchema" : {
+ "semanticDefinitionDeserializer$QuestionBlockSchema" : {
"children" : [ {
- "name" : "criteria",
- "type" : "array",
- "required" : true
- }, {
"name" : "expert",
"type" : "string"
}, {
- "name" : "instructions",
- "type" : "string"
+ "name" : "question",
+ "type" : "map",
+ "ref" : "semanticDefinitionDeserializer$EvaluationSchema",
+ "required" : true
}, {
"name" : "state",
"type" : "string"
- }, {
- "name" : "type",
- "type" : "enum",
- "enum" : [ "score" ],
- "required" : true
} ]
}
}
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
index 6dc8dda2f6c1..cae51c5e06a0 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
@@ -16480,25 +16480,63 @@
}
},
"org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer" : {
+ "type" : "object",
+ "anyOf" : [ {
+ "oneOf" : [ {
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionBlockSchema"
+ }, {
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationBlockSchema"
+ } ]
+ } ]
+ },
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$BlockSchema" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
- "question" : {
+ "expert" : {
+ "type" : "string"
+ },
+ "state" : {
+ "type" : "string"
+ }
+ }
+ },
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationBlockSchema" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "properties" : {
+ "evaluation" : {
"type" : "object",
"additionalProperties" : {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionSchema"
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema"
}
+ },
+ "expert" : {
+ "type" : "string"
+ },
+ "state" : {
+ "type" : "string"
}
},
- "required" : [ "question" ]
+ "required" : [ "evaluation" ]
+ },
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema" : {
+ "type" : "object",
+ "anyOf" : [ {
+ "oneOf" : [ {
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$MapCriteriaSchema"
+ }, {
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ListCriteriaSchema"
+ } ]
+ } ]
},
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$BooleanSchema" : {
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ListCriteriaSchema" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"criteria" : {
- "type" : "object",
- "additionalProperties" : {
+ "type" : "array",
+ "items" : {
"type" : "string"
}
},
@@ -16508,6 +16546,12 @@
"instructions" : {
"type" : "string"
},
+ "operation" : {
+ "type" : "string"
+ },
+ "parameters" : {
+ "type" : "object"
+ },
"state" : {
"type" : "string"
},
@@ -16515,20 +16559,18 @@
"type" : "number"
},
"type" : {
- "type" : "string",
- "enum" : [ "boolean" ]
+ "type" : "string"
},
"uncertainty" : {
"type" : "number"
},
"uncertaintyPolicy" : {
- "type" : "string",
- "enum" : [ "fail", "non-match" ]
+ "type" : "string"
}
},
- "required" : [ "type" ]
+ "required" : [ "criteria" ]
},
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ChoiceSchema" : {
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$MapCriteriaSchema" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
@@ -16544,53 +16586,47 @@
"instructions" : {
"type" : "string"
},
+ "operation" : {
+ "type" : "string"
+ },
+ "parameters" : {
+ "type" : "object"
+ },
"state" : {
"type" : "string"
},
+ "threshold" : {
+ "type" : "number"
+ },
"type" : {
- "type" : "string",
- "enum" : [ "choice" ]
+ "type" : "string"
+ },
+ "uncertainty" : {
+ "type" : "number"
+ },
+ "uncertaintyPolicy" : {
+ "type" : "string"
}
- },
- "required" : [ "criteria", "type" ]
- },
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionSchema" : {
- "type" : "object",
- "anyOf" : [ {
- "oneOf" : [ {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$BooleanSchema"
- }, {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ChoiceSchema"
- }, {
- "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ScoreSchema"
- } ]
- } ]
+ }
},
- "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$ScoreSchema" : {
+ "org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$QuestionBlockSchema" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
- "criteria" : {
- "type" : "array",
- "items" : {
- "type" : "string"
- }
- },
"expert" : {
"type" : "string"
},
- "instructions" : {
- "type" : "string"
+ "question" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "$ref" : "#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$EvaluationSchema"
+ }
},
"state" : {
"type" : "string"
- },
- "type" : {
- "type" : "string",
- "enum" : [ "score" ]
}
},
- "required" : [ "criteria", "type" ]
+ "required" : [ "question" ]
},
"org.apache.camel.dsl.yaml.deserializers.OutputAwareFromDefinition$Inline" : {
"type" : "object",
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java
index 57fa125d6d2d..9f6d4a306905 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java
@@ -126,6 +126,9 @@ public class YamlRoutesBuilderLoader extends YamlRoutesBuilderLoaderSupport {
}
protected RouteBuilder builder(final YamlDeserializationContext ctx, final Node root) {
+ // All resources have prepared their beans before declarations are validated and routes constructed.
+ setDeserializationContext(root, ctx);
+ ctx.preParse(root);
// we need to keep track of already configured items as the yaml-dsl returns a
// RouteConfigurationBuilder that is capable of both route and route
@@ -710,7 +713,6 @@ public class YamlRoutesBuilderLoader extends YamlRoutesBuilderLoaderSupport {
setDeserializationContext(root, ctx);
- ctx.preParse(root);
Object target = preConfigureNode(root, ctx, true);
Iterator<?> it = ObjectHelper.createIterator(target);
while (it.hasNext()) {
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java
index 646828db2cd4..7a3b3674f595 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java
@@ -20,20 +20,24 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
-import org.apache.camel.dsl.yaml.common.exception.InvalidEnumException;
import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
import org.apache.camel.dsl.yaml.support.YamlTestSupport;
import org.apache.camel.language.semantic.SemanticLanguage;
import org.apache.camel.semantic.SemanticAdapter;
+import org.apache.camel.semantic.SemanticExpert;
+import org.apache.camel.semantic.SemanticOperation;
+import org.apache.camel.semantic.SemanticParameter;
import org.apache.camel.semantic.SemanticQuestion;
import org.apache.camel.semantic.SemanticQuestions;
import org.apache.camel.semantic.SemanticResult;
import org.apache.camel.spi.Resource;
+import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.ResourceHelper;
import org.apache.camel.support.RouteWatcherReloadStrategy;
@@ -56,20 +60,56 @@ class SemanticQuestionTest extends YamlTestSupport {
@Override
public void doSetup() {
- context.getRegistry().bind("classifier", new SemanticAdapter() {
- public void validate(SemanticQuestion question) {
- }
-
- public SemanticResult evaluate(SemanticQuestion question, Object state) {
- calls.incrementAndGet();
- selected = state;
- return new SemanticResult(
- state.toString().contains("invoice") ? "billing" : "technical", null, null, null, null);
- }
- });
+ context.getRegistry().bind("classifier", new Classifier());
((SemanticLanguage) context.resolveLanguage("semantic")).setAdapter("classifier");
}
+ @SemanticExpert(name = "classifier", description = "Test classifier", provider = "test", artifactId = "test", operations = {
+ @SemanticOperation(name = "choice", description = "Choose", inputTypes = SemanticExpert.InputType.TEXT,
+ inputRequirements = "Text", resultType = SemanticExpert.ResultType.CHOICE,
+ resultMeaning = "Department",
+ parameters = {
+ @SemanticParameter(name = "instructions", description = "Question", required = true,
+ minSize = 1),
+ @SemanticParameter(name = "criteria", description = "Labels", type = Map.class,
+ itemType = String.class,
+ required = true, minSize = 2) }),
+ @SemanticOperation(name = "boolean", description = "Decide", inputTypes = SemanticExpert.InputType.TEXT,
+ inputRequirements = "Text", resultType = SemanticExpert.ResultType.BOOLEAN,
+ resultMeaning = "Decision",
+ parameters = {
+ @SemanticParameter(name = "instructions", description = "Question", required = true,
+ minSize = 1),
+ @SemanticParameter(name = "threshold", description = "Threshold", type = Number.class,
+ minimum = 0,
+ maximum = 1, omission = "0.5"),
+ @SemanticParameter(name = "uncertainty", description = "Uncertainty",
+ type = Number.class, minimum = 0,
+ maximum = 1, omission = "0"),
+ @SemanticParameter(name = "uncertaintyPolicy", description = "Policy",
+ values = { "fail", "non-match" },
+ omission = "fail") }),
+ @SemanticOperation(name = "score", description = "Score", inputTypes = SemanticExpert.InputType.TEXT,
+ inputRequirements = "Text", resultType = SemanticExpert.ResultType.SCORE,
+ resultMeaning = "Score",
+ parameters = {
+ @SemanticParameter(name = "instructions", description = "Question", required = true,
+ minSize = 1),
+ @SemanticParameter(name = "criteria", description = "Levels", type = List.class,
+ itemType = String.class,
+ required = true, minSize = 2) }) })
+ private class Classifier implements SemanticAdapter {
+ public void validate(SemanticQuestion question) {
+ }
+
+ public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ calls.incrementAndGet();
+ selected = state;
+ return new SemanticResult(
+ state.toString().contains("invoice") ? "billing" : "technical", null, null, null, null);
+ }
+ }
+
private static String declarations(String state) {
return """
- semantic:
@@ -84,6 +124,91 @@ class SemanticQuestionTest extends YamlTestSupport {
""".formatted(state);
}
+ @SemanticExpert(name = "security", description = "Documentation example expert", provider = "test", artifactId = "test",
+ operations = {
+ @SemanticOperation(name = "injection", description = "Detect injection",
+ inputTypes = SemanticExpert.InputType.TEXT,
+ inputRequirements = "Text", resultType = SemanticExpert.ResultType.BOOLEAN,
+ resultMeaning = "Injection detected"),
+ @SemanticOperation(name = "classify", description = "Find labels",
+ inputTypes = SemanticExpert.InputType.TEXT,
+ inputRequirements = "Text",
+ resultType = SemanticExpert.ResultType.CLASSIFICATION,
+ resultMeaning = "Content labels", labels = "privacy") })
+ public static class SecurityExpert implements SemanticAdapter {
+ public void validate(SemanticQuestion question) {
+ }
+
+ public SemanticResult evaluate(SemanticQuestion question, Object state) {
+ return new SemanticResult(
+ question.getOperation().equals("injection")
+ ? state.toString().contains("injection") : Set.of("privacy"),
+ null, null, null, null);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void reloadPreparesExpertsAcrossTheResourceSetBeforeValidatingDeclarations(boolean separateResource) throws Exception {
+ context.start();
+ String beans = """
+ - beans:
+ - name: newSecurity
+ type: org.apache.camel.dsl.yaml.SemanticQuestionTest$SecurityExpert
+ """;
+ String declarations = """
+ - semantic:
+ expert: newSecurity
+ evaluation:
+ injection:
+ operation: injection
+ """;
+ if (separateResource) {
+ loadRoutes(ResourceHelper.fromString("evaluations.yaml", declarations),
+ ResourceHelper.fromString("experts.yaml", beans));
+ } else {
+ loadRoutes(ResourceHelper.fromString("evaluations.yaml", declarations + beans));
+ }
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("injection");
+ assertThat(context.resolveLanguage("simple").createPredicate("${semantic('injection')}").matches(exchange)).isTrue();
+ }
+
+ @Test
+ void documentationExpertExampleExecutesBooleanChoiceAndClassification() throws Exception {
+ context.getRegistry().bind("security", new SecurityExpert());
+ context.getRegistry().bind("decisions", new Classifier());
+ String docs
+ = Files.readString(Path.of("../../../components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc"));
+ String declarations = docs.substring(docs.indexOf("- semantic:"));
+ declarations = declarations.substring(0, declarations.indexOf("----"));
+ String route = docs.substring(docs.indexOf("- route:"));
+ route = route.substring(0, route.indexOf("----"));
+ for (String target : List.of("security-review", "billing", "technical", "sales", "manual-review")) {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:" + target).to("mock:" + target);
+ }
+ });
+ }
+ loadRoutes(declarations + route);
+ context.start();
+ var billing = context.getEndpoint("mock:billing", MockEndpoint.class);
+ var review = context.getEndpoint("mock:security-review", MockEndpoint.class);
+ billing.expectedBodiesReceived("invoice");
+ review.expectedBodiesReceived("injection");
+ try (var template = context.createProducerTemplate()) {
+ template.sendBody("direct:incoming", "invoice");
+ template.sendBody("direct:incoming", "injection");
+ }
+ MockEndpoint.assertIsSatisfied(context);
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("invoice");
+ assertThat(context.resolveLanguage("simple").createExpression("${semantic('categories')}")
+ .evaluate(exchange, Object.class)).isEqualTo(Set.of("privacy"));
+ }
+
@Test
void javaDeclarationsCoexistWithUnchangedYamlDeclarations() throws Exception {
context.addRoutes(new RouteBuilder() {
@@ -286,8 +411,6 @@ class SemanticQuestionTest extends YamlTestSupport {
.hasStackTraceContaining("Duplicate semantic question");
assertThatThrownBy(() -> loadRoutesNoValidate(declarations("${body}").replace("instructions:", "typo:")))
.hasStackTraceContaining("Unknown property");
- assertThatThrownBy(() -> loadRoutesNoValidate(declarations("${body}").replace("type: choice", "type: unsupported")))
- .hasRootCauseInstanceOf(InvalidEnumException.class).hasStackTraceContaining("unsupported");
assertThatThrownBy(() -> loadRoutesNoValidate(declarations("${body}")
.replace("instructions:", "uncertainty-policy: fail\n instructions:")))
.hasStackTraceContaining("Unknown property");
@@ -310,8 +433,9 @@ class SemanticQuestionTest extends YamlTestSupport {
criteria: [Routine, Urgent]
""");
assertThat(SemanticQuestions.get(context).get("urgency").getLevels()).containsExactly("Routine", "Urgent");
+ context.start();
assertThatThrownBy(() -> loadRoutesNoValidate(declarations("${body}").replace("type: choice", "type: score")))
- .hasStackTraceContaining("Node type map is invalid, expected array");
+ .hasStackTraceContaining("criteria").hasStackTraceContaining("List");
}
@Test
@@ -342,10 +466,9 @@ class SemanticQuestionTest extends YamlTestSupport {
""".formatted(field);
assertThatThrownBy(() -> loadRoutesNoValidate(yaml))
.hasMessageContaining("route-0.yaml")
- .hasRootCauseInstanceOf(NumberFormatException.class)
- .cause().isInstanceOfSatisfying(YamlDeserializationException.class, error -> {
+ .isInstanceOfSatisfying(YamlDeserializationException.class, error -> {
assertThat(error).hasMessageContaining(
- "Invalid numeric value for '" + field + "' in semantic question 'spam': abc");
+ "Invalid numeric value for '" + field + "' in semantic question 'spam'");
assertThat(error.getProblemMark()).hasValueSatisfying(mark -> {
assertThat(mark.getLine()).isEqualTo(5);
assertThat(mark.getColumn()).isEqualTo(8 + field.length() + 2);
@@ -354,8 +477,8 @@ class SemanticQuestionTest extends YamlTestSupport {
}
@ParameterizedTest
- @MethodSource("invalidEnums")
- void invalidEnumValuesIdentifyQuestionFieldAndLocation(String field, String value, int line) {
+ @ValueSource(strings = { "type", "uncertaintyPolicy" })
+ void invalidContractValuesFailAtStartupWithoutInference(String field) throws Exception {
String yaml = """
- semantic:
question:
@@ -364,28 +487,13 @@ class SemanticQuestionTest extends YamlTestSupport {
instructions: Is this spam?
""";
yaml = field.equals("type")
- ? yaml.replace("type: boolean", "type: " + value)
- : yaml + " uncertaintyPolicy: " + value + "\n";
- String source = yaml;
- assertThatThrownBy(() -> loadRoutesNoValidate(source))
- .hasMessageContaining("route-0.yaml")
- .hasRootCauseInstanceOf(InvalidEnumException.class)
- .cause().isInstanceOfSatisfying(YamlDeserializationException.class, error -> {
- assertThat(error).hasMessageContaining("Invalid value for '" + field + "' in semantic question 'spam'");
- assertThat(error.getProblemMark()).hasValueSatisfying(mark -> {
- assertThat(mark.getLine()).isEqualTo(line);
- assertThat(mark.getColumn()).isEqualTo(8 + field.length() + 2);
- });
- });
- }
-
- static Stream<Arguments> invalidEnums() {
- return Stream.of(
- Arguments.of("type", "unsupported", 3),
- Arguments.of("type", "''", 3),
- Arguments.of("uncertaintyPolicy", "unsupported", 5),
- Arguments.of("uncertaintyPolicy", "''", 5),
- Arguments.of("uncertaintyPolicy", "null", 5));
+ ? yaml.replace("type: boolean", "type: unsupported")
+ : yaml + " uncertaintyPolicy: unsupported\n";
+ loadRoutesNoValidate(yaml);
+ assertThatThrownBy(context::start).hasStackTraceContaining("spam")
+ .hasStackTraceContaining("classifier")
+ .hasStackTraceContaining(field.equals("type") ? "operation" : "uncertaintyPolicy");
+ assertThat(calls).hasValue(0);
}
@ParameterizedTest
@@ -409,7 +517,7 @@ class SemanticQuestionTest extends YamlTestSupport {
void invalidStructuresIdentifySourceAndOffendingNode(String yaml, String message, int line, int column) {
assertThatThrownBy(() -> loadRoutesNoValidate(yaml))
.hasMessageContaining("route-0.yaml")
- .cause().isInstanceOfSatisfying(YamlDeserializationException.class, error -> {
+ .isInstanceOfSatisfying(YamlDeserializationException.class, error -> {
assertThat(error).hasMessageContaining(message);
assertThat(error.getProblemMark()).hasValueSatisfying(mark -> {
assertThat(mark.getLine()).isEqualTo(line);
@@ -429,15 +537,13 @@ class SemanticQuestionTest extends YamlTestSupport {
Arguments.of(declaration.replace("instructions:", "typo:"),
"Unknown property 'typo' in semantic question 'department'", 5, 14),
Arguments.of(declaration.replace(" type: choice\n", ""),
- "Semantic question type is required: department", 3, 8),
+ "Specify exactly one operation or type: department", 3, 8),
Arguments.of(declaration.replace("type: choice", "type: choice\n type: choice"),
"Duplicate key 'type' in semantic question 'department'", 4, 8),
- Arguments.of("- semantic: {other: {}}", "Semantic declaration requires only question", 0, 12),
+ Arguments.of("- semantic: {other: {}}", "Unknown property 'other' in semantic declaration", 0, 20),
Arguments.of(question + question, "Duplicate semantic question: q", 4, 4),
Arguments.of(question + " q: {type: boolean, instructions: Is it valid?}\n",
- "Duplicate key 'q' in semantic questions", 3, 6),
- Arguments.of(question.replace("Is it valid?", "''"),
- "Invalid semantic question 'q': Question instructions must not be blank", 2, 9));
+ "Duplicate key 'q' in semantic evaluations", 3, 6));
}
}
(camel) 08/08: CAMEL-25382: Validate predicates registered during replacement
Posted by ld...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 7c0fca7a09cb7cc2acf70715edd597a8adb334c1
Author: Luigi De Masi <ld...@redhat.com>
AuthorDate: Thu Oct 8 09:19:32 2026 +0200
CAMEL-25382: Validate predicates registered during replacement
Check reentrant validator registrations against the pending replacement as
well as the published definitions. This closes the gap for predicates
created after the replacement has copied its existing validators, without
exposing pending definitions through public getters or changing Simple.
Test compatible and incompatible replacements, rollback and subsequent
registration. Clarify published-snapshot reads and nested validation scopes
as requested in review.
Co-authored-by: Codex <no...@openai.com>
Signed-off-by: Luigi De Masi <ld...@redhat.com>
---
.../camel/language/semantic/SemanticLanguage.java | 1 +
.../apache/camel/semantic/SemanticEvaluations.java | 33 +++++++++----
.../camel/semantic/SemanticContractTest.java | 54 ++++++++++++++++++++++
3 files changed, 80 insertions(+), 8 deletions(-)
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
index 3ddc4d7620c4..25bbbbda764e 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
@@ -321,6 +321,7 @@ public class SemanticLanguage extends LanguageSupport {
private <T> T validationOnly(Map<String, SemanticEvaluation> snapshot, Supplier<T> action) {
Validation previous = validation.get();
+ // An explicit snapshot overrides the scope; nested calls without one inherit the outer validation scope.
validation.set(snapshot != null || previous == null ? new Validation(snapshot) : previous);
try {
return action.get();
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
index 30f65e8a1730..b343b6ee1851 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
@@ -21,6 +21,7 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Consumer;
@@ -36,6 +37,8 @@ public final class SemanticEvaluations {
private final Map<String, Resource> resources = new HashMap<>();
private volatile Map<String, SemanticEvaluation> evaluations = Map.of();
private final Map<Consumer<Map<String, SemanticEvaluation>>, List<String>> validators = new WeakHashMap<>();
+ // Accessed only under this registry's monitor, including reentrant validator registration.
+ private PendingReplacement pendingReplacement;
private SemanticEvaluations(CamelContext context) {
this.context = context;
@@ -51,11 +54,16 @@ public final class SemanticEvaluations {
/**
* Validate current declarations and weakly track a callback owned by its initialized expression. The callback
* receives the complete immutable snapshot so nested references can be validated against the same replacement.
+ * Registrations made during replacement validation must also accept the pending replacement.
*/
public synchronized void setValidator(List<String> names, Consumer<Map<String, SemanticEvaluation>> validator) {
// A replacement may have occurred between the expression's initial compilation and registration.
get(names);
+ // Reentrant registration first checks published definitions; the replacement is not visible until it succeeds.
validator.accept(evaluations);
+ if (pendingReplacement != null && !Collections.disjoint(names, pendingReplacement.changedNames)) {
+ validator.accept(pendingReplacement.snapshot);
+ }
validators.put(validator, List.copyOf(names));
}
@@ -97,15 +105,21 @@ public final class SemanticEvaluations {
}
});
Map<String, SemanticEvaluation> snapshot = Map.copyOf(replacement);
- if (context.isStarted() && !definitions.isEmpty()) {
- ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(definitions, snapshot);
- }
- // Expert callbacks may initialize expressions and register additional validators reentrantly.
- new LinkedHashMap<>(validators).forEach((validator, names) -> {
- if (!Collections.disjoint(names, definitions.keySet())) {
- validator.accept(snapshot);
+ PendingReplacement previous = pendingReplacement;
+ pendingReplacement = new PendingReplacement(snapshot, Set.copyOf(definitions.keySet()));
+ try {
+ if (context.isStarted() && !definitions.isEmpty()) {
+ ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(definitions, snapshot);
}
- });
+ // Expert callbacks may initialize expressions and register additional validators reentrantly.
+ new LinkedHashMap<>(validators).forEach((validator, names) -> {
+ if (!Collections.disjoint(names, definitions.keySet())) {
+ validator.accept(snapshot);
+ }
+ });
+ } finally {
+ pendingReplacement = previous;
+ }
if (definitions.isEmpty()) {
sources.remove(source);
} else {
@@ -174,4 +188,7 @@ public final class SemanticEvaluations {
return Collections.unmodifiableMap(selected);
}
+ private record PendingReplacement(Map<String, SemanticEvaluation> snapshot, Set<String> changedNames) {
+ }
+
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
index 7825fc0cffce..bc5eb7133641 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java
@@ -25,6 +25,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import org.apache.camel.Predicate;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.dsl.yaml.common.YamlDeserializationContext;
import org.apache.camel.impl.DefaultCamelContext;
@@ -239,6 +240,59 @@ class SemanticContractTest {
}
}
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void latePredicatesValidatePendingReplacement(boolean compatible) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var registry = SemanticEvaluations.get(context);
+ var original = new SemanticEvaluation("detect", "content", null, Map.of());
+ var candidate = new SemanticEvaluation("detect", "content", "${header.candidate}", Map.of());
+ var expert = new ContentExpert() {
+ int candidateValidations;
+ boolean attempted;
+ Predicate latePredicate;
+
+ @Override
+ public void validate(SemanticEvaluation evaluation) {
+ // First validate the declaration; then enter the already-copied expression validators.
+ if (evaluation == candidate && ++candidateValidations == 2) {
+ attempted = true;
+ assertThat(registry.get("target")).isSameAs(original);
+ latePredicate = context.resolveLanguage("semantic").createPredicate("ref:target");
+ }
+ }
+ };
+ context.getRegistry().bind("content", expert);
+ registry.replace("test", Map.of("outer", original, "target", original));
+ context.start();
+ var language = context.resolveLanguage("semantic");
+ var outer = language.createExpression("ref:outer");
+ var target = compatible
+ ? new SemanticEvaluation("detect", "content", null, Map.of("threshold", 0.9))
+ : new SemanticEvaluation("rank", "content", null, Map.of());
+ Map<String, SemanticEvaluation> replacement = Map.of("outer", candidate, "target", target);
+ var exchange = new DefaultExchange(context);
+ exchange.getMessage().setBody("published state");
+ exchange.getMessage().setHeader("candidate", "replacement state");
+ if (compatible) {
+ registry.replace("test", replacement);
+ assertThat(registry.get("target")).isSameAs(target);
+ assertThat(expert.latePredicate.matches(exchange)).isFalse();
+ } else {
+ assertThatThrownBy(() -> registry.replace("test", replacement))
+ .hasMessageContaining("Semantic predicate requires a boolean evaluation: target");
+ assertThat(registry.get("outer")).isSameAs(original);
+ assertThat(registry.get("target")).isSameAs(original);
+ assertThat(expert.latePredicate).isNull();
+ }
+ assertThat(expert.attempted).isTrue();
+ assertThat(outer.evaluate(exchange, Boolean.class)).isTrue();
+ // Both success and failure must clear the pending replacement before later registrations.
+ registry.replace("test", Map.of("outer", original, "target", original));
+ assertThat(language.createPredicate("ref:target").matches(exchange)).isTrue();
+ }
+ }
+
@ParameterizedTest
@ValueSource(strings = {
"${semantic('inner')} ready",
(camel) 04/08: CAMEL-25382: Reduce semantic registry and declaration overhead
Posted by ld...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 6a54949082794d582759af5f10d7b67c1a4fa67d
Author: Luigi De Masi <ld...@redhat.com>
AuthorDate: Wed Oct 7 17:13:52 2026 +0200
CAMEL-25382: Reduce semantic registry and declaration overhead
Return an existing evaluation registry before acquiring the private
creation lock, retaining the second check for concurrent creation and
avoiding public CamelContext monitors.
Copy nested parameter maps recursively in one pass without an intermediate
map. Preserve blank-key and string-key validation and deep immutability.
Cover cached lookup during another context's blocked registry lookup,
invalid nested keys, immutable containers and Java builder diagnostics.
Co-authored-by: Codex <no...@openai.com>
Signed-off-by: Luigi De Masi <ld...@redhat.com>
---
.../apache/camel/semantic/SemanticEvaluation.java | 7 +++--
.../apache/camel/semantic/SemanticEvaluations.java | 16 +++++++----
.../camel/semantic/SemanticCapabilitiesTest.java | 20 ++++++++++++++
.../semantic/SemanticEvaluationBuilderTest.java | 10 +++++++
.../camel/semantic/SemanticInitializationTest.java | 32 ++++++++++++++++++++++
5 files changed, 77 insertions(+), 8 deletions(-)
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java
index 3125cfee27c3..3ef2b236e3ce 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java
@@ -69,9 +69,12 @@ public final class SemanticEvaluation {
if (!(key instanceof String name)) {
throw new IllegalArgumentException("Parameter maps require string keys");
}
- copy.put(name, entry);
+ if (name.isBlank()) {
+ throw new IllegalArgumentException("Parameter names must not be blank");
+ }
+ copy.put(name, immutableValue(entry));
});
- return immutableMap(copy);
+ return Collections.unmodifiableMap(copy);
}
if (value instanceof List<?> list) {
List<Object> copy = new ArrayList<>();
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
index a7fb706f27ea..80816e246840 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java
@@ -57,14 +57,18 @@ public final class SemanticEvaluations {
}
public static SemanticEvaluations get(CamelContext context) {
- synchronized (CREATION_LOCK) {
- SemanticEvaluations answer = context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class);
- if (answer == null) {
- answer = new SemanticEvaluations(context);
- context.getCamelContextExtension().addContextPlugin(SemanticEvaluations.class, answer);
+ var extension = context.getCamelContextExtension();
+ SemanticEvaluations answer = extension.getContextPlugin(SemanticEvaluations.class);
+ if (answer == null) {
+ synchronized (CREATION_LOCK) {
+ answer = extension.getContextPlugin(SemanticEvaluations.class);
+ if (answer == null) {
+ answer = new SemanticEvaluations(context);
+ extension.addContextPlugin(SemanticEvaluations.class, answer);
+ }
}
- return answer;
}
+ return answer;
}
/**
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java
index 8cfba855aca3..7cf60c8d3a16 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.semantic;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -79,6 +80,25 @@ class SemanticCapabilitiesTest {
assertThat(evaluation.getParameters()).doesNotContainKey("threshold");
assertThat(evaluation.getParameters().get("policy")).isEqualTo(Map.of("labels", List.of("privacy")));
assertThatThrownBy(() -> evaluation.getParameters().clear()).isInstanceOf(UnsupportedOperationException.class);
+ var frozenPolicy = (Map<?, ?>) evaluation.getParameters().get("policy");
+ var frozenLabels = (List<?>) frozenPolicy.get("labels");
+ assertThatThrownBy(frozenPolicy::clear).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(frozenLabels::clear).isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ @Test
+ void nestedParameterMapsRejectInvalidKeys() {
+ for (Object key : Arrays.asList("", " \t", null, 1)) {
+ Map<Object, Object> invalid = new LinkedHashMap<>();
+ invalid.put(key, true);
+ for (Object value : List.of(invalid, List.of(invalid))) {
+ assertThatThrownBy(() -> new SemanticEvaluation("custom", null, null, Map.of("policy", value)))
+ .isExactlyInstanceOf(IllegalArgumentException.class)
+ .hasMessage(key instanceof String
+ ? "Parameter names must not be blank"
+ : "Parameter maps require string keys");
+ }
+ }
}
@Test
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEvaluationBuilderTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEvaluationBuilderTest.java
index 5be99c852b98..7712886a4bdf 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEvaluationBuilderTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEvaluationBuilderTest.java
@@ -116,6 +116,16 @@ class SemanticEvaluationBuilderTest {
}
}
+ @Test
+ void nonStringNestedKeysFailWithValidationError() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ var builder = new SemanticEvaluationBuilder().operation("custom").parameter("policy", Map.of(1, true));
+ assertThatThrownBy(() -> builder.build(context))
+ .isExactlyInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Parameter maps require string keys");
+ }
+ }
+
@Test
void nullPolicyIsRejectedWithContractContext() throws Exception {
try (var context = new DefaultCamelContext()) {
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
index 6599143d05f5..12ccaabcd399 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
@@ -76,6 +76,38 @@ class SemanticInitializationTest {
}
}
+ @Test
+ void cachedRegistryLookupDoesNotWaitForAnotherContext() throws Exception {
+ ExecutorService callers = Executors.newFixedThreadPool(2);
+ CountDownLatch entered = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ try (var creating = new DefaultCamelContext(); var cached = new DefaultCamelContext()) {
+ var expected = SemanticEvaluations.get(cached);
+ creating.getCamelContextExtension().lazyAddContextPlugin(SemanticEvaluations.class, () -> {
+ entered.countDown();
+ try {
+ assertThat(release.await(30, TimeUnit.SECONDS)).isTrue();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ return null;
+ });
+ var creation = callers.submit(() -> SemanticEvaluations.get(creating));
+ try {
+ assertThat(entered.await(10, TimeUnit.SECONDS)).isTrue();
+ var lookup = callers.submit(() -> SemanticEvaluations.get(cached));
+ assertThat(lookup.get(10, TimeUnit.SECONDS)).isSameAs(expected);
+ } finally {
+ release.countDown();
+ assertThat(creation.get(10, TimeUnit.SECONDS)).isNotNull();
+ }
+ } finally {
+ release.countDown();
+ callers.shutdownNow();
+ }
+ }
+
@Test
void concurrentLanguagesCannotOverwriteTheAdapterOwner() throws Exception {
ExecutorService callers = Executors.newFixedThreadPool(2);
(camel) 06/08: CAMEL-25382: Address semantic validation review nits
Posted by ld...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 03d215da95cf3119035e0a58c77195e12b6394f4
Author: Luigi De Masi <ld...@redhat.com>
AuthorDate: Wed Oct 7 20:10:37 2026 +0200
CAMEL-25382: Address semantic validation review nits
Recognize the stable missing-dependency phrase in JBang diagnostics instead
of requiring the entire error message. Cover changed surrounding wording
through the source validator while preserving malformed-argument checks.
Explain why the concurrent registry test returns no lazy plugin: it allows
SemanticEvaluations.get() to create and register the instance under test.
Co-authored-by: Codex <no...@openai.com>
Signed-off-by: Luigi De Masi <ld...@redhat.com>
---
.../camel/semantic/SemanticInitializationTest.java | 1 +
.../dsl/jbang/core/commands/ai/SimpleChecks.java | 2 +-
.../commands/ai/SourceValidatorSimpleTest.java | 23 +++++++++++++++++++++-
3 files changed, 24 insertions(+), 2 deletions(-)
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
index 12ccaabcd399..ef47233e353f 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java
@@ -91,6 +91,7 @@ class SemanticInitializationTest {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
+ // Leave the plugin absent so get() creates and registers it after this lookup is released.
return null;
});
var creation = callers.submit(() -> SemanticEvaluations.get(creating));
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
index 5d71ae876aab..77425ddd647c 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
@@ -223,7 +223,7 @@ final class SimpleChecks {
static boolean isMissingDependency(String error) {
return error.startsWith("No language could be found for:")
- || error.equals("The semantic function requires camel-semantic on the classpath")
+ || error.contains("requires camel-semantic")
|| error.startsWith("No " + SimpleLanguageFunctionFactory.FACTORY + "/")
&& error.contains("service could be found in the classpath");
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
index 13a0917d8020..9a16a68d2368 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
@@ -20,6 +20,7 @@ import java.util.List;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.catalog.LanguageValidationResult;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -255,10 +256,30 @@ class SourceValidatorSimpleTest {
assertThat(msgs).isEmpty();
}
+ @Test
+ void semanticDependencyDetectionToleratesSurroundingDiagnosticChanges() {
+ CamelCatalog changedDiagnostic = new DefaultCamelCatalog() {
+ @Override
+ public LanguageValidationResult validateLanguageExpression(ClassLoader classLoader, String language, String text) {
+ LanguageValidationResult result = new LanguageValidationResult(text);
+ result.setError("Semantic evaluation requires camel-semantic; add the dependency to use this function");
+ return result;
+ }
+ };
+ List<String> msgs = SourceValidator.validateYamlSimple("""
+ - from:
+ uri: direct:start
+ steps:
+ - setBody:
+ simple: "${semantic('department')}"
+ """, changedDiagnostic);
+ assertThat(msgs).isEmpty();
+ }
+
@Test
void semanticExpressionsAndPredicatesDoNotRequireTheRuntimeDependency() {
assertThat(catalog.validateLanguageExpression(null, "simple", "${semantic('department')}").getShortError())
- .isEqualTo("The semantic function requires camel-semantic on the classpath");
+ .contains("requires camel-semantic");
List<String> msgs = SourceValidator.validateYamlSimple("""
- from:
uri: direct:start