refactor(harness): freeze single-agent contracts

This commit is contained in:
zhuyongxin
2026-07-21 17:33:25 +08:00
parent 30d3296043
commit 58c39107c5
47 changed files with 2607 additions and 24 deletions
@@ -0,0 +1,13 @@
package com.superbiz.agent.harness.contract;
public enum AnalysisKind {
NORMAL,
NEGATIVE_OBSERVATION;
public boolean accepts(EvidenceStatus evidenceStatus) {
return switch (this) {
case NORMAL -> evidenceStatus == EvidenceStatus.EVIDENCE_FOUND;
case NEGATIVE_OBSERVATION -> evidenceStatus == EvidenceStatus.NO_EVIDENCE;
};
}
}
@@ -0,0 +1,13 @@
package com.superbiz.agent.harness.contract;
import java.util.List;
final class ContractCollections {
private ContractCollections() {
}
static <T> List<T> immutable(List<T> values) {
return values == null ? List.of() : List.copyOf(values);
}
}
@@ -0,0 +1,67 @@
package com.superbiz.agent.harness.contract;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record DiagnosisDraft(
@JsonProperty("conclusion") Conclusion conclusion,
@JsonProperty("analysis") List<AnalysisItem> analysis,
@JsonProperty("action_plan") List<ActionPlanItem> actionPlan,
@JsonProperty("recommendations") List<Recommendation> recommendations,
@JsonProperty("limitations") Limitations limitations) {
public DiagnosisDraft {
analysis = ContractCollections.immutable(analysis);
actionPlan = ContractCollections.immutable(actionPlan);
recommendations = ContractCollections.immutable(recommendations);
}
public record Conclusion(
@JsonProperty("text") String text,
@JsonProperty("based_on_analysis_ids") List<String> basedOnAnalysisIds) {
public Conclusion {
basedOnAnalysisIds = ContractCollections.immutable(basedOnAnalysisIds);
}
}
public record AnalysisItem(
@JsonProperty("analysis_id") String analysisId,
@JsonProperty("kind") AnalysisKind kind,
@JsonProperty("text") String text,
@JsonProperty("tool_call_ids") List<String> toolCallIds) {
public AnalysisItem {
toolCallIds = ContractCollections.immutable(toolCallIds);
}
}
public record ActionPlanItem(
@JsonProperty("action") String action,
@JsonProperty("based_on_analysis_ids") List<String> basedOnAnalysisIds,
@JsonProperty("requires_human_confirmation") boolean requiresHumanConfirmation) {
public ActionPlanItem {
basedOnAnalysisIds = ContractCollections.immutable(basedOnAnalysisIds);
}
}
public record Recommendation(
@JsonProperty("text") String text,
@JsonProperty("based_on_analysis_ids") List<String> basedOnAnalysisIds) {
public Recommendation {
basedOnAnalysisIds = ContractCollections.immutable(basedOnAnalysisIds);
}
}
public record Limitations(
@JsonProperty("scope") String scope,
@JsonProperty("missing_info") List<String> missingInfo) {
public Limitations {
missingInfo = ContractCollections.immutable(missingInfo);
}
}
}
@@ -0,0 +1,7 @@
package com.superbiz.agent.harness.contract;
public enum EvidenceStatus {
EVIDENCE_FOUND,
NO_EVIDENCE,
ERROR
}
@@ -0,0 +1,7 @@
package com.superbiz.agent.harness.contract;
public enum FallbackType {
EVIDENCE_VALIDATION_FAILED,
SEMANTIC_UNSUPPORTED,
SEMANTIC_UNAVAILABLE
}
@@ -0,0 +1,7 @@
package com.superbiz.agent.harness.contract;
public enum IntentType {
SYSTEM_CHAT,
KNOWLEDGE_QUERY,
DIAGNOSIS
}
@@ -0,0 +1,7 @@
package com.superbiz.agent.harness.contract;
public enum InvocationStatus {
PROJECTING,
READY,
ERROR
}
@@ -0,0 +1,25 @@
package com.superbiz.agent.harness.contract;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record KnowledgeAnswerDraft(
@JsonProperty("answer_items") List<AnswerItem> answerItems,
@JsonProperty("limitations") List<String> limitations) {
public KnowledgeAnswerDraft {
answerItems = ContractCollections.immutable(answerItems);
limitations = ContractCollections.immutable(limitations);
}
public record AnswerItem(
@JsonProperty("text") String text,
@JsonProperty("tool_call_id") String toolCallId,
@JsonProperty("document_ids") List<String> documentIds) {
public AnswerItem {
documentIds = ContractCollections.immutable(documentIds);
}
}
}
@@ -0,0 +1,27 @@
package com.superbiz.agent.harness.contract;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record PreviousTurn(
@JsonProperty("user_query") String userQuery,
@JsonProperty("published_conclusion") String publishedConclusion,
@JsonProperty("scope") String scope,
@JsonProperty("limitations") List<String> limitations,
@JsonProperty("source_documents") List<SourceDocument> sourceDocuments) {
public PreviousTurn {
limitations = ContractCollections.immutable(limitations);
sourceDocuments = ContractCollections.immutable(sourceDocuments);
}
public static PreviousTurn from(PublishedResult result) {
return new PreviousTurn(
result.userQuery(),
result.publishedConclusion(),
result.scope(),
result.limitations(),
result.sourceDocuments());
}
}
@@ -0,0 +1,18 @@
package com.superbiz.agent.harness.contract;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record PublishedResult(
@JsonProperty("user_query") String userQuery,
@JsonProperty("published_conclusion") String publishedConclusion,
@JsonProperty("scope") String scope,
@JsonProperty("limitations") List<String> limitations,
@JsonProperty("source_documents") List<SourceDocument> sourceDocuments) {
public PublishedResult {
limitations = ContractCollections.immutable(limitations);
sourceDocuments = ContractCollections.immutable(sourceDocuments);
}
}
@@ -0,0 +1,8 @@
package com.superbiz.agent.harness.contract;
public enum ReleaseOutcome {
SUCCESS,
FALLBACK,
FAILED,
CANCELLED
}
@@ -0,0 +1,26 @@
package com.superbiz.agent.harness.contract;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record SafeFallback(
@JsonProperty("type") FallbackType type,
@JsonProperty("conclusion") String conclusion,
@JsonProperty("message") String message,
@JsonProperty("verified_sources") List<VerifiedSource> verifiedSources,
@JsonProperty("limitations") List<String> limitations,
@JsonProperty("next_steps") List<String> nextSteps) {
public SafeFallback {
verifiedSources = ContractCollections.immutable(verifiedSources);
limitations = ContractCollections.immutable(limitations);
nextSteps = ContractCollections.immutable(nextSteps);
}
public record VerifiedSource(
@JsonProperty("source_type") String sourceType,
@JsonProperty("source") String source,
@JsonProperty("scope") String scope) {
}
}
@@ -0,0 +1,6 @@
package com.superbiz.agent.harness.contract;
public enum SemanticVerdict {
SUPPORTED,
UNSUPPORTED
}
@@ -0,0 +1,8 @@
package com.superbiz.agent.harness.contract;
import com.fasterxml.jackson.annotation.JsonProperty;
public record SourceDocument(
@JsonProperty("document_id") String documentId,
@JsonProperty("title") String title) {
}
@@ -0,0 +1,7 @@
package com.superbiz.agent.harness.contract;
public enum SseOutcome {
SUCCESS,
FALLBACK,
FAILED
}
+5 -5
View File
@@ -22,7 +22,7 @@ milvus:
password: ""
database: db_4a578da0f27ce9d
timeout: 10000
token: d246a77f43a109685596e3c68ecfd359e1cd8b29d35c41d708160f02b97ae623d2ab392738df740d0c77b58ca1bbadfa7c412140
token: ${MILVUS_TOKEN}
secure: true
vector-dim: 1024 # BGE-M3 = 1024,换模型时同步改
@@ -45,7 +45,7 @@ spring:
datasource:
url: jdbc:mysql://119.29.78.52:33306/superbiz_agent?useUnicode=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: '!Fucker123..'
password: ${SUPERBIZ_MYSQL_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 5
@@ -83,7 +83,7 @@ spring:
redis:
host: 119.29.78.52
port: 33308
password: '!Fucker123..'
password: ${SUPERBIZ_REDIS_PASSWORD}
database: 0
timeout: 3000
lettuce:
@@ -119,7 +119,7 @@ spring:
# --- Chat: DeepSeek (原生) ---
deepseek:
api-key: sk-1f44696abe644bd684f09cc43f12c557
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com
chat:
options:
@@ -136,7 +136,7 @@ spring:
# --- Embedding: SiliconFlow BGE-M3 ---
siliconflow:
api-key: sk-rlxqcnlohjqwkzoffollthmzzfiohngdrabrmmqhcgtewnzx
api-key: ${SILICONFLOW_API_KEY}
base-url: https://api.siliconflow.cn
embedding:
model: BAAI/bge-m3
@@ -0,0 +1,96 @@
package com.superbiz.agent.harness.contract;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HarnessContractTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void diagnosisDraftUsesStableBoundedShape() throws Exception {
DiagnosisDraft draft = new DiagnosisDraft(
new DiagnosisDraft.Conclusion("Pool exhausted", List.of("a1")),
List.of(new DiagnosisDraft.AnalysisItem(
"a1",
AnalysisKind.NORMAL,
"active=50 max=50",
List.of("call-1"))),
List.of(new DiagnosisDraft.ActionPlanItem(
"Inspect long transactions",
List.of("a1"),
false)),
List.of(new DiagnosisDraft.Recommendation(
"Add pool wait alerts",
List.of("a1"))),
new DiagnosisDraft.Limitations("order-service, last 30 minutes", List.of("No slow SQL data")));
JsonNode json = objectMapper.valueToTree(draft);
assertEquals("call-1", json.path("analysis").get(0).path("tool_call_ids").get(0).asText());
assertEquals("NORMAL", json.path("analysis").get(0).path("kind").asText());
assertTrue(json.has("action_plan"));
assertFalse(json.toString().contains("raw_response"));
assertFalse(json.toString().contains("thought"));
}
@Test
void evidenceStatusRemainsSeparateFromInvocationStatus() {
assertTrue(AnalysisKind.NORMAL.accepts(EvidenceStatus.EVIDENCE_FOUND));
assertFalse(AnalysisKind.NORMAL.accepts(EvidenceStatus.NO_EVIDENCE));
assertTrue(AnalysisKind.NEGATIVE_OBSERVATION.accepts(EvidenceStatus.NO_EVIDENCE));
assertFalse(AnalysisKind.NEGATIVE_OBSERVATION.accepts(EvidenceStatus.ERROR));
assertEquals(InvocationStatus.READY, InvocationStatus.valueOf("READY"));
}
@Test
void knowledgeAnswerBindsExactToolAndDocuments() throws Exception {
KnowledgeAnswerDraft draft = new KnowledgeAnswerDraft(
List.of(new KnowledgeAnswerDraft.AnswerItem(
"Check the timeout code first.",
"call-rag-1",
List.of("payment-timeout-guide"))),
List.of("Runtime state was not queried"));
JsonNode json = objectMapper.valueToTree(draft);
assertEquals("call-rag-1", json.path("answer_items").get(0).path("tool_call_id").asText());
assertEquals("payment-timeout-guide",
json.path("answer_items").get(0).path("document_ids").get(0).asText());
}
@Test
void fallbackAndPreviousTurnExcludeInternalEvidence() throws Exception {
SafeFallback fallback = new SafeFallback(
FallbackType.SEMANTIC_UNSUPPORTED,
null,
"Current evidence is insufficient",
List.of(new SafeFallback.VerifiedSource("LOG", "APPLICATION", "last 30 minutes")),
List.of("Semantic validation did not pass"),
List.of("Collect the missing data and retry"));
PublishedResult result = new PublishedResult(
"Check payment timeout",
"Connection pool exhaustion",
"order-service, last 30 minutes",
List.of("No slow SQL data"),
List.of(new SourceDocument("payment-timeout-guide", "Payment timeout guide")));
JsonNode fallbackJson = objectMapper.valueToTree(fallback);
JsonNode previousJson = objectMapper.valueToTree(PreviousTurn.from(result));
assertNull(fallback.conclusion());
assertEquals("SEMANTIC_UNSUPPORTED", fallbackJson.path("type").asText());
assertEquals("payment-timeout-guide",
previousJson.path("source_documents").get(0).path("document_id").asText());
assertFalse(previousJson.toString().contains("tool_call_id"));
assertFalse(previousJson.toString().contains("raw_response"));
}
}
@@ -4,6 +4,7 @@ import io.milvus.client.MilvusServiceClient;
import io.milvus.param.ConnectParam;
import io.milvus.param.R;
import io.milvus.param.collection.HasCollectionParam;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
/**
@@ -13,9 +14,12 @@ public class SimpleMilvusTest {
@Test
public void testConnection() {
String host = "in03-4a578da0f27ce9d.serverless.aws-eu-central-1.cloud.zilliz.com";
int port = 443;
String token = "d246a77f43a109685596e3c68ecfd359e1cd8b29d35c41d708160f02b97ae623d2ab392738df740d0c77b58ca1bbadfa7c412140";
String host = System.getenv().getOrDefault(
"MILVUS_HOST",
"in03-4a578da0f27ce9d.serverless.aws-eu-central-1.cloud.zilliz.com");
int port = Integer.parseInt(System.getenv().getOrDefault("MILVUS_PORT", "443"));
String token = System.getenv("MILVUS_TOKEN");
Assumptions.assumeTrue(token != null && !token.isBlank(), "MILVUS_TOKEN is required");
System.out.println("尝试连接 Milvus...");
System.out.println("Host: " + host);
@@ -4,6 +4,7 @@ import com.superbiz.agent.domain.model.SessionContext;
import com.superbiz.agent.domain.model.ToolCall;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
@@ -20,10 +21,11 @@ import static org.junit.jupiter.api.Assertions.*;
* RedisSessionManager 单元测试
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@EnabledIfEnvironmentVariable(named = "SUPERBIZ_REDIS_PASSWORD", matches = ".+")
@TestPropertySource(properties = {
"spring.redis.host=119.29.78.52",
"spring.redis.port=6379",
"spring.redis.password=!Fucker123.."
"spring.data.redis.host=${SUPERBIZ_REDIS_HOST:119.29.78.52}",
"spring.data.redis.port=${SUPERBIZ_REDIS_PORT:33308}",
"spring.data.redis.password=${SUPERBIZ_REDIS_PASSWORD}"
})
class RedisSessionManagerTest {