mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +08:00
177 lines
7.6 KiB
Java
177 lines
7.6 KiB
Java
package vip.mate.agent;
|
|
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.ai.chat.messages.UserMessage;
|
|
import vip.mate.llm.service.ModelCapabilityService;
|
|
import vip.mate.workspace.conversation.ConversationService;
|
|
import vip.mate.workspace.conversation.model.MessageContentPart;
|
|
import vip.mate.workspace.conversation.model.MessageEntity;
|
|
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.EnumSet;
|
|
import java.util.List;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
/**
|
|
* Issue #303 follow-up: a vision-capable model replays history as text only, so a
|
|
* follow-up question about an earlier image was answered blind. The current turn
|
|
* must re-attach the most recent image so the model actually re-sees it.
|
|
*/
|
|
class BaseAgentCarryRecentImageTest {
|
|
|
|
@Test
|
|
@DisplayName("Vision model + follow-up with no image → most recent image is carried into the turn")
|
|
void followUp_carriesRecentImage() throws Exception {
|
|
Path img = Files.createTempFile("carry-test", ".jpg");
|
|
Files.write(img, new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x00});
|
|
try {
|
|
TestAgent agent = visionAgent();
|
|
|
|
MessageEntity imgTurn = userMsg("看看这张图");
|
|
MessageContentPart imagePart = imagePart(img.toAbsolutePath().toString());
|
|
MessageEntity asst = assistantMsg("图里是手写的字");
|
|
MessageEntity followUp = userMsg("左上角有没有小字");
|
|
|
|
List<MessageEntity> history = List.of(imgTurn, asst, followUp);
|
|
when(agent.conversationService.listMessages("c1")).thenReturn(history);
|
|
when(agent.conversationService.renderMessageContent(followUp)).thenReturn("左上角有没有小字");
|
|
when(agent.conversationService.parseMessageParts(imgTurn)).thenReturn(List.of(imagePart));
|
|
when(agent.conversationService.parseMessageParts(followUp)).thenReturn(List.of());
|
|
when(agent.conversationService.parseMessageParts(asst)).thenReturn(List.of());
|
|
|
|
UserMessage result = agent.callBuildCurrent("c1", "左上角有没有小字");
|
|
|
|
assertTrue(result.getMedia() != null && result.getMedia().size() == 1,
|
|
"the recent image must be re-attached to the follow-up turn");
|
|
assertTrue(result.getText().contains("较早发送的"),
|
|
"a note must explain the carried image to the model");
|
|
} finally {
|
|
Files.deleteIfExists(img);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Text-only model → no image carried (relies on persisted caption instead)")
|
|
void textOnlyModel_doesNotCarry() throws Exception {
|
|
Path img = Files.createTempFile("carry-test", ".jpg");
|
|
Files.write(img, new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x00});
|
|
try {
|
|
TestAgent agent = newAgent(EnumSet.of(ModelCapabilityService.Modality.TEXT));
|
|
MessageEntity imgTurn = userMsg("看看这张图");
|
|
MessageEntity followUp = userMsg("左上角有没有小字");
|
|
List<MessageEntity> history = List.of(imgTurn, followUp);
|
|
when(agent.conversationService.listMessages("c1")).thenReturn(history);
|
|
when(agent.conversationService.renderMessageContent(followUp)).thenReturn("左上角有没有小字");
|
|
when(agent.conversationService.parseMessageParts(any())).thenReturn(List.of());
|
|
|
|
UserMessage result = agent.callBuildCurrent("c1", "左上角有没有小字");
|
|
|
|
assertTrue(result.getMedia() == null || result.getMedia().isEmpty(),
|
|
"text-only model must not get raw image bytes carried over");
|
|
} finally {
|
|
Files.deleteIfExists(img);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Current turn already has an image → nothing extra carried")
|
|
void currentTurnHasImage_noCarry() throws Exception {
|
|
Path older = Files.createTempFile("carry-old", ".jpg");
|
|
Path now = Files.createTempFile("carry-now", ".jpg");
|
|
Files.write(older, new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x00});
|
|
Files.write(now, new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x00});
|
|
try {
|
|
TestAgent agent = visionAgent();
|
|
MessageEntity oldTurn = userMsg("第一张");
|
|
MessageEntity curTurn = userMsg("第二张");
|
|
List<MessageEntity> history = List.of(oldTurn, curTurn);
|
|
when(agent.conversationService.listMessages("c1")).thenReturn(history);
|
|
when(agent.conversationService.renderMessageContent(curTurn)).thenReturn("第二张");
|
|
when(agent.conversationService.parseMessageParts(oldTurn))
|
|
.thenReturn(List.of(imagePart(older.toAbsolutePath().toString())));
|
|
when(agent.conversationService.parseMessageParts(curTurn))
|
|
.thenReturn(List.of(imagePart(now.toAbsolutePath().toString())));
|
|
|
|
UserMessage result = agent.callBuildCurrent("c1", "第二张");
|
|
|
|
assertTrue(result.getMedia() != null && result.getMedia().size() == 1,
|
|
"only the current turn's own image should be present — no extra carry");
|
|
assertFalse(result.getText().contains("较早发送的"),
|
|
"no carry note when the current turn already has an image");
|
|
} finally {
|
|
Files.deleteIfExists(older);
|
|
Files.deleteIfExists(now);
|
|
}
|
|
}
|
|
|
|
// ---------- scaffold ----------
|
|
|
|
private static MessageContentPart imagePart(String path) {
|
|
MessageContentPart p = new MessageContentPart();
|
|
p.setType("image");
|
|
p.setContentType("image/jpeg");
|
|
p.setFileName("image.jpg");
|
|
p.setPath(path);
|
|
return p;
|
|
}
|
|
|
|
private static MessageEntity userMsg(String content) {
|
|
MessageEntity m = new MessageEntity();
|
|
m.setRole("user");
|
|
m.setContent(content);
|
|
return m;
|
|
}
|
|
|
|
private static MessageEntity assistantMsg(String content) {
|
|
MessageEntity m = new MessageEntity();
|
|
m.setRole("assistant");
|
|
m.setContent(content);
|
|
return m;
|
|
}
|
|
|
|
private static TestAgent visionAgent() {
|
|
return newAgent(EnumSet.of(ModelCapabilityService.Modality.VISION, ModelCapabilityService.Modality.TEXT));
|
|
}
|
|
|
|
private static TestAgent newAgent(EnumSet<ModelCapabilityService.Modality> caps) {
|
|
ConversationService conv = mock(ConversationService.class);
|
|
TestAgent agent = new TestAgent(conv);
|
|
agent.modelCapabilities = caps;
|
|
agent.modelName = "test-model";
|
|
agent.agentName = "test-agent";
|
|
return agent;
|
|
}
|
|
|
|
static class TestAgent extends BaseAgent {
|
|
TestAgent(ConversationService conv) {
|
|
super(null, conv);
|
|
}
|
|
|
|
UserMessage callBuildCurrent(String conversationId, String text) {
|
|
return buildCurrentUserMessageWithRouting(conversationId, text).userMessage();
|
|
}
|
|
|
|
@Override
|
|
public String chat(String userMessage, String conversationId) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public reactor.core.publisher.Flux<String> chatStream(String userMessage, String conversationId) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public String execute(String goal, String conversationId) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
}
|
|
}
|