From 6bbd3ac4dac0c04f00f6a95bde6689766f1c51e5 Mon Sep 17 00:00:00 2001 From: taobig Date: Thu, 24 Sep 2026 03:14:24 -0400 Subject: [PATCH] fix(agent): parse employee drafts after reasoning blocks (#648) --- .../agent/service/AgentGenerationService.java | 8 ++ .../service/AgentGenerationServiceTest.java | 86 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 mateclaw-server/src/test/java/vip/mate/agent/service/AgentGenerationServiceTest.java diff --git a/mateclaw-server/src/main/java/vip/mate/agent/service/AgentGenerationService.java b/mateclaw-server/src/main/java/vip/mate/agent/service/AgentGenerationService.java index 8534cfca..c30e830d 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/service/AgentGenerationService.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/service/AgentGenerationService.java @@ -295,6 +295,14 @@ public class AgentGenerationService { private JsonNode parseJson(String response) { if (response == null || response.isBlank()) return null; String cleaned = response.trim(); + // Some reasoning models prepend thinking to content on non-streaming + // calls. Strip only leading blocks: tags inside JSON string values are + // legitimate persona text, and JSON examples in thinking are not drafts. + while (cleaned.startsWith("")) { + int end = cleaned.indexOf("", "".length()); + if (end < 0) return null; // Incomplete reasoning is not a final answer. + cleaned = cleaned.substring(end + "".length()).trim(); + } if (cleaned.startsWith("```json")) cleaned = cleaned.substring(7); else if (cleaned.startsWith("```")) cleaned = cleaned.substring(3); if (cleaned.endsWith("```")) cleaned = cleaned.substring(0, cleaned.length() - 3); diff --git a/mateclaw-server/src/test/java/vip/mate/agent/service/AgentGenerationServiceTest.java b/mateclaw-server/src/test/java/vip/mate/agent/service/AgentGenerationServiceTest.java new file mode 100644 index 00000000..8db913ef --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/agent/service/AgentGenerationServiceTest.java @@ -0,0 +1,86 @@ +package vip.mate.agent.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; +import org.springframework.ai.chat.prompt.Prompt; +import vip.mate.agent.AgentGraphBuilder; +import vip.mate.exception.MateClawException; +import vip.mate.llm.model.ModelConfigEntity; +import vip.mate.llm.service.ModelConfigService; +import vip.mate.skill.service.SkillService; +import vip.mate.tool.service.AvailableToolService; +import vip.mate.wiki.service.WikiKnowledgeBaseService; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class AgentGenerationServiceTest { + private ChatModel model; + private AgentGenerationService service; + + @BeforeEach + void setUp() { + var configs = mock(ModelConfigService.class); + var builder = mock(AgentGraphBuilder.class); + var tools = mock(AvailableToolService.class); + var skills = mock(SkillService.class); + var kbs = mock(WikiKnowledgeBaseService.class); + var config = new ModelConfigEntity(); + model = mock(ChatModel.class); + when(configs.getDefaultModel()).thenReturn(config); + when(builder.buildRuntimeChatModel(config)).thenReturn(model); + when(tools.listAvailable()).thenReturn(List.of()); + when(skills.listEnabledSkills(1L)).thenReturn(List.of()); + when(kbs.listByWorkspace(1L)).thenReturn(List.of()); + service = new AgentGenerationService(configs, builder, new ObjectMapper(), tools, skills, kbs); + } + + private void response(String text) { + when(model.call(any(Prompt.class))).thenReturn(new ChatResponse( + List.of(new Generation(new AssistantMessage(text))))); + } + + @ParameterizedTest + @ValueSource(strings = { + "{\"name\":\"周报助手\"}", + "```json\n{\"name\":\"周报助手\"}\n```", + "先考虑配置\n{\"name\":\"周报助手\"}", + "{\"name\":\"思考中的示例\"}\n```json\n{\"name\":\"周报助手\"}\n```", + " \n第一步\n第二步\n{\"name\":\"周报助手\"}" + }) + void acceptsFinalJsonAfterOptionalThinking(String text) { + response(text); + assertEquals("周报助手", service.generateDraft("跨部门周报汇总", 1L).getName()); + } + + @ParameterizedTest + @ValueSource(strings = { + "{\"name\":\"助手\",\"systemPrompt\":\"保留 示例 原文\"}", + "规划\n{\"name\":\"助手\",\"systemPrompt\":\"保留 示例 原文\"}" + }) + void preservesLiteralTagsInsideJsonStrings(String text) { + response(text); + assertEquals("保留 示例 原文", service.generateDraft("助手", 1L).getSystemPrompt()); + } + + @ParameterizedTest + @ValueSource(strings = { + "", "{\"name\":\"未完成思考\"}", + "{\"name\":\"只有思考\"}", + "规划\n{\"name\":", "[]", + "说明文字 {\"name\":\"不应猜测提取\"}" + }) + void rejectsMissingOrMalformedFinalObject(String text) { + response(text); + assertThrows(MateClawException.class, () -> service.generateDraft("助手", 1L)); + } +}