feat(graph): add diagnosis routing skeleton
This commit is contained in:
@@ -0,0 +1,609 @@
|
||||
package com.superbiz.agent.graph.diagnosis;
|
||||
|
||||
import com.alibaba.cloud.ai.graph.CompiledGraph;
|
||||
import com.alibaba.cloud.ai.graph.OverAllState;
|
||||
import com.alibaba.cloud.ai.graph.RunnableConfig;
|
||||
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.ExecutorStatus;
|
||||
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.GatekeeperStatus;
|
||||
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.PlannerMode;
|
||||
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.PlannerStatus;
|
||||
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.Verdict;
|
||||
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.VerifierStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
class DiagnosisGraphRoutingTest {
|
||||
|
||||
private static final String THREAD_ID = "run-routing-test";
|
||||
|
||||
@Test
|
||||
void normalPassPathUsesCompiledGraphAndPreservesEventOrder() throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
planner(script, PlannerStatus.COMPLETED);
|
||||
executor(script, ExecutorStatus.COMPLETED);
|
||||
gatekeeper(script, GatekeeperStatus.PASS, 1);
|
||||
verifiedInput(script);
|
||||
verifier(script, VerifierStatus.COMPLETED, Verdict.PASS,
|
||||
Verdict.PASS, Map.of());
|
||||
composerCompleted(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(List.of(
|
||||
DiagnosisGraphTopology.Node.PLANNER,
|
||||
DiagnosisGraphTopology.Node.EXECUTOR,
|
||||
DiagnosisGraphTopology.Node.GATEKEEPER,
|
||||
DiagnosisGraphTopology.Node.VERIFIED_INPUT,
|
||||
DiagnosisGraphTopology.Node.VERIFIER,
|
||||
DiagnosisGraphTopology.Node.COMPOSER),
|
||||
script.sequence());
|
||||
script.sequence().forEach(node -> assertEquals(
|
||||
List.of(THREAD_ID),
|
||||
script.threadIds(node),
|
||||
"threadId for " + node));
|
||||
assertEquals(6, DiagnosisGraphState.listValue(
|
||||
state, DiagnosisGraphState.ORCHESTRATION_EVENTS).size());
|
||||
DiagnosisOrchestrationTrace trace =
|
||||
new DiagnosisOrchestrationTraceBuilder().build(state);
|
||||
assertEquals(DiagnosisGraphTopology.Node.COMPOSER, trace.finalNode());
|
||||
assertFalse(trace.degraded());
|
||||
assertEquals(5, trace.transitions().size());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"INVALID_OUTPUT", "RETRYABLE_FAILED"})
|
||||
void plannerTechnicalFailureRetriesOnlyPlannerOnce(String firstStatus)
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
planner(script, firstStatus);
|
||||
planner(script, PlannerStatus.COMPLETED);
|
||||
appendNormalAfterPlanner(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(List.of(
|
||||
DiagnosisGraphTopology.Node.PLANNER,
|
||||
DiagnosisGraphTopology.Node.PLANNER,
|
||||
DiagnosisGraphTopology.Node.EXECUTOR,
|
||||
DiagnosisGraphTopology.Node.GATEKEEPER,
|
||||
DiagnosisGraphTopology.Node.VERIFIED_INPUT,
|
||||
DiagnosisGraphTopology.Node.VERIFIER,
|
||||
DiagnosisGraphTopology.Node.COMPOSER),
|
||||
script.sequence());
|
||||
assertEquals(1, DiagnosisGraphState.intValue(
|
||||
state, DiagnosisGraphState.PLANNER_RETRY_COUNT));
|
||||
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.PLANNER));
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.EXECUTOR));
|
||||
}
|
||||
|
||||
@Test
|
||||
void plannerSecondTechnicalFailureFallsBackWithoutExecutor()
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
planner(script, PlannerStatus.INVALID_OUTPUT);
|
||||
planner(script, PlannerStatus.RETRYABLE_FAILED);
|
||||
fallback(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(List.of(
|
||||
DiagnosisGraphTopology.Node.PLANNER,
|
||||
DiagnosisGraphTopology.Node.PLANNER,
|
||||
DiagnosisGraphTopology.Node.FALLBACK),
|
||||
script.sequence());
|
||||
assertEquals(1, DiagnosisGraphState.intValue(
|
||||
state, DiagnosisGraphState.PLANNER_RETRY_COUNT));
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.EXECUTOR));
|
||||
assertFallbackTrace(state);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"NON_RETRYABLE_FAILED", "UNKNOWN"})
|
||||
void plannerNonRetryableOrUnknownStatusFailsClosed(String status)
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
planner(script, status);
|
||||
fallback(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(List.of(
|
||||
DiagnosisGraphTopology.Node.PLANNER,
|
||||
DiagnosisGraphTopology.Node.FALLBACK),
|
||||
script.sequence());
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.EXECUTOR));
|
||||
assertFallbackTrace(state);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"INVALID_OUTPUT", "TOOL_BLOCKED", "FAILED", "UNKNOWN"
|
||||
})
|
||||
void executorFailureNeverRetriesOrInvokesGatekeeper(String status)
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
planner(script, PlannerStatus.COMPLETED);
|
||||
executor(script, status);
|
||||
fallback(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(List.of(
|
||||
DiagnosisGraphTopology.Node.PLANNER,
|
||||
DiagnosisGraphTopology.Node.EXECUTOR,
|
||||
DiagnosisGraphTopology.Node.FALLBACK),
|
||||
script.sequence());
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.EXECUTOR));
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.GATEKEEPER));
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
|
||||
assertFallbackTrace(state);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executorCompletedNoEvidenceStillInvokesGatekeeper() throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
planner(script, PlannerStatus.COMPLETED);
|
||||
script.step(
|
||||
DiagnosisGraphTopology.Node.EXECUTOR,
|
||||
ExecutorStatus.COMPLETED.name(),
|
||||
"legal_no_evidence",
|
||||
Map.of(
|
||||
DiagnosisGraphState.EXECUTOR_STATUS,
|
||||
ExecutorStatus.COMPLETED.name(),
|
||||
DiagnosisGraphState.EXECUTOR_OUTPUT,
|
||||
Map.of("claims", List.of())));
|
||||
gatekeeper(script, GatekeeperStatus.PASS, 0);
|
||||
verifiedInput(script);
|
||||
verifier(script, VerifierStatus.COMPLETED, Verdict.PASS,
|
||||
Verdict.PASS, Map.of());
|
||||
composerCompleted(script);
|
||||
|
||||
run(script);
|
||||
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.GATEKEEPER));
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("gatekeeperFallbackCases")
|
||||
void gatekeeperUnsafeOutcomesSkipVerifier(
|
||||
String status,
|
||||
int verifiedBindingCount) throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
planner(script, PlannerStatus.COMPLETED);
|
||||
executor(script, ExecutorStatus.COMPLETED);
|
||||
gatekeeper(script, status, verifiedBindingCount);
|
||||
fallback(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.VERIFIED_INPUT));
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
|
||||
assertFallbackTrace(state);
|
||||
}
|
||||
|
||||
@Test
|
||||
void gatekeeperLowConfidenceWithVerifiedBindingRunsVerifiedInput()
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
planner(script, PlannerStatus.COMPLETED);
|
||||
executor(script, ExecutorStatus.COMPLETED);
|
||||
gatekeeper(script, GatekeeperStatus.LOW_CONFID, 1);
|
||||
verifiedInput(script);
|
||||
verifier(script, VerifierStatus.COMPLETED, Verdict.LOW_CONFID,
|
||||
Verdict.LOW_CONFID, Map.of());
|
||||
composerCompleted(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.VERIFIED_INPUT));
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.EVIDENCE_RETRY));
|
||||
assertEquals(Verdict.LOW_CONFID,
|
||||
DiagnosisGraphState.enumValue(
|
||||
state,
|
||||
DiagnosisGraphState.EFFECTIVE_VERDICT,
|
||||
Verdict.class)
|
||||
.orElseThrow());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"INVALID_OUTPUT", "RETRYABLE_FAILED"})
|
||||
void verifierTechnicalFailureRetriesOnlyVerifier(String firstStatus)
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
appendThroughVerifiedInput(script);
|
||||
verifierFailure(script, firstStatus);
|
||||
verifier(script, VerifierStatus.COMPLETED, Verdict.PASS,
|
||||
Verdict.PASS, Map.of());
|
||||
composerCompleted(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.GATEKEEPER));
|
||||
assertEquals(1, DiagnosisGraphState.intValue(
|
||||
state, DiagnosisGraphState.VERIFIER_RETRY_COUNT));
|
||||
assertEquals(0, DiagnosisGraphState.intValue(
|
||||
state, DiagnosisGraphState.EVIDENCE_RETRY_COUNT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifierSecondTechnicalFailureFallsBack() throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
appendThroughVerifiedInput(script);
|
||||
verifierFailure(script, VerifierStatus.INVALID_OUTPUT.name());
|
||||
verifierFailure(script, VerifierStatus.RETRYABLE_FAILED.name());
|
||||
fallback(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
|
||||
assertEquals(1, DiagnosisGraphState.intValue(
|
||||
state, DiagnosisGraphState.VERIFIER_RETRY_COUNT));
|
||||
assertFallbackTrace(state);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"NON_RETRYABLE_FAILED", "UNKNOWN"})
|
||||
void verifierNonRetryableOrUnknownStatusFailsClosed(String status)
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
appendThroughVerifiedInput(script);
|
||||
verifierFailure(script, status);
|
||||
fallback(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
|
||||
assertFallbackTrace(state);
|
||||
}
|
||||
|
||||
@Test
|
||||
void evidenceRetryRunsOnceAndResetsPlannerStageCounter()
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
planner(script, PlannerStatus.INVALID_OUTPUT);
|
||||
planner(script, PlannerStatus.COMPLETED);
|
||||
appendExecutorThroughVerifiedInput(script);
|
||||
verifier(script, VerifierStatus.COMPLETED, Verdict.LOW_CONFID,
|
||||
Verdict.PASS, verifierOutputWithGap());
|
||||
evidenceRetry(script);
|
||||
|
||||
planner(script, PlannerStatus.INVALID_OUTPUT);
|
||||
planner(script, PlannerStatus.COMPLETED);
|
||||
appendExecutorThroughVerifiedInput(script);
|
||||
verifier(script, VerifierStatus.COMPLETED, Verdict.LOW_CONFID,
|
||||
Verdict.PASS, verifierOutputWithGap());
|
||||
composerCompleted(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(4, script.calls(DiagnosisGraphTopology.Node.PLANNER));
|
||||
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.EXECUTOR));
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.EVIDENCE_RETRY));
|
||||
assertEquals(1, DiagnosisGraphState.intValue(
|
||||
state, DiagnosisGraphState.EVIDENCE_RETRY_COUNT));
|
||||
assertEquals(1, DiagnosisGraphState.intValue(
|
||||
state, DiagnosisGraphState.PLANNER_RETRY_COUNT));
|
||||
assertEquals(0, DiagnosisGraphState.intValue(
|
||||
state, DiagnosisGraphState.VERIFIER_RETRY_COUNT));
|
||||
assertEquals(PlannerMode.EVIDENCE_GAP_ONLY,
|
||||
DiagnosisGraphState.enumValue(
|
||||
state,
|
||||
DiagnosisGraphState.PLANNER_MODE,
|
||||
PlannerMode.class)
|
||||
.orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifierLowConfidenceWithoutGapDoesNotRetryEvidence()
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
appendThroughVerifiedInput(script);
|
||||
verifier(script, VerifierStatus.COMPLETED, Verdict.LOW_CONFID,
|
||||
Verdict.PASS, Map.of("facts_checked", List.of(
|
||||
Map.of("fact", "cpu is high",
|
||||
"verification", "direct_support"))));
|
||||
composerCompleted(script);
|
||||
|
||||
run(script);
|
||||
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.EVIDENCE_RETRY));
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifierRejectStillRunsComposerWithSafeMaterial() throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
appendThroughVerifiedInput(script);
|
||||
verifier(script, VerifierStatus.COMPLETED, Verdict.REJECT,
|
||||
Verdict.PASS, Map.of());
|
||||
composerCompleted(script);
|
||||
|
||||
run(script);
|
||||
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.FALLBACK));
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifierCompletedWithoutEffectiveVerdictFailsClosed()
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
appendThroughVerifiedInput(script);
|
||||
verifier(script, VerifierStatus.COMPLETED, null,
|
||||
Verdict.PASS, Map.of());
|
||||
fallback(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
|
||||
assertFallbackTrace(state);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"INVALID_OUTPUT", "RETRYABLE_FAILED"})
|
||||
void composerTechnicalFailureRetriesOnlyComposer(String firstStatus)
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
appendThroughCompletedVerifier(script);
|
||||
composer(script, firstStatus);
|
||||
composerCompleted(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
|
||||
assertEquals(1, DiagnosisGraphState.intValue(
|
||||
state, DiagnosisGraphState.COMPOSER_RETRY_COUNT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void composerSecondTechnicalFailureFallsBackAndTerminates()
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
appendThroughCompletedVerifier(script);
|
||||
composer(script, "INVALID_OUTPUT");
|
||||
composer(script, "RETRYABLE_FAILED");
|
||||
fallback(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.FALLBACK));
|
||||
assertEquals(1, DiagnosisGraphState.intValue(
|
||||
state, DiagnosisGraphState.COMPOSER_RETRY_COUNT));
|
||||
assertFallbackTrace(state);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"NON_RETRYABLE_FAILED", "UNKNOWN"})
|
||||
void composerNonRetryableOrUnknownStatusFailsClosed(String status)
|
||||
throws Exception {
|
||||
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
|
||||
appendThroughCompletedVerifier(script);
|
||||
composer(script, status);
|
||||
fallback(script);
|
||||
|
||||
OverAllState state = run(script);
|
||||
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
|
||||
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.FALLBACK));
|
||||
assertFallbackTrace(state);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> gatekeeperFallbackCases() {
|
||||
return Stream.of(
|
||||
Arguments.of(GatekeeperStatus.REJECT.name(), 1),
|
||||
Arguments.of(GatekeeperStatus.LOW_CONFID.name(), 0),
|
||||
Arguments.of("UNKNOWN", 1));
|
||||
}
|
||||
|
||||
private OverAllState run(ScriptedDiagnosisGraphActions script)
|
||||
throws Exception {
|
||||
CompiledGraph graph = new DiagnosisGraphFactory().compile(script.actions());
|
||||
assertEquals(DiagnosisGraphTopology.RECURSION_LIMIT,
|
||||
graph.getMaxIterations());
|
||||
return graph.invoke(
|
||||
initialState(),
|
||||
RunnableConfig.builder().threadId(THREAD_ID).build())
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
private Map<String, Object> initialState() {
|
||||
return Map.of(
|
||||
DiagnosisGraphState.PLANNER_RETRY_COUNT, 0,
|
||||
DiagnosisGraphState.VERIFIER_RETRY_COUNT, 0,
|
||||
DiagnosisGraphState.COMPOSER_RETRY_COUNT, 0,
|
||||
DiagnosisGraphState.EVIDENCE_RETRY_COUNT, 0,
|
||||
DiagnosisGraphState.PLANNER_MODE, PlannerMode.NORMAL.name());
|
||||
}
|
||||
|
||||
private void appendNormalAfterPlanner(
|
||||
ScriptedDiagnosisGraphActions script) {
|
||||
executor(script, ExecutorStatus.COMPLETED);
|
||||
gatekeeper(script, GatekeeperStatus.PASS, 1);
|
||||
verifiedInput(script);
|
||||
verifier(script, VerifierStatus.COMPLETED, Verdict.PASS,
|
||||
Verdict.PASS, Map.of());
|
||||
composerCompleted(script);
|
||||
}
|
||||
|
||||
private void appendThroughVerifiedInput(
|
||||
ScriptedDiagnosisGraphActions script) {
|
||||
planner(script, PlannerStatus.COMPLETED);
|
||||
appendExecutorThroughVerifiedInput(script);
|
||||
}
|
||||
|
||||
private void appendExecutorThroughVerifiedInput(
|
||||
ScriptedDiagnosisGraphActions script) {
|
||||
executor(script, ExecutorStatus.COMPLETED);
|
||||
gatekeeper(script, GatekeeperStatus.PASS, 1);
|
||||
verifiedInput(script);
|
||||
}
|
||||
|
||||
private void appendThroughCompletedVerifier(
|
||||
ScriptedDiagnosisGraphActions script) {
|
||||
appendThroughVerifiedInput(script);
|
||||
verifier(script, VerifierStatus.COMPLETED, Verdict.PASS,
|
||||
Verdict.PASS, Map.of());
|
||||
}
|
||||
|
||||
private void planner(
|
||||
ScriptedDiagnosisGraphActions script,
|
||||
PlannerStatus status) {
|
||||
planner(script, status.name());
|
||||
}
|
||||
|
||||
private void planner(
|
||||
ScriptedDiagnosisGraphActions script,
|
||||
String status) {
|
||||
script.step(
|
||||
DiagnosisGraphTopology.Node.PLANNER,
|
||||
status,
|
||||
reason(status),
|
||||
Map.of(DiagnosisGraphState.PLANNER_STATUS, status));
|
||||
}
|
||||
|
||||
private void executor(
|
||||
ScriptedDiagnosisGraphActions script,
|
||||
ExecutorStatus status) {
|
||||
executor(script, status.name());
|
||||
}
|
||||
|
||||
private void executor(
|
||||
ScriptedDiagnosisGraphActions script,
|
||||
String status) {
|
||||
script.step(
|
||||
DiagnosisGraphTopology.Node.EXECUTOR,
|
||||
status,
|
||||
reason(status),
|
||||
Map.of(DiagnosisGraphState.EXECUTOR_STATUS, status));
|
||||
}
|
||||
|
||||
private void gatekeeper(
|
||||
ScriptedDiagnosisGraphActions script,
|
||||
GatekeeperStatus status,
|
||||
int verifiedBindingCount) {
|
||||
gatekeeper(script, status.name(), verifiedBindingCount);
|
||||
}
|
||||
|
||||
private void gatekeeper(
|
||||
ScriptedDiagnosisGraphActions script,
|
||||
String status,
|
||||
int verifiedBindingCount) {
|
||||
script.step(
|
||||
DiagnosisGraphTopology.Node.GATEKEEPER,
|
||||
status,
|
||||
reason(status),
|
||||
Map.of(
|
||||
DiagnosisGraphState.GATEKEEPER_STATUS, status,
|
||||
DiagnosisGraphState.VERIFIED_BINDING_COUNT,
|
||||
verifiedBindingCount));
|
||||
}
|
||||
|
||||
private void verifiedInput(ScriptedDiagnosisGraphActions script) {
|
||||
script.step(
|
||||
DiagnosisGraphTopology.Node.VERIFIED_INPUT,
|
||||
"COMPLETED",
|
||||
"verified_input_completed",
|
||||
Map.of());
|
||||
}
|
||||
|
||||
private void verifier(
|
||||
ScriptedDiagnosisGraphActions script,
|
||||
VerifierStatus status,
|
||||
Verdict verdict,
|
||||
Verdict ceiling,
|
||||
Map<String, Object> verifierOutput) {
|
||||
Map<String, Object> update = new java.util.LinkedHashMap<>();
|
||||
update.put(DiagnosisGraphState.VERIFIER_STATUS, status.name());
|
||||
if (verdict != null) {
|
||||
update.put(DiagnosisGraphState.EFFECTIVE_VERDICT, verdict.name());
|
||||
}
|
||||
if (ceiling != null) {
|
||||
update.put(DiagnosisGraphState.VERIFIER_VERDICT_CEILING,
|
||||
ceiling.name());
|
||||
}
|
||||
update.put(DiagnosisGraphState.VERIFIER_OUTPUT, verifierOutput);
|
||||
script.step(
|
||||
DiagnosisGraphTopology.Node.VERIFIER,
|
||||
status.name(),
|
||||
reason(status.name()),
|
||||
update);
|
||||
}
|
||||
|
||||
private void verifierFailure(
|
||||
ScriptedDiagnosisGraphActions script,
|
||||
String status) {
|
||||
script.step(
|
||||
DiagnosisGraphTopology.Node.VERIFIER,
|
||||
status,
|
||||
reason(status),
|
||||
Map.of(DiagnosisGraphState.VERIFIER_STATUS, status));
|
||||
}
|
||||
|
||||
private void composerCompleted(ScriptedDiagnosisGraphActions script) {
|
||||
composer(script, "COMPLETED");
|
||||
}
|
||||
|
||||
private void composer(
|
||||
ScriptedDiagnosisGraphActions script,
|
||||
String status) {
|
||||
script.step(
|
||||
DiagnosisGraphTopology.Node.COMPOSER,
|
||||
status,
|
||||
"composer_" + reason(status),
|
||||
Map.of(DiagnosisGraphState.COMPOSER_STATUS, status));
|
||||
}
|
||||
|
||||
private void evidenceRetry(ScriptedDiagnosisGraphActions script) {
|
||||
script.step(
|
||||
DiagnosisGraphTopology.Node.EVIDENCE_RETRY,
|
||||
"COMPLETED",
|
||||
DiagnosisGraphTopology.Reason.EVIDENCE_RETRY,
|
||||
Map.of(DiagnosisGraphState.RETRY_CONTEXT,
|
||||
Map.of("mode", PlannerMode.EVIDENCE_GAP_ONLY.name())));
|
||||
}
|
||||
|
||||
private Map<String, Object> verifierOutputWithGap() {
|
||||
return Map.of("facts_checked", List.of(
|
||||
Map.of(
|
||||
"fact", "database saturation remains unverified",
|
||||
"verification", "no_evidence")));
|
||||
}
|
||||
|
||||
private void fallback(ScriptedDiagnosisGraphActions script) {
|
||||
script.step(
|
||||
DiagnosisGraphTopology.Node.FALLBACK,
|
||||
"COMPLETED",
|
||||
DiagnosisGraphTopology.Reason.FALLBACK_COMPLETED,
|
||||
Map.of(DiagnosisGraphState.FINAL_ANSWER, "safe fallback"));
|
||||
}
|
||||
|
||||
private void assertFallbackTrace(OverAllState state) {
|
||||
DiagnosisOrchestrationTrace trace =
|
||||
new DiagnosisOrchestrationTraceBuilder().build(state);
|
||||
assertEquals(DiagnosisGraphTopology.Node.FALLBACK, trace.finalNode());
|
||||
assertEquals(DiagnosisGraphTopology.Reason.FALLBACK_COMPLETED,
|
||||
trace.terminationReason());
|
||||
assertEquals(true, trace.degraded());
|
||||
}
|
||||
|
||||
private String reason(String status) {
|
||||
return status.toLowerCase(java.util.Locale.ROOT);
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package com.superbiz.agent.graph.diagnosis;
|
||||
|
||||
import com.alibaba.cloud.ai.graph.OverAllState;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DiagnosisOrchestrationTraceBuilderTest {
|
||||
|
||||
private final DiagnosisOrchestrationTraceBuilder builder =
|
||||
new DiagnosisOrchestrationTraceBuilder();
|
||||
|
||||
@Test
|
||||
void buildsExactTransitionsAndRoutingOnlyMap() {
|
||||
OverAllState state = stateWithEvents(List.of(
|
||||
event("planner", "COMPLETED", "planner_completed", 1),
|
||||
event("executor", "COMPLETED", "executor_completed", 1),
|
||||
event("gatekeeper", "PASS", "gatekeeper_pass", 1),
|
||||
event("verifier", "LOW_CONFID", "verifier_low_confid", 1),
|
||||
event("evidence_retry", "COMPLETED", "evidence_retry", 1),
|
||||
event("planner", "COMPLETED", "planner_completed", 2),
|
||||
event("composer", "COMPLETED", "composer_completed", 1)
|
||||
), 1);
|
||||
|
||||
DiagnosisOrchestrationTrace trace = builder.build(state);
|
||||
|
||||
assertEquals("stategraph-v1", trace.version());
|
||||
assertEquals("composer", trace.finalNode());
|
||||
assertEquals("composer_completed", trace.terminationReason());
|
||||
assertEquals(1, trace.evidenceRetryCount());
|
||||
assertFalse(trace.degraded());
|
||||
assertEquals(List.of(
|
||||
"planner->executor",
|
||||
"executor->gatekeeper",
|
||||
"gatekeeper->verifier",
|
||||
"verifier->evidence_retry",
|
||||
"evidence_retry->planner",
|
||||
"planner->composer"),
|
||||
trace.transitions().stream()
|
||||
.map(value -> value.from() + "->" + value.to())
|
||||
.toList());
|
||||
assertEquals("verifier_low_confid",
|
||||
trace.transitions().get(3).reasonCode());
|
||||
|
||||
Map<String, Object> traceMap = trace.toMap();
|
||||
assertEquals(Set.of(
|
||||
"version",
|
||||
"transitions",
|
||||
"final_node",
|
||||
"termination_reason",
|
||||
"degraded",
|
||||
"evidence_retry_count"),
|
||||
traceMap.keySet());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> transitionMap =
|
||||
((List<Map<String, Object>>) traceMap.get("transitions")).get(0);
|
||||
assertEquals(Set.of("from", "to", "reason_code", "attempt"),
|
||||
transitionMap.keySet());
|
||||
assertFalse(traceMap.toString().contains("prompt"));
|
||||
assertFalse(traceMap.toString().contains("tool_output"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallbackFinalEventMarksTraceDegraded() {
|
||||
OverAllState state = stateWithEvents(List.of(
|
||||
event("planner", "NON_RETRYABLE_FAILED",
|
||||
"planner_non_retryable_failed", 1),
|
||||
event("fallback", "COMPLETED", "fallback_completed", 1)
|
||||
), 0);
|
||||
|
||||
DiagnosisOrchestrationTrace trace = builder.build(state);
|
||||
|
||||
assertEquals("fallback", trace.finalNode());
|
||||
assertEquals("fallback_completed", trace.terminationReason());
|
||||
assertTrue(trace.degraded());
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyEventsFailExplicitly() {
|
||||
OverAllState state = new OverAllState(Map.of());
|
||||
|
||||
IllegalStateException error = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> builder.build(state));
|
||||
|
||||
assertEquals("orchestration events must not be empty",
|
||||
error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsupportedEventValueFailsExplicitly() {
|
||||
OverAllState state = new OverAllState(Map.of(
|
||||
DiagnosisGraphState.ORCHESTRATION_EVENTS,
|
||||
List.of(Map.of("node", "planner"))));
|
||||
|
||||
IllegalArgumentException error = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> builder.build(state));
|
||||
|
||||
assertTrue(error.getMessage().contains("unsupported value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventRejectsBlankRoutingMetadataAndInvalidAttempt() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> event("", "COMPLETED", "completed", 1));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> event("planner", " ", "completed", 1));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> event("planner", "COMPLETED", null, 1));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> event("planner", "COMPLETED", "completed", 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void traceAndTransitionCopiesAreImmutable() {
|
||||
DiagnosisOrchestrationTrace trace = builder.build(stateWithEvents(
|
||||
List.of(
|
||||
event("planner", "COMPLETED", "completed", 1),
|
||||
event("composer", "COMPLETED", "completed", 1)),
|
||||
0));
|
||||
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> trace.transitions().add(
|
||||
new OrchestrationTransition("x", "y", "z", 1)));
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> trace.toMap().put("prompt", "forbidden"));
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> trace.transitions().get(0).toMap()
|
||||
.put("tool_output", "forbidden"));
|
||||
}
|
||||
|
||||
private OverAllState stateWithEvents(
|
||||
List<OrchestrationEvent> events,
|
||||
int evidenceRetryCount) {
|
||||
return new OverAllState(Map.of(
|
||||
DiagnosisGraphState.ORCHESTRATION_EVENTS, events,
|
||||
DiagnosisGraphState.EVIDENCE_RETRY_COUNT, evidenceRetryCount));
|
||||
}
|
||||
|
||||
private OrchestrationEvent event(
|
||||
String node,
|
||||
String outcome,
|
||||
String reasonCode,
|
||||
int attempt) {
|
||||
return new OrchestrationEvent(node, outcome, reasonCode, attempt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.superbiz.agent.graph.diagnosis;
|
||||
|
||||
import com.alibaba.cloud.ai.graph.OverAllState;
|
||||
import com.alibaba.cloud.ai.graph.RunnableConfig;
|
||||
import com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
final class ScriptedDiagnosisGraphActions {
|
||||
|
||||
private final Map<String, ScriptedNode> nodes = new LinkedHashMap<>();
|
||||
private final List<String> sequence = new ArrayList<>();
|
||||
|
||||
ScriptedDiagnosisGraphActions() {
|
||||
nodes.put(DiagnosisGraphTopology.Node.PLANNER,
|
||||
new ScriptedNode(DiagnosisGraphTopology.Node.PLANNER));
|
||||
nodes.put(DiagnosisGraphTopology.Node.EXECUTOR,
|
||||
new ScriptedNode(DiagnosisGraphTopology.Node.EXECUTOR));
|
||||
nodes.put(DiagnosisGraphTopology.Node.GATEKEEPER,
|
||||
new ScriptedNode(DiagnosisGraphTopology.Node.GATEKEEPER));
|
||||
nodes.put(DiagnosisGraphTopology.Node.VERIFIED_INPUT,
|
||||
new ScriptedNode(DiagnosisGraphTopology.Node.VERIFIED_INPUT));
|
||||
nodes.put(DiagnosisGraphTopology.Node.VERIFIER,
|
||||
new ScriptedNode(DiagnosisGraphTopology.Node.VERIFIER));
|
||||
nodes.put(DiagnosisGraphTopology.Node.EVIDENCE_RETRY,
|
||||
new ScriptedNode(DiagnosisGraphTopology.Node.EVIDENCE_RETRY));
|
||||
nodes.put(DiagnosisGraphTopology.Node.COMPOSER,
|
||||
new ScriptedNode(DiagnosisGraphTopology.Node.COMPOSER));
|
||||
nodes.put(DiagnosisGraphTopology.Node.FALLBACK,
|
||||
new ScriptedNode(DiagnosisGraphTopology.Node.FALLBACK));
|
||||
}
|
||||
|
||||
ScriptedDiagnosisGraphActions step(
|
||||
String node,
|
||||
String outcome,
|
||||
String reasonCode,
|
||||
Map<String, Object> update) {
|
||||
node(node).steps.addLast(new Step(outcome, reasonCode, Map.copyOf(update)));
|
||||
return this;
|
||||
}
|
||||
|
||||
DiagnosisGraphActions actions() {
|
||||
return new DiagnosisGraphActions(
|
||||
node(DiagnosisGraphTopology.Node.PLANNER).action(),
|
||||
node(DiagnosisGraphTopology.Node.EXECUTOR).action(),
|
||||
node(DiagnosisGraphTopology.Node.GATEKEEPER).action(),
|
||||
node(DiagnosisGraphTopology.Node.VERIFIED_INPUT).action(),
|
||||
node(DiagnosisGraphTopology.Node.VERIFIER).action(),
|
||||
node(DiagnosisGraphTopology.Node.EVIDENCE_RETRY).action(),
|
||||
node(DiagnosisGraphTopology.Node.COMPOSER).action(),
|
||||
node(DiagnosisGraphTopology.Node.FALLBACK).action());
|
||||
}
|
||||
|
||||
List<String> sequence() {
|
||||
return List.copyOf(sequence);
|
||||
}
|
||||
|
||||
int calls(String node) {
|
||||
return node(node).calls;
|
||||
}
|
||||
|
||||
List<String> threadIds(String node) {
|
||||
return List.copyOf(node(node).threadIds);
|
||||
}
|
||||
|
||||
private ScriptedNode node(String name) {
|
||||
ScriptedNode node = nodes.get(name);
|
||||
if (node == null) {
|
||||
throw new IllegalArgumentException("unknown node: " + name);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private final class ScriptedNode {
|
||||
|
||||
private final String name;
|
||||
private final Deque<Step> steps = new ArrayDeque<>();
|
||||
private final List<String> threadIds = new ArrayList<>();
|
||||
private int calls;
|
||||
|
||||
private ScriptedNode(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
private AsyncNodeActionWithConfig action() {
|
||||
return this::apply;
|
||||
}
|
||||
|
||||
private CompletableFuture<Map<String, Object>> apply(
|
||||
OverAllState state,
|
||||
RunnableConfig config) {
|
||||
if (steps.isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"unexpected invocation of node " + name);
|
||||
}
|
||||
Step step = steps.removeFirst();
|
||||
calls++;
|
||||
sequence.add(name);
|
||||
threadIds.add(config.threadId().orElse(null));
|
||||
|
||||
Map<String, Object> update = new LinkedHashMap<>(step.update());
|
||||
update.put(DiagnosisGraphState.ORCHESTRATION_EVENTS,
|
||||
List.of(new OrchestrationEvent(
|
||||
name,
|
||||
step.outcome(),
|
||||
step.reasonCode(),
|
||||
calls)));
|
||||
return CompletableFuture.completedFuture(update);
|
||||
}
|
||||
}
|
||||
|
||||
private record Step(
|
||||
String outcome,
|
||||
String reasonCode,
|
||||
Map<String, Object> update) {
|
||||
|
||||
private Step {
|
||||
Objects.requireNonNull(outcome, "outcome");
|
||||
Objects.requireNonNull(reasonCode, "reasonCode");
|
||||
Objects.requireNonNull(update, "update");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user