mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +08:00
feat(wiki): page-input transformations — run a template against an existing page
This commit is contained in:
parent
4c8a42b0e2
commit
e1759e0e3c
@ -102,10 +102,10 @@ public class WikiTransformationController {
|
||||
// ==================== Apply ====================
|
||||
|
||||
@RequireWorkspaceRole("member")
|
||||
@Operation(summary = "Run a transformation against a raw material",
|
||||
description = "Set sync=true to block until the LLM call returns "
|
||||
+ "(the response carries the populated run). "
|
||||
+ "When false (default) the call returns immediately with the pending run row.")
|
||||
@Operation(summary = "Run a transformation against a raw material or wiki page",
|
||||
description = "Body accepts exactly one of {rawId, pageId}. Set sync=true to block "
|
||||
+ "until the LLM call returns; when false (default) the call returns "
|
||||
+ "immediately with the pending run row.")
|
||||
@PostMapping("/{id}/apply")
|
||||
public R<WikiTransformationRunEntity> apply(@PathVariable Long id,
|
||||
@RequestBody Map<String, Object> body,
|
||||
@ -116,16 +116,23 @@ public class WikiTransformationController {
|
||||
verifyTemplateWorkspace(t, workspaceId);
|
||||
|
||||
Object rawIdRaw = body == null ? null : body.get("rawId");
|
||||
if (rawIdRaw == null) {
|
||||
return R.fail("rawId is required");
|
||||
Object pageIdRaw = body == null ? null : body.get("pageId");
|
||||
if (rawIdRaw == null && pageIdRaw == null) {
|
||||
return R.fail("One of rawId / pageId is required");
|
||||
}
|
||||
if (rawIdRaw != null && pageIdRaw != null) {
|
||||
return R.fail("Pass only one of rawId / pageId, not both");
|
||||
}
|
||||
Long rawId = Long.valueOf(rawIdRaw.toString());
|
||||
|
||||
if (sync) {
|
||||
return R.ok(executor.runOnRawSync(t, rawId, "manual"));
|
||||
if (rawIdRaw != null) {
|
||||
Long rawId = Long.valueOf(rawIdRaw.toString());
|
||||
if (sync) return R.ok(executor.runOnRawSync(t, rawId, "manual"));
|
||||
executor.runOnRawAsync(t, rawId, "manual");
|
||||
} else {
|
||||
Long pageId = Long.valueOf(pageIdRaw.toString());
|
||||
if (sync) return R.ok(executor.runOnPageSync(t, pageId, "manual"));
|
||||
executor.runOnPageAsync(t, pageId, "manual");
|
||||
}
|
||||
executor.runOnRawAsync(t, rawId, "manual");
|
||||
// Async path — caller polls /runs to see the result land.
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
|
||||
@ -93,6 +93,141 @@ public class WikiTransformationExecutor {
|
||||
() -> runOnRawSync(transformation, rawId, triggeredBy), WORKER);
|
||||
}
|
||||
|
||||
public CompletableFuture<WikiTransformationRunEntity> runOnPageAsync(
|
||||
WikiTransformationEntity transformation, Long pageId, String triggeredBy) {
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> runOnPageSync(transformation, pageId, triggeredBy), WORKER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the transformation against an existing wiki page (e.g. a previous
|
||||
* synthesis page or a manually-authored page). Mirrors
|
||||
* {@link #runOnRawSync} but uses page content as the source. Output is
|
||||
* not auto-saved as a wiki page even when the template has
|
||||
* {@code outputTarget=page} — overwriting the input page would be
|
||||
* surprising; users can still save manually from the run history.
|
||||
*/
|
||||
public WikiTransformationRunEntity runOnPageSync(
|
||||
WikiTransformationEntity transformation, Long pageId, String triggeredBy) {
|
||||
if (transformation == null) {
|
||||
throw new IllegalArgumentException("transformation is required");
|
||||
}
|
||||
if (pageId == null) {
|
||||
throw new IllegalArgumentException("pageId is required");
|
||||
}
|
||||
if (pageService == null) {
|
||||
throw new IllegalStateException("Page service unavailable");
|
||||
}
|
||||
WikiPageEntity page = pageService.getById(pageId);
|
||||
if (page == null) {
|
||||
throw new IllegalArgumentException("Page not found: " + pageId);
|
||||
}
|
||||
if (Boolean.FALSE.equals(transformation.getEnabled())) {
|
||||
log.debug("[WikiTransformation] skipping disabled template id={} name={}",
|
||||
transformation.getId(), transformation.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
long startNanos = System.nanoTime();
|
||||
WikiTransformationRunEntity run = new WikiTransformationRunEntity();
|
||||
run.setTransformationId(transformation.getId());
|
||||
run.setKbId(page.getKbId());
|
||||
run.setWorkspaceId(transformation.getWorkspaceId());
|
||||
run.setInputKind("page");
|
||||
run.setPageId(pageId);
|
||||
run.setStatus("running");
|
||||
run.setTriggeredBy(triggeredBy == null ? "manual" : triggeredBy);
|
||||
run.setStartedAt(LocalDateTime.now());
|
||||
transformationService.insertRun(run);
|
||||
|
||||
try {
|
||||
String inputText = page.getContent();
|
||||
if (inputText == null || inputText.isBlank()) {
|
||||
throw new IllegalStateException("Page has no content");
|
||||
}
|
||||
String output = renderAndCallLlm(transformation, page.getKbId(),
|
||||
page.getTitle() == null ? ("page#" + pageId) : page.getTitle(),
|
||||
inputText, run);
|
||||
|
||||
WikiTransformationRunEntity current = transformationService.getRun(run.getId());
|
||||
if (current != null && "cancelled".equalsIgnoreCase(current.getStatus())) {
|
||||
log.info("[WikiTransformation] run={} was cancelled mid-flight; discarding {} chars of LLM output",
|
||||
run.getId(), output.length());
|
||||
return current;
|
||||
}
|
||||
|
||||
run.setOutput(output);
|
||||
run.setStatus("completed");
|
||||
run.setCompletedAt(LocalDateTime.now());
|
||||
run.setDurationMs(Duration.ofNanos(System.nanoTime() - startNanos).toMillis());
|
||||
// For page input we never auto-save back to a page — the call site
|
||||
// can use the manual save-as-page endpoint with a derived slug.
|
||||
transformationService.updateRun(run);
|
||||
|
||||
metrics.recordCompileStage("transformation_run", page.getKbId(),
|
||||
Duration.ofNanos(System.nanoTime() - startNanos));
|
||||
log.info("[WikiTransformation] ok run={} transformation={} pageId={} kbId={} ({} ms)",
|
||||
run.getId(), transformation.getName(), pageId, page.getKbId(),
|
||||
run.getDurationMs());
|
||||
} catch (Exception e) {
|
||||
WikiTransformationRunEntity current = transformationService.getRun(run.getId());
|
||||
if (current != null && "cancelled".equalsIgnoreCase(current.getStatus())) {
|
||||
log.info("[WikiTransformation] run={} was cancelled before failure could be recorded ({})",
|
||||
run.getId(), e.getMessage());
|
||||
return current;
|
||||
}
|
||||
run.setStatus("failed");
|
||||
String msg = e.getMessage();
|
||||
run.setError(msg == null ? e.getClass().getSimpleName() : msg);
|
||||
run.setCompletedAt(LocalDateTime.now());
|
||||
run.setDurationMs(Duration.ofNanos(System.nanoTime() - startNanos).toMillis());
|
||||
transformationService.updateRun(run);
|
||||
log.warn("[WikiTransformation] failed run={} transformation={} pageId={}: {}",
|
||||
run.getId(), transformation.getName(), pageId, msg);
|
||||
}
|
||||
return run;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared prompt-render + LLM-call + output-cleanup stage used by both
|
||||
* raw-input and page-input entry points. Sets {@code run.modelId} as a
|
||||
* side-effect so the run row reflects which model produced the output.
|
||||
*/
|
||||
private String renderAndCallLlm(WikiTransformationEntity transformation, Long kbId,
|
||||
String sourceTitle, String sourceText,
|
||||
WikiTransformationRunEntity run) {
|
||||
String trimmedInput = sourceText.length() > MAX_INPUT_CHARS
|
||||
? sourceText.substring(0, MAX_INPUT_CHARS) + "\n…(truncated)"
|
||||
: sourceText;
|
||||
|
||||
String systemPrompt = PromptLoader.loadPrompt("wiki/transformation-system");
|
||||
String instruction = (transformation.getPromptTemplate() == null ? "" : transformation.getPromptTemplate())
|
||||
.replace("{input_text}", trimmedInput)
|
||||
.replace("{title}", sourceTitle);
|
||||
String userPrompt = PromptLoader.loadPrompt("wiki/transformation-user")
|
||||
.replace("{instruction}", instruction)
|
||||
.replace("{source_title}", sourceTitle)
|
||||
.replace("{source_text}", trimmedInput);
|
||||
|
||||
Long resolvedModelId = resolveModelId(transformation, kbId);
|
||||
ChatModel chatModel = buildChatModel(resolvedModelId);
|
||||
run.setModelId(resolvedModelId);
|
||||
|
||||
ChatResponse resp = chatModel.call(new Prompt(List.of(
|
||||
new SystemMessage(systemPrompt), new UserMessage(userPrompt))));
|
||||
String rawOutput = (resp == null || resp.getResult() == null
|
||||
|| resp.getResult().getOutput() == null)
|
||||
? null : resp.getResult().getOutput().getText();
|
||||
if (rawOutput == null || rawOutput.isBlank()) {
|
||||
throw new IllegalStateException("LLM returned empty output");
|
||||
}
|
||||
String output = cleanLlmOutput(rawOutput);
|
||||
if (output.isBlank()) {
|
||||
throw new IllegalStateException("LLM output was empty after cleanup");
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the transformation against the given raw material and persist
|
||||
* the outcome. The returned entity is the persisted run row, regardless
|
||||
@ -134,32 +269,8 @@ public class WikiTransformationExecutor {
|
||||
if (inputText == null || inputText.isBlank()) {
|
||||
throw new IllegalStateException("Raw material has no extractable text yet");
|
||||
}
|
||||
String trimmedInput = inputText.length() > MAX_INPUT_CHARS
|
||||
? inputText.substring(0, MAX_INPUT_CHARS) + "\n…(truncated)"
|
||||
: inputText;
|
||||
|
||||
String systemPrompt = PromptLoader.loadPrompt("wiki/transformation-system");
|
||||
String userPrompt = PromptLoader.loadPrompt("wiki/transformation-user")
|
||||
.replace("{instruction}", renderTemplate(transformation.getPromptTemplate(), raw, trimmedInput))
|
||||
.replace("{source_title}", safeTitle(raw))
|
||||
.replace("{source_text}", trimmedInput);
|
||||
|
||||
Long resolvedModelId = resolveModelId(transformation, raw.getKbId());
|
||||
ChatModel chatModel = buildChatModel(resolvedModelId);
|
||||
run.setModelId(resolvedModelId);
|
||||
|
||||
ChatResponse resp = chatModel.call(new Prompt(List.of(
|
||||
new SystemMessage(systemPrompt), new UserMessage(userPrompt))));
|
||||
String rawOutput = (resp == null || resp.getResult() == null
|
||||
|| resp.getResult().getOutput() == null)
|
||||
? null : resp.getResult().getOutput().getText();
|
||||
if (rawOutput == null || rawOutput.isBlank()) {
|
||||
throw new IllegalStateException("LLM returned empty output");
|
||||
}
|
||||
String output = cleanLlmOutput(rawOutput);
|
||||
if (output.isBlank()) {
|
||||
throw new IllegalStateException("LLM output was empty after cleanup");
|
||||
}
|
||||
String output = renderAndCallLlm(transformation, raw.getKbId(),
|
||||
safeTitle(raw), inputText, run);
|
||||
// Honour a mid-flight cancel: the cancel endpoint flipped the run
|
||||
// row to 'cancelled' while the LLM was still working. Drop the
|
||||
// output and stop here rather than overwrite the cancelled state.
|
||||
@ -234,14 +345,6 @@ public class WikiTransformationExecutor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private String renderTemplate(String template, WikiRawMaterialEntity raw, String inputText) {
|
||||
if (template == null) return "";
|
||||
String result = template;
|
||||
result = result.replace("{input_text}", inputText);
|
||||
result = result.replace("{title}", safeTitle(raw));
|
||||
return result;
|
||||
}
|
||||
|
||||
private Long resolveModelId(WikiTransformationEntity transformation, Long kbId) {
|
||||
if (transformation.getModelId() != null) return transformation.getModelId();
|
||||
if (modelRoutingService == null) {
|
||||
|
||||
@ -713,6 +713,55 @@ public class WikiTool {
|
||||
}
|
||||
}
|
||||
|
||||
@Tool(description = """
|
||||
Run a transformation template against an existing wiki page and return
|
||||
the generated text. Use this when you want to derive a new artifact
|
||||
from an existing page — e.g. "summarize the contract-review page",
|
||||
"extract action items from this meeting-notes page". The run output
|
||||
is persisted in the wiki UI; pass slug (not page id) for convenience.
|
||||
""")
|
||||
public String wiki_apply_transformation_to_page(
|
||||
@ToolParam(description = "Agent ID") Long agentId,
|
||||
@ToolParam(description = "Transformation name (from wiki_list_transformations)") String name,
|
||||
@ToolParam(description = "Source wiki page slug to run the transformation against") String slug) {
|
||||
if (name == null || name.isBlank()) return error("name is required");
|
||||
if (slug == null || slug.isBlank()) return error("slug is required");
|
||||
Long kbId = resolveKbId(agentId);
|
||||
if (kbId == null) return error("No wiki knowledge base found for this agent");
|
||||
if (transformationService == null || transformationExecutor == null) {
|
||||
return error("Transformations not available");
|
||||
}
|
||||
|
||||
WikiPageEntity page = pageService.getBySlug(kbId, slug);
|
||||
if (page == null) return error("Page not found: " + slug);
|
||||
|
||||
WikiKnowledgeBaseEntity kb = kbService.getById(kbId);
|
||||
Long wsId = (kb == null || kb.getWorkspaceId() == null) ? 1L : kb.getWorkspaceId();
|
||||
|
||||
WikiTransformationEntity template = transformationService.findByName(kbId, wsId, name).orElse(null);
|
||||
if (template == null) return error("Transformation not found: " + name);
|
||||
|
||||
try {
|
||||
WikiTransformationRunEntity run = transformationExecutor.runOnPageSync(template, page.getId(), "agent_tool");
|
||||
if (run == null) return error("Transformation is disabled: " + name);
|
||||
if ("failed".equals(run.getStatus())) {
|
||||
return error("Transformation failed: " + run.getError());
|
||||
}
|
||||
return JSONUtil.createObj()
|
||||
.set("ok", true)
|
||||
.set("runId", run.getId())
|
||||
.set("transformation", template.getName())
|
||||
.set("inputPage", slug)
|
||||
.set("output", run.getOutput())
|
||||
.toString();
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
return error(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.warn("[WikiTool] wiki_apply_transformation_to_page failed: {}", e.getMessage());
|
||||
return error("Apply failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private Long resolveKbId(Long agentId) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user