mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): LLM pipeline step executor via model routing
This commit is contained in:
parent
85f5df394b
commit
15a8b2d73c
@ -0,0 +1,75 @@
|
||||
package vip.mate.wiki.pipeline;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.wiki.job.WikiModelRoutingService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Pipeline step executor that calls a chat model. The step config supplies a
|
||||
* {@code prompt} (system instruction) and an optional {@code model_id}; the
|
||||
* previous step's output is passed as the user message so steps compose. The
|
||||
* model is resolved through the existing wiki model routing.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WikiLlmStepExecutor implements WikiStepExecutor {
|
||||
|
||||
private final WikiModelRoutingService modelRoutingService;
|
||||
|
||||
public WikiLlmStepExecutor(WikiModelRoutingService modelRoutingService) {
|
||||
this.modelRoutingService = modelRoutingService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return "llm";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(WikiStepContext context) {
|
||||
String prompt = stringConfig(context, "prompt");
|
||||
if (prompt == null || prompt.isBlank()) {
|
||||
throw new IllegalArgumentException("llm step '" + context.stepId() + "' has no prompt");
|
||||
}
|
||||
Long modelId = longConfig(context, "model_id");
|
||||
ChatModel chatModel = modelRoutingService.buildChatModel(modelId);
|
||||
if (chatModel == null) {
|
||||
throw new IllegalStateException("No chat model available for pipeline step " + context.stepId());
|
||||
}
|
||||
String userContent = context.previousOutput() == null || context.previousOutput().isBlank()
|
||||
? "(no prior output)" : context.previousOutput();
|
||||
ChatResponse resp = chatModel.call(new Prompt(List.of(
|
||||
new SystemMessage(prompt), new UserMessage(userContent))));
|
||||
if (resp == null || resp.getResult() == null || resp.getResult().getOutput() == null
|
||||
|| resp.getResult().getOutput().getText() == null) {
|
||||
throw new IllegalStateException("Chat model returned no text for step " + context.stepId());
|
||||
}
|
||||
return resp.getResult().getOutput().getText();
|
||||
}
|
||||
|
||||
private String stringConfig(WikiStepContext context, String key) {
|
||||
Object v = context.stepConfig() == null ? null : context.stepConfig().get(key);
|
||||
return v == null ? null : String.valueOf(v);
|
||||
}
|
||||
|
||||
private Long longConfig(WikiStepContext context, String key) {
|
||||
Object v = context.stepConfig() == null ? null : context.stepConfig().get(key);
|
||||
if (v == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(v));
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package vip.mate.wiki.pipeline;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
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.wiki.job.WikiModelRoutingService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link WikiLlmStepExecutor}: it builds a prompt from the step
|
||||
* config and previous output, calls the routed model, and returns its text.
|
||||
* Model routing is mocked to return a fixed-response ChatModel.
|
||||
*/
|
||||
class WikiLlmStepExecutorTest {
|
||||
|
||||
private WikiLlmStepExecutor executor(String modelReply) {
|
||||
WikiModelRoutingService routing = mock(WikiModelRoutingService.class);
|
||||
ChatModel chat = prompt -> new ChatResponse(List.of(new Generation(new AssistantMessage(modelReply))));
|
||||
when(routing.buildChatModel(any())).thenReturn(chat);
|
||||
return new WikiLlmStepExecutor(routing);
|
||||
}
|
||||
|
||||
private WikiStepContext ctx(Map<String, Object> config, String previousOutput) {
|
||||
return new WikiStepContext(1L, 42L, "s1", config, previousOutput);
|
||||
}
|
||||
|
||||
@Test
|
||||
void callsModelAndReturnsText() throws Exception {
|
||||
WikiLlmStepExecutor e = executor("pattern summary");
|
||||
String out = e.execute(ctx(Map.of("prompt", "Summarize the episodes"), "ep1, ep2"));
|
||||
assertEquals("pattern summary", out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingPrompt_throws() {
|
||||
WikiLlmStepExecutor e = executor("x");
|
||||
assertThrows(IllegalArgumentException.class, () -> e.execute(ctx(Map.of(), "prior")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeIsLlm() {
|
||||
assertTrue("llm".equals(executor("x").type()));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user