mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +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 ====================
|
// ==================== Apply ====================
|
||||||
|
|
||||||
@RequireWorkspaceRole("member")
|
@RequireWorkspaceRole("member")
|
||||||
@Operation(summary = "Run a transformation against a raw material",
|
@Operation(summary = "Run a transformation against a raw material or wiki page",
|
||||||
description = "Set sync=true to block until the LLM call returns "
|
description = "Body accepts exactly one of {rawId, pageId}. Set sync=true to block "
|
||||||
+ "(the response carries the populated run). "
|
+ "until the LLM call returns; when false (default) the call returns "
|
||||||
+ "When false (default) the call returns immediately with the pending run row.")
|
+ "immediately with the pending run row.")
|
||||||
@PostMapping("/{id}/apply")
|
@PostMapping("/{id}/apply")
|
||||||
public R<WikiTransformationRunEntity> apply(@PathVariable Long id,
|
public R<WikiTransformationRunEntity> apply(@PathVariable Long id,
|
||||||
@RequestBody Map<String, Object> body,
|
@RequestBody Map<String, Object> body,
|
||||||
@ -116,16 +116,23 @@ public class WikiTransformationController {
|
|||||||
verifyTemplateWorkspace(t, workspaceId);
|
verifyTemplateWorkspace(t, workspaceId);
|
||||||
|
|
||||||
Object rawIdRaw = body == null ? null : body.get("rawId");
|
Object rawIdRaw = body == null ? null : body.get("rawId");
|
||||||
if (rawIdRaw == null) {
|
Object pageIdRaw = body == null ? null : body.get("pageId");
|
||||||
return R.fail("rawId is required");
|
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) {
|
if (rawIdRaw != null) {
|
||||||
return R.ok(executor.runOnRawSync(t, rawId, "manual"));
|
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();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -93,6 +93,141 @@ public class WikiTransformationExecutor {
|
|||||||
() -> runOnRawSync(transformation, rawId, triggeredBy), WORKER);
|
() -> 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
|
* Run the transformation against the given raw material and persist
|
||||||
* the outcome. The returned entity is the persisted run row, regardless
|
* the outcome. The returned entity is the persisted run row, regardless
|
||||||
@ -134,32 +269,8 @@ public class WikiTransformationExecutor {
|
|||||||
if (inputText == null || inputText.isBlank()) {
|
if (inputText == null || inputText.isBlank()) {
|
||||||
throw new IllegalStateException("Raw material has no extractable text yet");
|
throw new IllegalStateException("Raw material has no extractable text yet");
|
||||||
}
|
}
|
||||||
String trimmedInput = inputText.length() > MAX_INPUT_CHARS
|
String output = renderAndCallLlm(transformation, raw.getKbId(),
|
||||||
? inputText.substring(0, MAX_INPUT_CHARS) + "\n…(truncated)"
|
safeTitle(raw), inputText, run);
|
||||||
: 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");
|
|
||||||
}
|
|
||||||
// Honour a mid-flight cancel: the cancel endpoint flipped the run
|
// Honour a mid-flight cancel: the cancel endpoint flipped the run
|
||||||
// row to 'cancelled' while the LLM was still working. Drop the
|
// row to 'cancelled' while the LLM was still working. Drop the
|
||||||
// output and stop here rather than overwrite the cancelled state.
|
// output and stop here rather than overwrite the cancelled state.
|
||||||
@ -234,14 +345,6 @@ public class WikiTransformationExecutor {
|
|||||||
return true;
|
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) {
|
private Long resolveModelId(WikiTransformationEntity transformation, Long kbId) {
|
||||||
if (transformation.getModelId() != null) return transformation.getModelId();
|
if (transformation.getModelId() != null) return transformation.getModelId();
|
||||||
if (modelRoutingService == null) {
|
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 ====================
|
// ==================== Helpers ====================
|
||||||
|
|
||||||
private Long resolveKbId(Long agentId) {
|
private Long resolveKbId(Long agentId) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user