技能展示样本运行器 + 参考文献来源过滤调整
- skills 路由新增 _run_skill_python_sample,按本地 Python 候选实际执行 技能脚本,作为展示适配器的真实样本来源 - skill-display 运行时与适配器调整,参考文献收集放宽起始消息约束 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
26a388e3bf
commit
d3d7db1f34
4 files changed
+303
-104
No files matched your search
@@ -65,25 +65,16 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|||||||
() => findLatestHumanMessageIndex(thread.values.messages ?? []),
|
() => findLatestHumanMessageIndex(thread.values.messages ?? []),
|
||||||
[thread.values.messages],
|
[thread.values.messages],
|
||||||
);
|
);
|
||||||
const latestAssistant = useMemo(
|
|
||||||
() => findLatestAssistantMessageAfter(thread.values.messages ?? [], latestHumanIndex),
|
|
||||||
[latestHumanIndex, thread.values.messages],
|
|
||||||
);
|
|
||||||
const displaySources = useMemo(() => {
|
const displaySources = useMemo(() => {
|
||||||
if (!settings.display.ragModeEnabled) {
|
if (!settings.display.ragModeEnabled) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const messages = thread.values.messages ?? [];
|
const messages = thread.values.messages ?? [];
|
||||||
if (!latestAssistant?.id) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return collectRuntimeDisplaySources(messages, skillDisplayMap, {
|
return collectRuntimeDisplaySources(messages, skillDisplayMap, {
|
||||||
afterMessageId: latestHumanIndex >= 0 ? messages[latestHumanIndex]?.id : undefined,
|
afterMessageId: latestHumanIndex >= 0 ? messages[latestHumanIndex]?.id : undefined,
|
||||||
beforeMessageId: latestAssistant.id,
|
|
||||||
includeBeforeMessage: true,
|
|
||||||
includeModes: ["citation"],
|
includeModes: ["citation"],
|
||||||
});
|
});
|
||||||
}, [settings.display.ragModeEnabled, skillDisplayMap, latestAssistant?.id, latestHumanIndex, thread.values.messages]);
|
}, [settings.display.ragModeEnabled, skillDisplayMap, latestHumanIndex, thread.values.messages]);
|
||||||
const referenceSources = useMemo(
|
const referenceSources = useMemo(
|
||||||
() => displaySources.filter((source) => source.mode === "citation"),
|
() => displaySources.filter((source) => source.mode === "citation"),
|
||||||
[displaySources],
|
[displaySources],
|
||||||
@@ -93,7 +84,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|||||||
if (hasCitationReferences) {
|
if (hasCitationReferences) {
|
||||||
setReferencesOpen(true);
|
setReferencesOpen(true);
|
||||||
}
|
}
|
||||||
}, [hasCitationReferences, latestAssistant?.id]);
|
}, [hasCitationReferences]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleShowReference = () => {
|
const handleShowReference = () => {
|
||||||
setReferencesOpen(true);
|
setReferencesOpen(true);
|
||||||
@@ -333,17 +324,4 @@ function findLatestHumanMessageIndex(messages: Message[]) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLatestAssistantMessageAfter(messages: Message[], afterIndex: number) {
|
|
||||||
for (let index = messages.length - 1; index >= 0; index--) {
|
|
||||||
if (afterIndex >= 0 && index <= afterIndex) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const message = messages[index];
|
|
||||||
if (message?.type === "ai") {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ChatBox };
|
export { ChatBox };
|
||||||
@@ -34,62 +34,6 @@ export const emptyDisplayConfig = (): SkillDisplayConfig => ({
|
|||||||
tableColumns: [],
|
tableColumns: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mockKnowledgeSearchV2Result = {
|
|
||||||
success: true,
|
|
||||||
query: "参考文献模式测试 知识库固定测试",
|
|
||||||
count: 4,
|
|
||||||
source: "fixed-display-sample",
|
|
||||||
results: [
|
|
||||||
{
|
|
||||||
index: 1,
|
|
||||||
recUuid: "ksv2-fixed-001",
|
|
||||||
relevance_score: 0.982,
|
|
||||||
title: "参考文献模式测试:参考文献模式固定测试数据一",
|
|
||||||
source_category: "知识库/测试资料",
|
|
||||||
source: "knowledge-search-v2/fixed/source-001",
|
|
||||||
publish_time: "2026-05-25",
|
|
||||||
content_preview:
|
|
||||||
"这是 knowledge-search-v2 返回的第一条固定测试资料,用于验证右侧参考文献列表、正文编号以及 recUuid 模板跳转是否正常。",
|
|
||||||
content_length: 168,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 2,
|
|
||||||
recUuid: "ksv2-fixed-002",
|
|
||||||
relevance_score: 0.936,
|
|
||||||
title: "参考文献模式测试:结构化字段映射验证",
|
|
||||||
source_category: "知识库/字段映射",
|
|
||||||
source: "knowledge-search-v2/fixed/source-002",
|
|
||||||
publish_time: "2026-05-24",
|
|
||||||
content_preview:
|
|
||||||
"本条用于验证 title、source_category、source、publish_time、content_preview、relevance_score、recUuid 等字段能被前端正确识别并渲染。",
|
|
||||||
content_length: 154,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 3,
|
|
||||||
recUuid: "ksv2-fixed-003",
|
|
||||||
relevance_score: 0.887,
|
|
||||||
title: "参考文献模式测试:多引用排序验证",
|
|
||||||
source_category: "知识库/排序验证",
|
|
||||||
source: "knowledge-search-v2/fixed/source-003",
|
|
||||||
publish_time: "2026-05-23",
|
|
||||||
content_preview:
|
|
||||||
"本条用于验证多条参考文献的编号顺序是否与 results 数组顺序一致,并验证点击正文编号时能定位到右侧对应参考文献。",
|
|
||||||
content_length: 132,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 4,
|
|
||||||
recUuid: "",
|
|
||||||
relevance_score: 0.801,
|
|
||||||
title: "参考文献模式测试:无 recUuid 跳转隐藏验证",
|
|
||||||
source_category: "知识库/边界场景",
|
|
||||||
source: "knowledge-search-v2/fixed/source-004",
|
|
||||||
publish_time: "2026-05-22",
|
|
||||||
content_preview: "本条故意把 recUuid 置为空,用于验证右侧弹框仍展示详情,但不展示外部跳转按钮。",
|
|
||||||
content_length: 98,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const candidateFields: Record<string, string[]> = {
|
const candidateFields: Record<string, string[]> = {
|
||||||
titleField: ["title", "m_title", "name", "subject", "headline"],
|
titleField: ["title", "m_title", "name", "subject", "headline"],
|
||||||
summaryField: ["summary", "snippet", "content_preview", "description", "abstract"],
|
summaryField: ["summary", "snippet", "content_preview", "description", "abstract"],
|
||||||
@@ -99,18 +43,23 @@ const candidateFields: Record<string, string[]> = {
|
|||||||
timeField: ["publish_time", "m_publish", "updatedAt", "createdAt", "date", "time"],
|
timeField: ["publish_time", "m_publish", "updatedAt", "createdAt", "date", "time"],
|
||||||
scoreField: ["relevance_score", "rerank_score", "score", "similarity"],
|
scoreField: ["relevance_score", "rerank_score", "score", "similarity"],
|
||||||
urlField: ["url", "link", "href", "source_url"],
|
urlField: ["url", "link", "href", "source_url"],
|
||||||
idField: ["recUuid", "id", "uuid", "docId", "taskId", "recordId"],
|
idField: ["recUuid", "rec_uuid", "uuid", "docId", "taskId", "recordId", "id"],
|
||||||
};
|
};
|
||||||
|
|
||||||
function getByPath(value: unknown, path: string): unknown {
|
function getByPath(value: unknown, path: string): unknown {
|
||||||
if (!path) return value;
|
if (!path) return value;
|
||||||
return path.split(".").reduce<unknown>((acc, key) => {
|
return path.split(".").reduce<unknown>((acc, key) => {
|
||||||
if (acc && typeof acc === "object") return (acc as Record<string, unknown>)[key];
|
if (acc && typeof acc === "object") {
|
||||||
|
return (acc as Record<string, unknown>)[key];
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, value);
|
}, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findFirstArray(value: unknown, path = ""): { path: string; items: Record<string, unknown>[] } | null {
|
function findFirstArray(
|
||||||
|
value: unknown,
|
||||||
|
path = "",
|
||||||
|
): { path: string; items: Record<string, unknown>[] } | null {
|
||||||
if (Array.isArray(value) && value.every((item) => item && typeof item === "object")) {
|
if (Array.isArray(value) && value.every((item) => item && typeof item === "object")) {
|
||||||
return { path, items: value as Record<string, unknown>[] };
|
return { path, items: value as Record<string, unknown>[] };
|
||||||
}
|
}
|
||||||
@@ -157,12 +106,12 @@ export function inferDisplayConfig(samplePayload: unknown, skillName?: string):
|
|||||||
inferred.typeField = inferred.typeField || "source_category";
|
inferred.typeField = inferred.typeField || "source_category";
|
||||||
inferred.timeField = inferred.timeField || "publish_time";
|
inferred.timeField = inferred.timeField || "publish_time";
|
||||||
inferred.scoreField = inferred.scoreField || "relevance_score";
|
inferred.scoreField = inferred.scoreField || "relevance_score";
|
||||||
inferred.idField = inferred.idField || "recUuid";
|
inferred.idField = pickField(sample, ["recUuid", "rec_uuid", "uuid", "id"]) || inferred.idField;
|
||||||
inferred.linkStrategy = "id";
|
inferred.linkStrategy = inferred.urlField ? "auto" : "id";
|
||||||
inferred.linkTemplate = "https://xxx.xxx.xxx?recuuid={recUuid}";
|
inferred.linkTemplate = "https://xxx.xxx.xxx?recuuid={recUuid}";
|
||||||
}
|
}
|
||||||
inferred.tableColumns = buildDefaultTableColumns(sample, inferred);
|
|
||||||
|
|
||||||
|
inferred.tableColumns = buildDefaultTableColumns(sample, inferred);
|
||||||
return inferred;
|
return inferred;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,14 +141,18 @@ function stringify(value: unknown, fallback = ""): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTemplate(template: string, item: Record<string, unknown>): string {
|
function renderTemplate(template: string, item: Record<string, unknown>): string {
|
||||||
return template.replace(/\{([^}]+)\}/g, (_match, key: string) => encodeURIComponent(stringify(item[key])));
|
return template.replace(/\{([^}]+)\}/g, (_match, key: string) =>
|
||||||
|
encodeURIComponent(stringify(item[key])),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLink(item: Record<string, unknown>, config: SkillDisplayConfig): string | undefined {
|
function resolveLink(item: Record<string, unknown>, config: SkillDisplayConfig): string | undefined {
|
||||||
const url = config.urlField ? stringify(item[config.urlField]) : "";
|
const url = config.urlField ? stringify(item[config.urlField]) : "";
|
||||||
const id = config.idField ? stringify(item[config.idField]) : "";
|
const id = config.idField ? stringify(item[config.idField]) : "";
|
||||||
if (config.linkStrategy === "url") return url || undefined;
|
if (config.linkStrategy === "url") return url || undefined;
|
||||||
if (config.linkStrategy === "id") return id && config.linkTemplate ? renderTemplate(config.linkTemplate, item) : undefined;
|
if (config.linkStrategy === "id") {
|
||||||
|
return id && config.linkTemplate ? renderTemplate(config.linkTemplate, item) : undefined;
|
||||||
|
}
|
||||||
return url || (id && config.linkTemplate ? renderTemplate(config.linkTemplate, item) : undefined) || undefined;
|
return url || (id && config.linkTemplate ? renderTemplate(config.linkTemplate, item) : undefined) || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +170,10 @@ export function normalizeDisplaySources(payload: unknown, config: SkillDisplayCo
|
|||||||
content: config.contentField ? stringify(item[config.contentField]) : undefined,
|
content: config.contentField ? stringify(item[config.contentField]) : undefined,
|
||||||
source: config.sourceField ? stringify(item[config.sourceField]) : undefined,
|
source: config.sourceField ? stringify(item[config.sourceField]) : undefined,
|
||||||
time: config.timeField ? stringify(item[config.timeField]) : undefined,
|
time: config.timeField ? stringify(item[config.timeField]) : undefined,
|
||||||
score: config.scoreField && typeof item[config.scoreField] === "number" ? item[config.scoreField] as number : undefined,
|
score:
|
||||||
|
config.scoreField && typeof item[config.scoreField] === "number"
|
||||||
|
? (item[config.scoreField] as number)
|
||||||
|
: undefined,
|
||||||
url: resolveLink(item, config),
|
url: resolveLink(item, config),
|
||||||
raw: item,
|
raw: item,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export type RuntimeDisplaySource = DisplaySourceItem & {
|
|||||||
|
|
||||||
export type SkillDisplayMap = Map<string, SkillDisplayConfig>;
|
export type SkillDisplayMap = Map<string, SkillDisplayConfig>;
|
||||||
|
|
||||||
|
type RuntimeToolCall = {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
args?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
export function buildSkillDisplayMap(skills: Skill[]): SkillDisplayMap {
|
export function buildSkillDisplayMap(skills: Skill[]): SkillDisplayMap {
|
||||||
const map = new Map<string, SkillDisplayConfig>();
|
const map = new Map<string, SkillDisplayConfig>();
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
@@ -163,13 +169,13 @@ export function collectRuntimeDisplaySources(
|
|||||||
if (!message || message.type !== "ai") {
|
if (!message || message.type !== "ai") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const toolCall of message.tool_calls ?? []) {
|
for (const toolCall of extractRuntimeToolCalls(message)) {
|
||||||
const toolMatch = findDisplayConfigForTool(
|
const toolMatch = findDisplayConfigForTool(
|
||||||
toolCall.name,
|
toolCall.name,
|
||||||
displays,
|
displays,
|
||||||
toolCall.args as Record<string, unknown> | undefined,
|
toolCall.args,
|
||||||
);
|
);
|
||||||
const result = findToolResultPayload(toolCall.id, messages);
|
const result = findToolResultPayload(toolCall, messages, index);
|
||||||
if (result === undefined) {
|
if (result === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -192,7 +198,7 @@ export function collectRuntimeDisplaySources(
|
|||||||
: shouldUseDefaultCitationMode(
|
: shouldUseDefaultCitationMode(
|
||||||
toolCall.name,
|
toolCall.name,
|
||||||
result,
|
result,
|
||||||
toolCall.args as Record<string, unknown> | undefined,
|
toolCall.args,
|
||||||
)
|
)
|
||||||
? normalizeDefaultDisplaySources(result)
|
? normalizeDefaultDisplaySources(result)
|
||||||
: [];
|
: [];
|
||||||
@@ -214,6 +220,80 @@ export function collectRuntimeDisplaySources(
|
|||||||
return sources;
|
return sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractRuntimeToolCalls(message: Message): RuntimeToolCall[] {
|
||||||
|
const calls: RuntimeToolCall[] = [];
|
||||||
|
const direct = (message as { tool_calls?: unknown }).tool_calls;
|
||||||
|
if (Array.isArray(direct)) {
|
||||||
|
for (const raw of direct) {
|
||||||
|
const call = normalizeRuntimeToolCall(raw);
|
||||||
|
if (call) {
|
||||||
|
calls.push(call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const kwargs = (message as { additional_kwargs?: Record<string, unknown> })
|
||||||
|
.additional_kwargs;
|
||||||
|
const extra = kwargs?.tool_calls;
|
||||||
|
if (Array.isArray(extra)) {
|
||||||
|
for (const raw of extra) {
|
||||||
|
const call = normalizeRuntimeToolCall(raw);
|
||||||
|
if (call && !calls.some((item) => item.id && item.id === call.id)) {
|
||||||
|
calls.push(call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = (message as { tool_call_chunks?: unknown }).tool_call_chunks;
|
||||||
|
if (Array.isArray(chunks)) {
|
||||||
|
for (const raw of chunks) {
|
||||||
|
const call = normalizeRuntimeToolCall(raw);
|
||||||
|
if (call && !calls.some((item) => item.id && item.id === call.id)) {
|
||||||
|
calls.push(call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRuntimeToolCall(raw: unknown): RuntimeToolCall | null {
|
||||||
|
if (!isRecord(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fn = isRecord(raw.function) ? raw.function : null;
|
||||||
|
const name =
|
||||||
|
stringifyMaybe(raw.name) ??
|
||||||
|
stringifyMaybe(fn?.name) ??
|
||||||
|
stringifyMaybe(raw.tool_name);
|
||||||
|
if (!name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id:
|
||||||
|
stringifyMaybe(raw.id) ??
|
||||||
|
stringifyMaybe(raw.tool_call_id) ??
|
||||||
|
stringifyMaybe(raw.call_id),
|
||||||
|
name,
|
||||||
|
args: normalizeToolArgs(raw.args ?? raw.arguments ?? fn?.arguments),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeToolArgs(value: unknown): Record<string, unknown> | undefined {
|
||||||
|
if (isRecord(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return isRecord(parsed) ? parsed : undefined;
|
||||||
|
} catch {
|
||||||
|
return { command: value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function findDisplayConfigForPayload(
|
export function findDisplayConfigForPayload(
|
||||||
toolName: string,
|
toolName: string,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
@@ -445,16 +525,18 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringifyMaybe(value: unknown): string | undefined {
|
||||||
|
return typeof value === "string" && value.trim() ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function findToolResultPayload(
|
function findToolResultPayload(
|
||||||
toolCallId: string | undefined,
|
toolCall: RuntimeToolCall,
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
|
toolCallMessageIndex = -1,
|
||||||
): unknown {
|
): unknown {
|
||||||
if (!toolCallId) {
|
const toolMessage =
|
||||||
return undefined;
|
findToolMessageById(toolCall.id, messages) ??
|
||||||
}
|
findToolMessageByName(toolCall, messages, toolCallMessageIndex);
|
||||||
const toolMessage = messages.find(
|
|
||||||
(message) => message.type === "tool" && message.tool_call_id === toolCallId,
|
|
||||||
);
|
|
||||||
if (!toolMessage) {
|
if (!toolMessage) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -473,6 +555,73 @@ function findToolResultPayload(
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findToolMessageById(
|
||||||
|
toolCallId: string | undefined,
|
||||||
|
messages: Message[],
|
||||||
|
): Message | undefined {
|
||||||
|
if (!toolCallId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return messages.find((message) => {
|
||||||
|
if (message.type !== "tool") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const candidates = [
|
||||||
|
(message as { tool_call_id?: unknown }).tool_call_id,
|
||||||
|
(message as { id?: unknown }).id,
|
||||||
|
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
|
||||||
|
?.tool_call_id,
|
||||||
|
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
|
||||||
|
?.id,
|
||||||
|
];
|
||||||
|
return candidates.some((candidate) => candidate === toolCallId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findToolMessageByName(
|
||||||
|
toolCall: RuntimeToolCall,
|
||||||
|
messages: Message[],
|
||||||
|
toolCallMessageIndex: number,
|
||||||
|
): Message | undefined {
|
||||||
|
const start = Math.max(toolCallMessageIndex + 1, 0);
|
||||||
|
for (let index = start; index < messages.length; index++) {
|
||||||
|
const message = messages[index];
|
||||||
|
if (!message) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (index > start && (message.type === "human" || message.type === "ai")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (message.type !== "tool") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name =
|
||||||
|
stringifyMaybe((message as { name?: unknown }).name) ??
|
||||||
|
stringifyMaybe(
|
||||||
|
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
|
||||||
|
?.name,
|
||||||
|
);
|
||||||
|
const messageToolCallId = stringifyMaybe(
|
||||||
|
(message as { tool_call_id?: unknown }).tool_call_id,
|
||||||
|
);
|
||||||
|
if (messageToolCallId && toolCall.id && messageToolCallId !== toolCall.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (name && normalizeName(name) !== normalizeName(toolCall.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const payload = extractStructuredToolPayload(message.content);
|
||||||
|
if (payload !== undefined) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
const text = extractToolText(message.content);
|
||||||
|
if (text && parseJSONLikeText(text) !== undefined) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function extractStructuredToolPayload(content: unknown): unknown {
|
function extractStructuredToolPayload(content: unknown): unknown {
|
||||||
if (isRecord(content)) {
|
if (isRecord(content)) {
|
||||||
return unwrapToolPayloadObject(content);
|
return unwrapToolPayloadObject(content);
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -412,6 +415,102 @@ def _extract_json_from_llm_output(raw: str) -> dict[str, Any] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _skill_sample_python_candidates() -> list[str]:
|
||||||
|
candidates = [
|
||||||
|
os.environ.get("DEERFLOW_SKILL_SAMPLE_PYTHON"),
|
||||||
|
r"C:\Users\ji\anaconda3\envs\open_manus\python.exe",
|
||||||
|
sys.executable,
|
||||||
|
"python",
|
||||||
|
]
|
||||||
|
result: list[str] = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if not candidate or candidate in result:
|
||||||
|
continue
|
||||||
|
result.append(candidate)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _find_skill_sample_script(skill: Skill) -> Path | None:
|
||||||
|
skill_dir = skill.skill_file.parent
|
||||||
|
preferred = [
|
||||||
|
skill_dir / "scripts" / "search.py",
|
||||||
|
skill_dir / "search.py",
|
||||||
|
skill_dir / "scripts" / "run.py",
|
||||||
|
skill_dir / "run.py",
|
||||||
|
]
|
||||||
|
for path in preferred:
|
||||||
|
if path.exists() and path.is_file():
|
||||||
|
return path
|
||||||
|
scripts_dir = skill_dir / "scripts"
|
||||||
|
if scripts_dir.exists():
|
||||||
|
for path in sorted(scripts_dir.glob("*.py")):
|
||||||
|
if path.is_file():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _run_skill_python_sample(skill: Skill) -> dict[str, Any] | None:
|
||||||
|
"""Run a skill's Python script to capture a real JSON sample.
|
||||||
|
|
||||||
|
This is used by the display adapter editor. It intentionally runs the same
|
||||||
|
script the agent would run, instead of guessing from SKILL.md examples.
|
||||||
|
"""
|
||||||
|
script = _find_skill_sample_script(skill)
|
||||||
|
if script is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
query = os.environ.get("DEERFLOW_SKILL_SAMPLE_QUERY") or "参考文献模式测试"
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.setdefault("PYTHONIOENCODING", "utf-8")
|
||||||
|
env.setdefault("DEERFLOW_MOCK_KNOWLEDGE_SEARCH", "1")
|
||||||
|
env.setdefault("KNOWLEDGE_SEARCH_V2_FIXED", "1")
|
||||||
|
|
||||||
|
for python_exe in _skill_sample_python_candidates():
|
||||||
|
try:
|
||||||
|
completed = await asyncio.to_thread(
|
||||||
|
subprocess.run,
|
||||||
|
[python_exe, str(script), query],
|
||||||
|
cwd=str(script.parent),
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as exc:
|
||||||
|
logger.warning("Skill sample runner failed for %s via %s: %s", skill.name, python_exe, exc)
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Skill sample runner crashed for %s via %s", skill.name, python_exe, exc_info=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
out_text = (completed.stdout or "").strip()
|
||||||
|
err_text = (completed.stderr or "").strip()
|
||||||
|
if completed.returncode != 0:
|
||||||
|
logger.warning(
|
||||||
|
"Skill sample script exited %s for %s via %s: %s",
|
||||||
|
completed.returncode,
|
||||||
|
skill.name,
|
||||||
|
python_exe,
|
||||||
|
err_text[-1000:],
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
parsed = _extract_json_from_llm_output(out_text)
|
||||||
|
if parsed:
|
||||||
|
parsed.setdefault("__sampleSource", "python-script")
|
||||||
|
parsed.setdefault("__sampleScript", str(script))
|
||||||
|
return parsed
|
||||||
|
logger.warning(
|
||||||
|
"Skill sample script returned non-JSON for %s via %s: stdout=%s stderr=%s",
|
||||||
|
skill.name,
|
||||||
|
python_exe,
|
||||||
|
out_text[-1000:],
|
||||||
|
err_text[-1000:],
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def _generate_skill_display_sample(skill: Skill, app_config: AppConfig) -> dict[str, Any]:
|
async def _generate_skill_display_sample(skill: Skill, app_config: AppConfig) -> dict[str, Any]:
|
||||||
"""Generate a realistic return-shape sample for ``skill`` via the LLM.
|
"""Generate a realistic return-shape sample for ``skill`` via the LLM.
|
||||||
|
|
||||||
@@ -420,6 +519,10 @@ async def _generate_skill_display_sample(skill: Skill, app_config: AppConfig) ->
|
|||||||
when the model is unconfigured, the call times out, or the output cannot
|
when the model is unconfigured, the call times out, or the output cannot
|
||||||
be parsed as a JSON object.
|
be parsed as a JSON object.
|
||||||
"""
|
"""
|
||||||
|
script_sample = await _run_skill_python_sample(skill)
|
||||||
|
if script_sample:
|
||||||
|
return script_sample
|
||||||
|
|
||||||
try:
|
try:
|
||||||
skill_content = skill.skill_file.read_text(encoding="utf-8")
|
skill_content = skill.skill_file.read_text(encoding="utf-8")
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
@@ -461,7 +564,23 @@ async def _generate_skill_display_sample(skill: Skill, app_config: AppConfig) ->
|
|||||||
logger.warning("Skill display sample LLM call timed out for %s; falling back to static mock", skill.name)
|
logger.warning("Skill display sample LLM call timed out for %s; falling back to static mock", skill.name)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Skill display sample LLM call failed for %s; falling back to static mock", skill.name, exc_info=True)
|
logger.warning("Skill display sample LLM call failed for %s; falling back to static mock", skill.name, exc_info=True)
|
||||||
return _mock_skill_display_sample(skill.name)
|
return _generic_skill_display_sample(skill.name)
|
||||||
|
|
||||||
|
|
||||||
|
def _generic_skill_display_sample(skill_name: str) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"query": "sample query",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": f"{skill_name}-sample-001",
|
||||||
|
"title": "Sample result",
|
||||||
|
"summary": "Sample structured output used to infer field mapping.",
|
||||||
|
"source": skill_name,
|
||||||
|
"updatedAt": "2026-05-25",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _mock_skill_display_sample(skill_name: str) -> dict[str, Any]:
|
def _mock_skill_display_sample(skill_name: str) -> dict[str, Any]:
|
||||||
@@ -1883,9 +2002,6 @@ async def get_skill_display_sample(
|
|||||||
skill = next((s for s in skills if s.name == skill_name), None)
|
skill = next((s for s in skills if s.name == skill_name), None)
|
||||||
if skill is None:
|
if skill is None:
|
||||||
raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found")
|
raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found")
|
||||||
adapter = await _get_skill_display_adapter(skill_name)
|
|
||||||
if adapter and isinstance(adapter.get("sample"), dict):
|
|
||||||
return SkillDisplaySampleResponse(skill_name=skill_name, sample=adapter["sample"])
|
|
||||||
sample = await _generate_skill_display_sample(skill, config)
|
sample = await _generate_skill_display_sample(skill, config)
|
||||||
await _upsert_skill_display_adapter(skill_name, sample=sample)
|
await _upsert_skill_display_adapter(skill_name, sample=sample)
|
||||||
return SkillDisplaySampleResponse(skill_name=skill_name, sample=sample)
|
return SkillDisplaySampleResponse(skill_name=skill_name, sample=sample)
|
||||||
|
|||||||
Reference in new issue
Block a user