技能展示样本运行器 + 参考文献来源过滤调整
- 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 ?? []),
|
||||
[thread.values.messages],
|
||||
);
|
||||
const latestAssistant = useMemo(
|
||||
() => findLatestAssistantMessageAfter(thread.values.messages ?? [], latestHumanIndex),
|
||||
[latestHumanIndex, thread.values.messages],
|
||||
);
|
||||
const displaySources = useMemo(() => {
|
||||
if (!settings.display.ragModeEnabled) {
|
||||
return [];
|
||||
}
|
||||
const messages = thread.values.messages ?? [];
|
||||
if (!latestAssistant?.id) {
|
||||
return [];
|
||||
}
|
||||
return collectRuntimeDisplaySources(messages, skillDisplayMap, {
|
||||
afterMessageId: latestHumanIndex >= 0 ? messages[latestHumanIndex]?.id : undefined,
|
||||
beforeMessageId: latestAssistant.id,
|
||||
includeBeforeMessage: true,
|
||||
includeModes: ["citation"],
|
||||
});
|
||||
}, [settings.display.ragModeEnabled, skillDisplayMap, latestAssistant?.id, latestHumanIndex, thread.values.messages]);
|
||||
}, [settings.display.ragModeEnabled, skillDisplayMap, latestHumanIndex, thread.values.messages]);
|
||||
const referenceSources = useMemo(
|
||||
() => displaySources.filter((source) => source.mode === "citation"),
|
||||
[displaySources],
|
||||
@@ -93,7 +84,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
||||
if (hasCitationReferences) {
|
||||
setReferencesOpen(true);
|
||||
}
|
||||
}, [hasCitationReferences, latestAssistant?.id]);
|
||||
}, [hasCitationReferences]);
|
||||
useEffect(() => {
|
||||
const handleShowReference = () => {
|
||||
setReferencesOpen(true);
|
||||
@@ -333,17 +324,4 @@ function findLatestHumanMessageIndex(messages: Message[]) {
|
||||
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 };
|
||||
@@ -34,62 +34,6 @@ export const emptyDisplayConfig = (): SkillDisplayConfig => ({
|
||||
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[]> = {
|
||||
titleField: ["title", "m_title", "name", "subject", "headline"],
|
||||
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"],
|
||||
scoreField: ["relevance_score", "rerank_score", "score", "similarity"],
|
||||
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 {
|
||||
if (!path) return value;
|
||||
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;
|
||||
}, 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")) {
|
||||
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.timeField = inferred.timeField || "publish_time";
|
||||
inferred.scoreField = inferred.scoreField || "relevance_score";
|
||||
inferred.idField = inferred.idField || "recUuid";
|
||||
inferred.linkStrategy = "id";
|
||||
inferred.idField = pickField(sample, ["recUuid", "rec_uuid", "uuid", "id"]) || inferred.idField;
|
||||
inferred.linkStrategy = inferred.urlField ? "auto" : "id";
|
||||
inferred.linkTemplate = "https://xxx.xxx.xxx?recuuid={recUuid}";
|
||||
}
|
||||
inferred.tableColumns = buildDefaultTableColumns(sample, inferred);
|
||||
|
||||
inferred.tableColumns = buildDefaultTableColumns(sample, inferred);
|
||||
return inferred;
|
||||
}
|
||||
|
||||
@@ -192,14 +141,18 @@ function stringify(value: unknown, fallback = ""): 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 {
|
||||
const url = config.urlField ? stringify(item[config.urlField]) : "";
|
||||
const id = config.idField ? stringify(item[config.idField]) : "";
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -217,7 +170,10 @@ export function normalizeDisplaySources(payload: unknown, config: SkillDisplayCo
|
||||
content: config.contentField ? stringify(item[config.contentField]) : undefined,
|
||||
source: config.sourceField ? stringify(item[config.sourceField]) : 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),
|
||||
raw: item,
|
||||
}));
|
||||
|
||||
@@ -17,6 +17,12 @@ export type RuntimeDisplaySource = DisplaySourceItem & {
|
||||
|
||||
export type SkillDisplayMap = Map<string, SkillDisplayConfig>;
|
||||
|
||||
type RuntimeToolCall = {
|
||||
id?: string;
|
||||
name: string;
|
||||
args?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export function buildSkillDisplayMap(skills: Skill[]): SkillDisplayMap {
|
||||
const map = new Map<string, SkillDisplayConfig>();
|
||||
for (const skill of skills) {
|
||||
@@ -163,13 +169,13 @@ export function collectRuntimeDisplaySources(
|
||||
if (!message || message.type !== "ai") {
|
||||
continue;
|
||||
}
|
||||
for (const toolCall of message.tool_calls ?? []) {
|
||||
for (const toolCall of extractRuntimeToolCalls(message)) {
|
||||
const toolMatch = findDisplayConfigForTool(
|
||||
toolCall.name,
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
@@ -192,7 +198,7 @@ export function collectRuntimeDisplaySources(
|
||||
: shouldUseDefaultCitationMode(
|
||||
toolCall.name,
|
||||
result,
|
||||
toolCall.args as Record<string, unknown> | undefined,
|
||||
toolCall.args,
|
||||
)
|
||||
? normalizeDefaultDisplaySources(result)
|
||||
: [];
|
||||
@@ -214,6 +220,80 @@ export function collectRuntimeDisplaySources(
|
||||
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(
|
||||
toolName: string,
|
||||
payload: unknown,
|
||||
@@ -445,16 +525,18 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function stringifyMaybe(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value : undefined;
|
||||
}
|
||||
|
||||
function findToolResultPayload(
|
||||
toolCallId: string | undefined,
|
||||
toolCall: RuntimeToolCall,
|
||||
messages: Message[],
|
||||
toolCallMessageIndex = -1,
|
||||
): unknown {
|
||||
if (!toolCallId) {
|
||||
return undefined;
|
||||
}
|
||||
const toolMessage = messages.find(
|
||||
(message) => message.type === "tool" && message.tool_call_id === toolCallId,
|
||||
);
|
||||
const toolMessage =
|
||||
findToolMessageById(toolCall.id, messages) ??
|
||||
findToolMessageByName(toolCall, messages, toolCallMessageIndex);
|
||||
if (!toolMessage) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -473,6 +555,73 @@ function findToolResultPayload(
|
||||
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 {
|
||||
if (isRecord(content)) {
|
||||
return unwrapToolPayloadObject(content);
|
||||
|
||||
@@ -2,7 +2,10 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -412,6 +415,102 @@ def _extract_json_from_llm_output(raw: str) -> dict[str, Any] | 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]:
|
||||
"""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
|
||||
be parsed as a JSON object.
|
||||
"""
|
||||
script_sample = await _run_skill_python_sample(skill)
|
||||
if script_sample:
|
||||
return script_sample
|
||||
|
||||
try:
|
||||
skill_content = skill.skill_file.read_text(encoding="utf-8")
|
||||
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)
|
||||
except Exception:
|
||||
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]:
|
||||
@@ -1883,9 +2002,6 @@ async def get_skill_display_sample(
|
||||
skill = next((s for s in skills if s.name == skill_name), None)
|
||||
if skill is None:
|
||||
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)
|
||||
await _upsert_skill_display_adapter(skill_name, sample=sample)
|
||||
return SkillDisplaySampleResponse(skill_name=skill_name, sample=sample)
|
||||
|
||||
Reference in new issue
Block a user