Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,16 @@ public abstract class CorpusWorkFlowSuite {
public static final String API_VALIDATE_NEGOTIATION_REJECT = "validateRejectPromptAndDataFilling";
public static final String API_VALIDATE_NEGOTIATION_ABORT = "validateAbortPromptAndDataFilling";

public static final String API_GENERATE_NOTIFICATION_FROM_TEXT = "generateNotificationPromptFromText";
public static final String API_GENERATE_NOTIFICATION_FROM_DATA = "generateNotificationPromptFromDataWithSchema";
public static final String API_VALIDATE_NOTIFICATION = "validateNotificationPromptAndDataFilling";

private static volatile SdkRuntimeAssembler.Runtime taskRuntimeSingleton;

private static volatile SdkRuntimeAssembler.NegotiationRuntime negotiationRuntimeSingleton;

private static volatile SdkRuntimeAssembler.NotificationRuntime notificationRuntimeSingleton;

private final List<WorkflowEngine.CaseResult> results = new ArrayList<>();

private final Map<String, Path> scenarioDirs = new LinkedHashMap<>();
Expand All @@ -76,7 +82,7 @@ public abstract class CorpusWorkFlowSuite {
/** Flow type used for the transcript and summary file names: {@code from_text} or {@code from_data}. */
protected abstract String flowType();

/** Runtime kind: {@code "task"} (default) or {@code "negotiation"}. */
/** Runtime kind: {@code "task"} (default), {@code "negotiation"} or {@code "notification"}. */
protected String runtimeKind() {
return "task";
}
Expand All @@ -90,6 +96,9 @@ Stream<DynamicTest> corpusCases() {
if ("negotiation".equals(runtimeKind())) {
SdkRuntimeAssembler.NegotiationRuntime rt = negotiationRuntime();
registry = SdkRuntimeAssembler.buildNegotiationRegistry(rt);
} else if ("notification".equals(runtimeKind())) {
SdkRuntimeAssembler.NotificationRuntime rt = notificationRuntime();
registry = SdkRuntimeAssembler.buildNotificationRegistry(rt);
} else {
SdkRuntimeAssembler.Runtime rt = taskRuntime();
registry = SdkRuntimeAssembler.buildTaskRegistry(rt);
Expand Down Expand Up @@ -151,10 +160,20 @@ private static synchronized SdkRuntimeAssembler.NegotiationRuntime negotiationRu
return negotiationRuntimeSingleton;
}

private static synchronized SdkRuntimeAssembler.NotificationRuntime notificationRuntime() {
if (notificationRuntimeSingleton == null) {
notificationRuntimeSingleton = SdkRuntimeAssembler.notificationRuntime();
}
return notificationRuntimeSingleton;
}

private RecordingMeteredLlmClient registryRecorder() {
if ("negotiation".equals(runtimeKind())) {
return negotiationRuntime().recorder();
}
if ("notification".equals(runtimeKind())) {
return notificationRuntime().recorder();
}
return taskRuntime().recorder();
}

Expand Down Expand Up @@ -188,4 +207,12 @@ public static Set<String> negotiationApiNames() {
API_VALIDATE_NEGOTIATION_REJECT,
API_VALIDATE_NEGOTIATION_ABORT);
}

/** Notification-T phase-1 API names, used for load-time membership checks without touching the LLM runtime. */
public static Set<String> notificationApiNames() {
return Set.of(
API_GENERATE_NOTIFICATION_FROM_TEXT,
API_GENERATE_NOTIFICATION_FROM_DATA,
API_VALIDATE_NOTIFICATION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package net.openan.a2at.sdk.corpus.engine.registry;

import net.openan.a2at.sdk.client.prompt.orchestration.ClientPromptGenerationOrchestrator;
import net.openan.a2at.sdk.core.validation.ContentValidator;

/**
* Notification-T API registration: JSON step arguments are bound to the production
* {@link ClientPromptGenerationOrchestrator} (from-text / from-data generation) and the Notification-T
* {@link ContentValidator} (validation) assembled by {@link SdkRuntimeAssembler}.
*/
public final class NotificationApis {

private NotificationApis() {}

public static void registerNotificationApis(
ApiRegistry registry,
ClientPromptGenerationOrchestrator client,
ContentValidator notificationValidator) {
registry.register("generateNotificationPromptFromText", args ->
ClientApis.metadataToMap(client.generateNotificationPromptFromText(
ClientApis.Args.text(args, "text"), ClientApis.Args.templateUri(args, "templateUri"))));
registry.register("generateNotificationPromptFromDataWithSchema", args ->
ClientApis.metadataToMap(client.generateNotificationPromptFromDataWithSchema(
ClientApis.Args.map(args, "data"),
ClientApis.Args.map(args, "schema"),
ClientApis.Args.templateUri(args, "templateUri"))));
registry.register("validateNotificationPromptAndDataFilling", args ->
ServerApis.filledToMap(notificationValidator.validate(
ClientApis.Args.text(args, "promptText"),
ClientApis.Args.map(args, "schema"),
ClientApis.Args.templateUri(args, "templateUri"))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ public record NegotiationRuntime(
RecordingMeteredLlmClient recorder,
NegotiationGenerationOrchestrator orchestrator) {}

/** Fully assembled Notification-T runtime with the single shared recording LLM client. */
public record NotificationRuntime(
RecordingMeteredLlmClient recorder,
ClientPromptGenerationOrchestrator clientGeneration,
ContentValidator notificationValidator) {}

private SdkRuntimeAssembler() {}

private static RecordingMeteredLlmClient newRecorder(CorpusEnvConfig env) {
Expand Down Expand Up @@ -122,4 +128,49 @@ public static ApiRegistry buildNegotiationRegistry(NegotiationRuntime runtime) {
NegotiationApis.registerNegotiationApis(registry, runtime.orchestrator());
return registry;
}

public static NotificationRuntime notificationRuntime() {
CorpusEnvConfig env = CorpusEnvConfig.load();
RecordingMeteredLlmClient recorder = newRecorder(env);

LlmConfig llmConfig = new LlmConfig(
env.provider(),
env.model(),
env.apiKey(),
env.baseUrl(),
10,
env.maxTokens(),
env.temperature(),
env.timeoutSeconds(),
300,
100,
false,
null,
env.maxAttempts(),
List.of());
A2ATConfig config = new A2ATConfig(
new PromptRuntimeConfig("zh-CN", PromptRuntimeConfig.SOURCE_TYPE_CLASSPATH, null),
llmConfig,
new InputLimitConfig(InputLimitConfig.DEFAULT_MAX_TEXT_CHARS),
new NegotiationConfig("in_memory"));

ClientPromptGenerationOrchestrator clientGeneration = DefaultA2ATClientBuilder.builder()
.config(config)
.envPath(env.envPath())
.llmClient(recorder)
.buildPromptGenerationOrchestrator();
ContentValidator notificationValidator = DefaultA2ATServerBuilder.builder()
.config(config)
.envPath(env.envPath())
.llmClient(recorder)
.buildNotificationContentValidator();

return new NotificationRuntime(recorder, clientGeneration, notificationValidator);
}

public static ApiRegistry buildNotificationRegistry(NotificationRuntime runtime) {
ApiRegistry registry = new ApiRegistry();
NotificationApis.registerNotificationApis(registry, runtime.clientGeneration(), runtime.notificationValidator());
return registry;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package net.openan.a2at.sdk.corpus.suites.notification;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.List;
import net.openan.a2at.sdk.corpus.engine.CorpusWorkFlowSuite;
import net.openan.a2at.sdk.corpus.engine.discover.ScenarioScanner;
import net.openan.a2at.sdk.corpus.engine.loader.CaseFileLoader;
import net.openan.a2at.sdk.corpus.engine.loader.ExpectedStep;
import net.openan.a2at.sdk.corpus.engine.loader.InputCase;
import org.junit.jupiter.api.Test;

/**
* Notification-T self-guard (meta test): validates every Notification-T scenario case file structurally without any
* LLM call - unknown keys, misaligned expectations, unknown api names, out-of-catalog error codes and
* category/expectation contradictions all fail here.
*
* <p>Id uniqueness is enforced per-file by the {@link CaseFileLoader}; ids may repeat across different flow files
* within the same scenario.
*/
public final class NotificationSelfGuardTest {

/** Categories carrying a cross-step expectation contract (case data is Chinese). */
static final String CATEGORY_NORMAL = "正常";

static final String CATEGORY_EXCEPTION = "异常";

@Test
void notificationScenarioCaseFilesAreStructurallyValid() {
List<String> problems = new ArrayList<>();
for (String flowFile : List.of(
CorpusWorkFlowSuite.INPUT_CASE_FROM_TEXT, CorpusWorkFlowSuite.INPUT_CASE_FROM_DATA)) {
for (ScenarioScanner.Scenario scenario : ScenarioScanner.discover("notification", flowFile)) {
List<InputCase> cases = new CaseFileLoader()
.load(scenario.inputFile(flowFile), CorpusWorkFlowSuite.notificationApiNames());
for (InputCase inputCase : cases) {
checkCategoryContract(inputCase, problems);
}
}
}
assertTrue(problems.isEmpty(), "Notification corpus self-guard failures:\n- " + String.join("\n- ", problems));
}

private static void checkCategoryContract(InputCase inputCase, List<String> problems) {
long errorExpected = 0;
for (ExpectedStep expectedStep : inputCase.expected()) {
if ("error".equals(expectedStep.result())) {
errorExpected++;
if (expectedStep.errCode().isBlank()) {
problems.add(inputCase.id() + ": error expectations must declare an errCode from the SDK error catalog");
}
}
}
if (CATEGORY_EXCEPTION.equals(inputCase.caseCategory()) && errorExpected == 0) {
problems.add(inputCase.id() + ": categorized as " + CATEGORY_EXCEPTION + " but no step expects an error");
}
if (CATEGORY_NORMAL.equals(inputCase.caseCategory()) && errorExpected > 0) {
problems.add(inputCase.id() + ": categorized as " + CATEGORY_NORMAL + " but a step expects an error; use "
+ CATEGORY_EXCEPTION + " or \"boundary\"");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package net.openan.a2at.sdk.corpus.suites.notification;

import java.util.Set;
import net.openan.a2at.sdk.corpus.engine.CorpusWorkFlowSuite;

/**
* Notification-T structured-input workflow suite: for every scenario directory under
* {@code suites/notification/resources/}, runs the recorded SDK API flow of each case in
* {@code input_case_from_data.json} against a real LLM.
*
* <p>Default API flow (also shown in the case files): {@code generateNotificationPromptFromDataWithSchema} →
* {@code validateNotificationPromptAndDataFilling}.
*
* <p>Filters: {@code -Dcorpus.scenario} and {@code -Dcase.filter}, see {@link NotificationTFromTextWorkFlowTest}.
*/
public final class NotificationTFromDataWorkFlowTest extends CorpusWorkFlowSuite {

@Override
protected String extensionFolder() {
return "notification";
}

@Override
protected String inputFileName() {
return INPUT_CASE_FROM_DATA;
}

@Override
protected String flowType() {
return FLOW_FROM_DATA;
}

@Override
protected String runtimeKind() {
return "notification";
}

@Override
protected Set<String> apiNames() {
return notificationApiNames();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.openan.a2at.sdk.corpus.suites.notification;

import java.util.Set;
import net.openan.a2at.sdk.corpus.engine.CorpusWorkFlowSuite;

/**
* Notification-T natural-language workflow suite: for every scenario directory under
* {@code suites/notification/resources/}, runs the recorded SDK API flow of each case in
* {@code input_case_from_text.json} against a real LLM.
*
* <p>Default API flow (also shown in the case files): {@code generateNotificationPromptFromText} →
* {@code validateNotificationPromptAndDataFilling}.
*
* <p>Filters: {@code -Dcorpus.scenario=service-recovery} (glob, comma-separated) and
* {@code -Dcase.filter=TC00000001} (glob on case id).
*/
public final class NotificationTFromTextWorkFlowTest extends CorpusWorkFlowSuite {

@Override
protected String extensionFolder() {
return "notification";
}

@Override
protected String inputFileName() {
return INPUT_CASE_FROM_TEXT;
}

@Override
protected String flowType() {
return FLOW_FROM_TEXT;
}

@Override
protected String runtimeKind() {
return "notification";
}

@Override
protected Set<String> apiNames() {
return notificationApiNames();
}
}
Loading
Loading