代码提交
This commit is contained in:
1 parent
1eae02f782
commit
e58da3fbdf
4 files changed
+240
-57
No files matched your search
@@ -563,7 +563,12 @@ export function MessageList({
|
|||||||
questionMarkdown={
|
questionMarkdown={
|
||||||
msg.id ? questionMarkdownByAssistantId.get(msg.id) : undefined
|
msg.id ? questionMarkdownByAssistantId.get(msg.id) : undefined
|
||||||
}
|
}
|
||||||
isLoading={messageIsLoading}
|
isLoading={
|
||||||
|
isMultiRoundRagMessage(msg)
|
||||||
|
? thread.isLoading &&
|
||||||
|
msg.id === messages[messages.length - 1]?.id
|
||||||
|
: messageIsLoading
|
||||||
|
}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
tokenUsageEnabled={tokenUsageEnabled}
|
tokenUsageEnabled={tokenUsageEnabled}
|
||||||
displaySources={displaySources}
|
displaySources={displaySources}
|
||||||
|
|||||||
@@ -181,17 +181,110 @@ function lastRound(trace: MultiRoundRagTrace): MultiRoundRagRound {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseEventLine(line: string): MultiRoundEvent | null {
|
function eventsFromLine(line: string): MultiRoundEvent[] {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed.startsWith("data:")) return null;
|
if (!trimmed || trimmed.startsWith(":")) return [];
|
||||||
const payload = trimmed.replace(/^data:\s?/, "").trim();
|
let payload = trimmed;
|
||||||
if (!payload || payload === "[DONE]") return { event: "done" };
|
if (/^data:\s?/i.test(trimmed)) {
|
||||||
try {
|
payload = trimmed.replace(/^data:\s?/i, "").trim();
|
||||||
const parsed = parseJson(payload);
|
} else if (/^(id|event|retry):/i.test(trimmed)) {
|
||||||
return asRecord(parsed) as MultiRoundEvent | null;
|
return [];
|
||||||
} catch {
|
}
|
||||||
|
if (!payload) return [];
|
||||||
|
if (payload === "[DONE]") return [{ event: "done" }];
|
||||||
|
if (payload.startsWith("{") || payload.startsWith("[")) {
|
||||||
|
try {
|
||||||
|
return expandStreamEvents(JSON.parse(payload));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expandStreamEvents(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function asStreamEvent(value: unknown): MultiRoundEvent | null {
|
||||||
|
const parsed = parseJson(value);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const record = asRecord(parsed);
|
||||||
|
if (!record) {
|
||||||
|
if (typeof parsed === "string" && parsed.trim() && !/^[{[]/.test(parsed.trim())) {
|
||||||
|
return { event: "message", data: parsed };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const eventName = asText(record.event ?? record.type ?? record.event_type);
|
||||||
|
if (eventName) {
|
||||||
|
return {
|
||||||
|
event: eventName,
|
||||||
|
data: record.data !== undefined ? record.data : (record.payload ?? record),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (record.tool_name || record.name) {
|
||||||
|
return { event: "tool_call", data: record };
|
||||||
|
}
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandStreamEvents(value: unknown): MultiRoundEvent[] {
|
||||||
|
const parsed = parseJson(value);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.flatMap((item) => expandStreamEvents(item));
|
||||||
|
}
|
||||||
|
const event = asStreamEvent(parsed);
|
||||||
|
return event ? [event] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractJsonObjects(raw: string): { objects: string[]; rest: string } {
|
||||||
|
const objects: string[] = [];
|
||||||
|
let depth = 0;
|
||||||
|
let stringQuote: '"' | "'" | null = null;
|
||||||
|
let escaped = false;
|
||||||
|
let start = -1;
|
||||||
|
for (let index = 0; index < raw.length; index += 1) {
|
||||||
|
const char = raw[index]!;
|
||||||
|
if (stringQuote) {
|
||||||
|
if (escaped) escaped = false;
|
||||||
|
else if (char === "\\") escaped = true;
|
||||||
|
else if (char === stringQuote) stringQuote = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '"' || char === "'") {
|
||||||
|
stringQuote = char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "{") {
|
||||||
|
if (depth === 0) start = index;
|
||||||
|
depth += 1;
|
||||||
|
} else if (char === "}") {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0 && start !== -1) {
|
||||||
|
objects.push(raw.slice(start, index + 1));
|
||||||
|
start = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return depth > 0 && start !== -1
|
||||||
|
? { objects, rest: raw.slice(start) }
|
||||||
|
: { objects, rest: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function pullStreamEvents(buffer: string): { events: MultiRoundEvent[]; rest: string } {
|
||||||
|
const events: MultiRoundEvent[] = [];
|
||||||
|
const lines = buffer.split(/\r?\n/);
|
||||||
|
const restLine = lines.pop() ?? "";
|
||||||
|
for (const line of lines) {
|
||||||
|
events.push(...eventsFromLine(line));
|
||||||
|
}
|
||||||
|
if (events.length > 0) {
|
||||||
|
return { events, rest: restLine };
|
||||||
|
}
|
||||||
|
const extracted = extractJsonObjects(buffer);
|
||||||
|
for (const jsonText of extracted.objects) {
|
||||||
|
events.push(...expandStreamEvents(parseJson(jsonText)));
|
||||||
|
}
|
||||||
|
return { events, rest: extracted.rest || restLine };
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapReferenceToSource(item: unknown, index: number): MultiRoundRagSource {
|
function mapReferenceToSource(item: unknown, index: number): MultiRoundRagSource {
|
||||||
@@ -230,6 +323,22 @@ function extractInfoText(result: unknown): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeThink(existing: string, incoming: string) {
|
||||||
|
if (!incoming) return existing;
|
||||||
|
if (!existing) return incoming;
|
||||||
|
if (incoming.startsWith(existing)) return incoming;
|
||||||
|
if (existing.endsWith(incoming)) return existing;
|
||||||
|
return `${existing}${incoming}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractThinkContent(chatText: string) {
|
||||||
|
const closed = chatText.match(/<think>([\s\S]*?)<\/think>/i);
|
||||||
|
if (closed?.[1] != null) return closed[1].trim();
|
||||||
|
const open = chatText.match(/<think>([\s\S]*)$/i);
|
||||||
|
if (open?.[1] != null) return open[1].trim();
|
||||||
|
return chatText.replace(/<\/?think>/gi, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GeneralQA `handleToolCall` 的独立实现:按 tool_name 把 SSE 工具事件填进最近一轮 trace。
|
* GeneralQA `handleToolCall` 的独立实现:按 tool_name 把 SSE 工具事件填进最近一轮 trace。
|
||||||
* `show_text` / `show-text` 负责思考与最终回答;检索类工具先写检索词,再把 result 渲染成卡片。
|
* `show_text` / `show-text` 负责思考与最终回答;检索类工具先写检索词,再把 result 渲染成卡片。
|
||||||
@@ -245,9 +354,21 @@ export function applyMultiRoundToolCall(trace: MultiRoundRagTrace, toolValue: un
|
|||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case "show_text": {
|
case "show_text": {
|
||||||
const chatText = asText(input.text ?? input.content);
|
const chatText = asText(input.text ?? input.content);
|
||||||
|
if (!chatText) return;
|
||||||
if (chatText.includes("<think>")) {
|
if (chatText.includes("<think>")) {
|
||||||
const thinkContent = chatText.replace("<think>", "").replace("</think>", "").trim();
|
const thinkContent = extractThinkContent(chatText);
|
||||||
if (trace.rounds[0]?.title !== "正在准备检索") {
|
const firstRound = trace.rounds[0];
|
||||||
|
const stillOnFirstThink =
|
||||||
|
firstRound?.title === "正在准备检索" ||
|
||||||
|
((!round.query && round.retrievals.length === 0) &&
|
||||||
|
(round.title === "思考过程" || round.title === "思考中" || round.title === "正在准备检索"));
|
||||||
|
if (stillOnFirstThink && firstRound) {
|
||||||
|
firstRound.title = "思考过程";
|
||||||
|
firstRound.thoughtMarkdown = mergeThink(firstRound.thoughtMarkdown, thinkContent);
|
||||||
|
} else if (!round.query && round.retrievals.length === 0) {
|
||||||
|
round.title = round.title === "正在准备检索" ? "思考过程" : round.title;
|
||||||
|
round.thoughtMarkdown = mergeThink(round.thoughtMarkdown, thinkContent);
|
||||||
|
} else {
|
||||||
round.queryLoading = false;
|
round.queryLoading = false;
|
||||||
round.queryShow = round.retrievals.length > 0;
|
round.queryShow = round.retrievals.length > 0;
|
||||||
trace.rounds.push({
|
trace.rounds.push({
|
||||||
@@ -259,9 +380,6 @@ export function applyMultiRoundToolCall(trace: MultiRoundRagTrace, toolValue: un
|
|||||||
queryShow: true,
|
queryShow: true,
|
||||||
retrievals: [],
|
retrievals: [],
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
trace.rounds[0]!.title = "思考过程";
|
|
||||||
trace.rounds[0]!.thoughtMarkdown = thinkContent;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
round.queryLoading = false;
|
round.queryLoading = false;
|
||||||
@@ -515,8 +633,20 @@ export function applyMultiRoundToolCall(trace: MultiRoundRagTrace, toolValue: un
|
|||||||
|
|
||||||
function consumeToolCallPayload(trace: MultiRoundRagTrace, payload: unknown) {
|
function consumeToolCallPayload(trace: MultiRoundRagTrace, payload: unknown) {
|
||||||
const parsed = parseJson(payload);
|
const parsed = parseJson(payload);
|
||||||
const tools = Array.isArray(parsed) ? parsed : [parsed];
|
if (Array.isArray(parsed)) {
|
||||||
for (const tool of tools) applyMultiRoundToolCall(trace, tool);
|
for (const tool of parsed) applyMultiRoundToolCall(trace, tool);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const record = asRecord(parsed);
|
||||||
|
if (record?.tool_name || record?.name) {
|
||||||
|
applyMultiRoundToolCall(trace, record);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (record?.data != null) {
|
||||||
|
consumeToolCallPayload(trace, record.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyMultiRoundToolCall(trace, parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
function textFromEventData(value: unknown): string {
|
function textFromEventData(value: unknown): string {
|
||||||
@@ -532,7 +662,7 @@ function applyStreamEvent(
|
|||||||
onSources?: (sources: MultiRoundRagSource[]) => void,
|
onSources?: (sources: MultiRoundRagSource[]) => void,
|
||||||
onError?: (message: string) => void,
|
onError?: (message: string) => void,
|
||||||
) {
|
) {
|
||||||
const eventType = asText(event.event);
|
const eventType = normalizeToolName(asText(event.event));
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
if (eventType === "tool_call") {
|
if (eventType === "tool_call") {
|
||||||
consumeToolCallPayload(trace, data);
|
consumeToolCallPayload(trace, data);
|
||||||
@@ -560,6 +690,8 @@ function applyStreamEvent(
|
|||||||
const finalText = textFromEventData(data);
|
const finalText = textFromEventData(data);
|
||||||
if (finalText && !trace.finalAnswerMarkdown) trace.finalAnswerMarkdown += finalText;
|
if (finalText && !trace.finalAnswerMarkdown) trace.finalAnswerMarkdown += finalText;
|
||||||
finishTrace(trace);
|
finishTrace(trace);
|
||||||
|
} else if (asRecord(data)?.tool_name || asRecord(parseJson(data))?.tool_name) {
|
||||||
|
consumeToolCallPayload(trace, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +711,10 @@ async function* readMultiRoundSse(
|
|||||||
): AsyncGenerator<MultiRoundEvent> {
|
): AsyncGenerator<MultiRoundEvent> {
|
||||||
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/generate`, {
|
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/generate`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "text/event-stream, application/json, text/plain",
|
||||||
|
},
|
||||||
signal,
|
signal,
|
||||||
body: JSON.stringify({ prompt: question }),
|
body: JSON.stringify({ prompt: question }),
|
||||||
});
|
});
|
||||||
@@ -596,17 +731,25 @@ async function* readMultiRoundSse(
|
|||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
const lines = buffer.split(/\r?\n/);
|
const pulled = pullStreamEvents(buffer);
|
||||||
buffer = lines.pop() ?? "";
|
buffer = pulled.rest;
|
||||||
for (const line of lines) {
|
for (const event of pulled.events) {
|
||||||
const event = parseEventLine(line);
|
yield event;
|
||||||
if (event) yield event;
|
if (normalizeToolName(asText(event.event)) === "done") return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buffer += decoder.decode();
|
buffer += decoder.decode();
|
||||||
for (const line of buffer.split(/\r?\n/)) {
|
const pulled = pullStreamEvents(`${buffer}\n`);
|
||||||
const event = parseEventLine(line);
|
for (const event of pulled.events) {
|
||||||
if (event) yield event;
|
yield event;
|
||||||
|
}
|
||||||
|
if (pulled.rest.trim()) {
|
||||||
|
for (const event of [
|
||||||
|
...eventsFromLine(pulled.rest),
|
||||||
|
...expandStreamEvents(parseJson(pulled.rest)),
|
||||||
|
]) {
|
||||||
|
yield event;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,8 +765,10 @@ export function useMultiRoundRagMessages(threadId: string) {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const controllerRef = useRef<AbortController | null>(null);
|
const controllerRef = useRef<AbortController | null>(null);
|
||||||
|
const inFlightThreadIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (inFlightThreadIdRef.current === threadId) return;
|
||||||
const stored = readStoredMessages(threadId);
|
const stored = readStoredMessages(threadId);
|
||||||
messagesRef.current = stored;
|
messagesRef.current = stored;
|
||||||
setMessages(stored);
|
setMessages(stored);
|
||||||
@@ -683,6 +828,7 @@ export function useMultiRoundRagMessages(threadId: string) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
updateMessages(targetThreadId, (current) => [...current, userMessage, assistantMessage]);
|
updateMessages(targetThreadId, (current) => [...current, userMessage, assistantMessage]);
|
||||||
|
inFlightThreadIdRef.current = targetThreadId;
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
@@ -717,6 +863,7 @@ export function useMultiRoundRagMessages(threadId: string) {
|
|||||||
setError,
|
setError,
|
||||||
);
|
);
|
||||||
refreshAssistant();
|
refreshAssistant();
|
||||||
|
if (normalizeToolName(asText(event.event)) === "done") break;
|
||||||
}
|
}
|
||||||
finishTrace(trace);
|
finishTrace(trace);
|
||||||
refreshAssistant();
|
refreshAssistant();
|
||||||
@@ -729,6 +876,9 @@ export function useMultiRoundRagMessages(threadId: string) {
|
|||||||
}
|
}
|
||||||
refreshAssistant();
|
refreshAssistant();
|
||||||
} finally {
|
} finally {
|
||||||
|
if (inFlightThreadIdRef.current === targetThreadId) {
|
||||||
|
inFlightThreadIdRef.current = null;
|
||||||
|
}
|
||||||
if (controllerRef.current === controller) controllerRef.current = null;
|
if (controllerRef.current === controller) controllerRef.current = null;
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,8 +138,9 @@ const emptyForm = {
|
|||||||
html_negative_prompt: "",
|
html_negative_prompt: "",
|
||||||
html_reference_favorite_id: "",
|
html_reference_favorite_id: "",
|
||||||
html_model_name: "",
|
html_model_name: "",
|
||||||
// 参考选了内置模板(tpl:<id>)时的「数据抽取」开关:关闭=用模板默认数据,开启=检索抽取。
|
// 参考选了内置模板(tpl:<id>)时的「数据抽取」开关:默认开启(检索抽取+抽不到回退
|
||||||
html_template_extract: false,
|
// 模板原数据),仅可转换模板生效;显式关闭=直接用模板默认数据。
|
||||||
|
html_template_extract: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 覆盖 Textarea 默认 field-sizing-content,避免输入过多时撑破弹窗 */
|
/** 覆盖 Textarea 默认 field-sizing-content,避免输入过多时撑破弹窗 */
|
||||||
@@ -331,7 +332,8 @@ export default function ScheduledTasksPage({
|
|||||||
html_negative_prompt: html.negative_prompt ?? "",
|
html_negative_prompt: html.negative_prompt ?? "",
|
||||||
html_reference_favorite_id: html.reference_favorite_id ?? "",
|
html_reference_favorite_id: html.reference_favorite_id ?? "",
|
||||||
html_model_name: html.model_name ?? "",
|
html_model_name: html.model_name ?? "",
|
||||||
html_template_extract: !!html.template_extract,
|
// 与后端缺省一致:未存该键(老任务)视为开启,仅显式 false 才关闭。
|
||||||
|
html_template_extract: html.template_extract !== false,
|
||||||
});
|
});
|
||||||
// Prefer the canonical cron_expr over the user-typed schedule_text: the
|
// Prefer the canonical cron_expr over the user-typed schedule_text: the
|
||||||
// latter may be natural language ("每天9点") that parseCron can't read,
|
// latter may be natural language ("每天9点") that parseCron can't read,
|
||||||
@@ -358,6 +360,10 @@ export default function ScheduledTasksPage({
|
|||||||
isTemplateAgent || form.require_html,
|
isTemplateAgent || form.require_html,
|
||||||
);
|
);
|
||||||
const selectedTemplate = htmlTemplates.find((t) => t.id === form.template_id);
|
const selectedTemplate = htmlTemplates.find((t) => t.id === form.template_id);
|
||||||
|
// 「参考收藏页面」选中的内置模板(require_html 模式,值形如 tpl:<id>)。
|
||||||
|
const selectedTplRef = htmlTemplates.find(
|
||||||
|
(t) => `tpl:${t.id}` === form.html_reference_favorite_id,
|
||||||
|
);
|
||||||
// 选中的模板是「只展示」型(convertible===false)时,不需要源 JSON,隐藏上传/输入。
|
// 选中的模板是「只展示」型(convertible===false)时,不需要源 JSON,隐藏上传/输入。
|
||||||
const templateDisplayOnly =
|
const templateDisplayOnly =
|
||||||
isTemplateAgent && selectedTemplate?.convertible === false;
|
isTemplateAgent && selectedTemplate?.convertible === false;
|
||||||
@@ -1396,23 +1402,28 @@ export default function ScheduledTasksPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 参考选了内置模板(tpl:<id>)时:数据抽取开关。关闭=用模板默认数据。 */}
|
{/* 参考选了内置模板(tpl:<id>)时:数据抽取开关(仅可转换模板;只展示模板给提示)。 */}
|
||||||
{form.html_reference_favorite_id.startsWith("tpl:") && (
|
{form.html_reference_favorite_id.startsWith("tpl:") &&
|
||||||
<div className="flex items-center justify-between rounded-md border p-3">
|
(selectedTplRef?.convertible === false ? (
|
||||||
<div className="min-w-0 pr-3">
|
<p className="text-muted-foreground rounded-md border p-3 text-xs">
|
||||||
<p className="text-sm font-medium">执行数据抽取</p>
|
该模板暂为「直接展示已有数据」,执行时将原样输出其现有页面,不做数据抽取。
|
||||||
<p className="text-muted-foreground text-xs">
|
</p>
|
||||||
关闭则直接用所选模板的默认数据生成(含全部子页面);开启才检索并按模板结构抽取数据填充。
|
) : (
|
||||||
</p>
|
<div className="flex items-center justify-between rounded-md border p-3">
|
||||||
|
<div className="min-w-0 pr-3">
|
||||||
|
<p className="text-sm font-medium">执行数据抽取</p>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
开启后执行时由所选智能体检索生成内容,并按模板结构抽取数据填充;抽取不到的子页面自动回退模板原有数据。关闭则直接用所选模板的默认数据生成(含全部子页面)。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={form.html_template_extract}
|
||||||
|
onCheckedChange={(v) =>
|
||||||
|
setForm((value) => ({ ...value, html_template_extract: v }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
))}
|
||||||
checked={form.html_template_extract}
|
|
||||||
onCheckedChange={(v) =>
|
|
||||||
setForm((value) => ({ ...value, html_template_extract: v }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="text-muted-foreground text-xs">生成模型</p>
|
<p className="text-muted-foreground text-xs">生成模型</p>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
+29
-12
@@ -1445,6 +1445,27 @@ class ScheduledTaskService:
|
|||||||
return "\n".join(p for p in parts if p)
|
return "\n".join(p for p in parts if p)
|
||||||
return str(content)
|
return str(content)
|
||||||
|
|
||||||
|
async def _agent_soul_and_model(
|
||||||
|
self, task: dict[str, Any], model_name: str | None
|
||||||
|
) -> tuple[str | None, str | None]:
|
||||||
|
"""Resolve the task agent's SOUL; fall back to its model when none chosen.
|
||||||
|
|
||||||
|
Used by both template pipelines so the **task's selected agent** shapes
|
||||||
|
content conversion: the SOUL carries the agent's domain instructions,
|
||||||
|
and when the user didn't pick an explicit model the agent's configured
|
||||||
|
model is preferred over the system default. Returns ``(soul, model)``.
|
||||||
|
"""
|
||||||
|
agent_ref = task.get("execution_agent_name")
|
||||||
|
agent_id = await self._resolve_agent_id(agent_ref, task["user_id"]) if agent_ref else None
|
||||||
|
soul = load_agent_soul(agent_id) if agent_id else None
|
||||||
|
if not model_name and agent_id:
|
||||||
|
try:
|
||||||
|
agent_cfg = load_agent_config(agent_id)
|
||||||
|
model_name = (agent_cfg.model if agent_cfg else None) or None
|
||||||
|
except Exception:
|
||||||
|
model_name = None
|
||||||
|
return soul, model_name
|
||||||
|
|
||||||
async def _convert_to_template_data(
|
async def _convert_to_template_data(
|
||||||
self,
|
self,
|
||||||
template_id: str,
|
template_id: str,
|
||||||
@@ -1574,8 +1595,9 @@ class ScheduledTaskService:
|
|||||||
cfg = self._html_config(task)
|
cfg = self._html_config(task)
|
||||||
model_name = str(cfg.get("model_name") or "").strip() or None
|
model_name = str(cfg.get("model_name") or "").strip() or None
|
||||||
# 「执行数据抽取」开关**只**控制 HTML 用「抽取数据」还是「模板默认数据」,
|
# 「执行数据抽取」开关**只**控制 HTML 用「抽取数据」还是「模板默认数据」,
|
||||||
# 不影响 Markdown 生成等其它正常操作。
|
# 不影响 Markdown 生成等其它正常操作。缺省(未存该键的老任务)视为开启:
|
||||||
extract = bool(cfg.get("template_extract"))
|
# 模板有真实数据来源后,默认走「智能体检索 → 转换填充」,显式 False 才关闭。
|
||||||
|
extract = cfg.get("template_extract") is not False
|
||||||
also_markdown = _task_requires_markdown(task)
|
also_markdown = _task_requires_markdown(task)
|
||||||
prompt = str(task.get("prompt") or "").strip()
|
prompt = str(task.get("prompt") or "").strip()
|
||||||
description = str(task.get("description") or "").strip()
|
description = str(task.get("description") or "").strip()
|
||||||
@@ -1626,9 +1648,12 @@ class ScheduledTaskService:
|
|||||||
source = (data_text or "").strip()
|
source = (data_text or "").strip()
|
||||||
used_default = True
|
used_default = True
|
||||||
if do_extract and source:
|
if do_extract and source:
|
||||||
|
# 转换阶段同样带上任务所选智能体:SOUL 提供领域口径,未显式选模型时
|
||||||
|
# 兜底用该智能体配置的模型(与模板填充模式一致)。
|
||||||
|
soul, conv_model = await self._agent_soul_and_model(task, model_name)
|
||||||
try:
|
try:
|
||||||
converted = await self._convert_to_template_data(
|
converted = await self._convert_to_template_data(
|
||||||
tpl_id, source, model_name=model_name, soul=None, run_id=run_id
|
tpl_id, source, model_name=conv_model, soul=soul, run_id=run_id
|
||||||
)
|
)
|
||||||
# 子页面没抽到有效内容的,回退模板原有数据,避免子页面空白。
|
# 子页面没抽到有效内容的,回退模板原有数据,避免子页面空白。
|
||||||
data_obj = template_fill.merge_with_reference(tpl_id, converted)
|
data_obj = template_fill.merge_with_reference(tpl_id, converted)
|
||||||
@@ -1712,16 +1737,8 @@ class ScheduledTaskService:
|
|||||||
tcfg = tcfg if isinstance(tcfg, dict) else {}
|
tcfg = tcfg if isinstance(tcfg, dict) else {}
|
||||||
|
|
||||||
# 选中的内置 agent 的 SOUL + 模型(template_config.model_name 优先,其次 agent.model)。
|
# 选中的内置 agent 的 SOUL + 模型(template_config.model_name 优先,其次 agent.model)。
|
||||||
agent_ref = task.get("execution_agent_name")
|
|
||||||
agent_id = await self._resolve_agent_id(agent_ref, task["user_id"]) if agent_ref else None
|
|
||||||
soul = load_agent_soul(agent_id) if agent_id else None
|
|
||||||
model_name = str(tcfg.get("model_name") or "").strip() or None
|
model_name = str(tcfg.get("model_name") or "").strip() or None
|
||||||
if not model_name and agent_id:
|
soul, model_name = await self._agent_soul_and_model(task, model_name)
|
||||||
try:
|
|
||||||
agent_cfg = load_agent_config(agent_id)
|
|
||||||
model_name = (agent_cfg.model if agent_cfg else None) or None
|
|
||||||
except Exception:
|
|
||||||
model_name = None
|
|
||||||
|
|
||||||
converted = await self._convert_to_template_data(
|
converted = await self._convert_to_template_data(
|
||||||
template_id, source, model_name=model_name, soul=soul, run_id=run_id
|
template_id, source, model_name=model_name, soul=soul, run_id=run_id
|
||||||
|
|||||||
Reference in new issue
Block a user