support self-check

This commit is contained in:
锦恢 2025-07-03 19:06:55 +08:00
parent 25f74b8f1e
commit 13673d798b
5 changed files with 211 additions and 76 deletions

View File

@ -5,21 +5,36 @@
<span class="item-status" :class="props.dataView.status">{{ props.dataView.status }}</span> <span class="item-status" :class="props.dataView.status">{{ props.dataView.status }}</span>
</div> </div>
<div class="item-desc">{{ props.dataView.tool.description }}</div> <div class="item-desc">{{ props.dataView.tool.description }}</div>
<div class="item-schema">
<span class="item-label">Input Schema:</span> <div v-if="props.dataView.function !== undefined" class="item-result">
<pre class="item-json">{{ formatJson(props.dataView.tool.inputSchema) }}</pre> <span class="item-label">Function</span>
</div> <pre class="item-json">{{ props.dataView.function.name }}</pre>
<div v-if="props.dataView.result !== undefined" class="item-result"> <span class="item-label">Arguments</span>
<span class="item-label">Result:</span> <pre class="item-json">{{ formatJson(props.dataView.function.arguments) }}</pre>
<pre class="item-json">{{ formatJson(props.dataView.result) }}</pre>
</div> </div>
<div v-if="props.dataView.result !== undefined" class="item-result">
<span class="item-label">Result</span>
<template v-if="Array.isArray(props.dataView.result)">
<div
v-for="(item, idx) in props.dataView.result"
:key="idx"
class="result-block"
>
<pre class="item-json" v-if="typeof item === 'object' && item.text !== undefined">{{ item.text }}</pre>
<pre class="item-json" v-else>{{ formatJson(item) }}</pre>
</div>
</template>
<pre class="item-json" v-else-if="typeof props.dataView.result === 'string'">{{ props.dataView.result }}</pre>
<pre class="item-json" v-else>{{ formatJson(props.dataView.result) }}</pre>
</div>
</div>
<div v-else class="diagram-item-record">
<div class="item-header">
<span class="item-title">No Tool Selected</span>
</div>
<div class="item-desc">Please select a tool to view its details.</div>
</div> </div>
<div v-else class="diagram-item-record">
<div class="item-header">
<span class="item-title">No Tool Selected</span>
</div>
<div class="item-desc">Please select a tool to view its details.</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -48,8 +63,7 @@ function formatJson(obj: any) {
border-radius: 8px; border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.04); box-shadow: 0 1px 4px rgba(0,0,0,0.04);
font-size: 15px; font-size: 15px;
color: #222; max-width: 300px;
max-width: 420px;
word-break: break-all; word-break: break-all;
} }
@ -104,4 +118,10 @@ function formatJson(obj: any) {
.item-result { .item-result {
margin-top: 6px; margin-top: 6px;
} }
.result-block {
margin-bottom: 6px;
border-radius: .5em;
background-color: rgba(245, 108, 108, 0.3);
}
</style> </style>

View File

@ -1,11 +1,12 @@
import type { ElkNode } from 'elkjs/lib/elk-api'; import type { ElkNode } from 'elkjs/lib/elk-api';
import { TaskLoop } from '../../chat/core/task-loop'; import { MessageState, TaskLoop } from '../../chat/core/task-loop';
import type { Reactive } from 'vue'; import type { Reactive } from 'vue';
import type { ChatStorage } from '../../chat/chat-box/chat'; import type { ChatStorage } from '../../chat/chat-box/chat';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import type { ToolItem } from '@/hook/type'; import type { ToolItem } from '@/hook/type';
import I18n from '@/i18n'; import I18n from '@/i18n';
import type { ChatCompletionChunk } from 'openai/resources/index.mjs';
const { t } = I18n.global; const { t } = I18n.global;
@ -13,11 +14,14 @@ export interface Edge {
id: string; id: string;
sources: string[]; sources: string[];
targets: string[]; targets: string[];
section?: any; // { startPoint: { x, y }, endPoint: { x, sections?: any; // { startPoint: { x, y }, endPoint: { x,
} }
export type Node = ElkNode & { export type Node = ElkNode & {
[key: string]: any; [key: string]: any;
width: number;
height: number;
id: string;
}; };
export interface DiagramState { export interface DiagramState {
@ -36,7 +40,8 @@ export interface CanConnectResult {
export interface NodeDataView { export interface NodeDataView {
tool: ToolItem; tool: ToolItem;
status: 'default' | 'running' | 'waiting' | 'success' | 'error'; status: 'default' | 'running' | 'waiting' | 'success' | 'error';
result: any; function?: ChatCompletionChunk.Choice.Delta.ToolCall.Function;
result?: any;
} }
export interface DiagramContext { export interface DiagramContext {
@ -191,7 +196,7 @@ export async function makeNodeTest(
let aiMockJson: any = undefined; let aiMockJson: any = undefined;
loop.registerOnToolCall(toolCall => { loop.registerOnToolCall(toolCall => {
console.log(toolCall); dataView.function = toolCall.function;
if (toolCall.function?.name === dataView.tool?.name) { if (toolCall.function?.name === dataView.tool?.name) {
try { try {
@ -202,19 +207,34 @@ export async function makeNodeTest(
dataView.status = 'error'; dataView.status = 'error';
dataView.result = t('ai-gen-error-json'); dataView.result = t('ai-gen-error-json');
context.render(); context.render();
loop.abort();
} }
} else { } else {
// ElMessage.error('AI 调用了未知的工具'); // ElMessage.error('AI 调用了未知的工具');
dataView.status = 'error'; dataView.status = 'error';
dataView.result = t('ai-invoke-unknown-tool') + ' ' + toolCall.function?.name; dataView.result = t('ai-invoke-unknown-tool') + ' ' + toolCall.function?.name;
context.render(); context.render();
loop.abort();
} }
loop.abort();
return toolCall; return toolCall;
}); });
loop.registerOnToolCalled(toolCalled => {
if (toolCalled.state === MessageState.Success) {
dataView.status = 'success';
dataView.result = toolCalled.content;
} else {
dataView.status = 'error';
dataView.result = toolCalled.content;
}
loop.abort();
return toolCalled;
})
loop.registerOnError(error => { loop.registerOnError(error => {
ElMessage.error(error + ''); dataView.status = 'error';
dataView.result = error;
context.render();
}); });
await loop.start(chatStorage, usePrompt); await loop.start(chatStorage, usePrompt);

View File

@ -11,7 +11,9 @@
:style="getNodePopupStyle(node)" :style="getNodePopupStyle(node)"
class="node-popup" class="node-popup"
> >
<DiagramItemRecord :data-view="state.dataView.get(node.id)"/> <el-scrollbar height="100%" width="100%">
<DiagramItemRecord :data-view="state.dataView.get(node.id)"/>
</el-scrollbar>
</div> </div>
</transition> </transition>
</template> </template>
@ -29,16 +31,25 @@ import { ElMessage } from 'element-plus';
import DiagramItemRecord from './diagram-item-record.vue'; import DiagramItemRecord from './diagram-item-record.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { ToolStorage } from '../tools';
import { tabs } from '../../panel';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const svgContainer = ref<HTMLDivElement | null>(null); const svgContainer = ref<HTMLDivElement | null>(null);
let prevNodes: any[] = []; let prevNodes: any[] = [];
let prevEdges: any[] = []; let prevEdges: any[] = [];
const state = reactive({ const state = reactive({
nodes: [] as any[], nodes: [] as Node[],
edges: [] as any[], edges: [] as Edge[],
selectedNodeId: null as string | null, selectedNodeId: null as string | null,
draggingNodeId: null as string | null, draggingNodeId: null as string | null,
hoverNodeId: null as string | null, hoverNodeId: null as string | null,
@ -46,6 +57,29 @@ const state = reactive({
dataView: new Map<string, NodeDataView> dataView: new Map<string, NodeDataView>
}); });
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ToolStorage;
const autoDetectDiagram = tabStorage.autoDetectDiagram;
if (autoDetectDiagram) {
// tabStorage.autoDetectDiagram dataView state
autoDetectDiagram.views?.forEach(item => {
state.dataView.set(item.tool.name, {
tool: item.tool,
status: item.status || 'waiting',
result: item.result || null
});
});
} else {
tabStorage.autoDetectDiagram = {
edges: [],
views: []
};
}
console.log(tabStorage.autoDetectDiagram!.views);
console.log(state.dataView);
let cancelHoverHandler: NodeJS.Timeout | undefined = undefined; let cancelHoverHandler: NodeJS.Timeout | undefined = undefined;
@ -88,17 +122,26 @@ const recomputeLayout = async () => {
children: state.nodes, children: state.nodes,
edges: state.edges edges: state.edges
}; };
const layout = await elk.layout(elkGraph) as Node; const layout = await elk.layout(elkGraph) as unknown as Node;
state.nodes.forEach((n, i) => { state.nodes.forEach((n, i) => {
const ln = layout.children?.find(c => c.id === n.id); const ln = layout.children?.find(c => c.id === n.id);
if (ln) { if (ln) {
n.x = ln.x; n.x = ln.x;
n.y = ln.y; n.y = ln.y;
n.width = ln.width; n.width = ln.width || 200; //
n.height = ln.height; n.height = ln.height || 64; //
} }
}); });
state.edges = layout.edges || []; state.edges = layout.edges || [];
// tabStorage
tabStorage.autoDetectDiagram!.edges = state.edges.map(edge => ({
id: edge.id,
sources: edge.sources || [],
targets: edge.targets || []
}));
return layout; return layout;
}; };
@ -109,14 +152,28 @@ const drawDiagram = async () => {
const nodes = [] as Node[]; const nodes = [] as Node[];
const edges = [] as Edge[]; const edges = [] as Edge[];
for (let i = 0; i < tools.length - 1; ++i) { // edges
const prev = tools[i]; const reservedEdges = autoDetectDiagram?.edges;
const next = tools[i + 1]; if (reservedEdges) {
edges.push({ for (const edge of reservedEdges) {
id: prev.name + '-' + next.name, if (edge.sources && edge.targets && edge.sources.length > 0 && edge.targets.length > 0) {
sources: [prev.name], edges.push({
targets: [next.name] id: edge.id,
}) sources: edge.sources || [],
targets: edge.targets || [],
});
}
}
} else {
for (let i = 0; i < tools.length - 1; ++i) {
const prev = tools[i];
const next = tools[i + 1];
edges.push({
id: prev.name + '-' + next.name,
sources: [prev.name],
targets: [next.name]
})
}
} }
for (const tool of tools) { for (const tool of tools) {
@ -127,11 +184,13 @@ const drawDiagram = async () => {
labels: [{ text: tool.name || 'Tool' }] labels: [{ text: tool.name || 'Tool' }]
}); });
state.dataView.set(tool.name, { if (!state.dataView.has(tool.name)) {
tool, // dataView
status: 'waiting', state.dataView.set(tool.name, {
result: null tool,
}); status: 'waiting'
});
}
} }
state.edges = edges; state.edges = edges;
@ -291,10 +350,8 @@ function renderSvg() {
if (state.selectedNodeId) { if (state.selectedNodeId) {
const { canConnect, reason } = invalidConnectionDetector(state, d); const { canConnect, reason } = invalidConnectionDetector(state, d);
console.log(reason); console.log(reason);
if (reason) { if (reason) {
ElMessage.warning(reason); ElMessage.warning(reason);
} }
@ -340,8 +397,8 @@ function renderSvg() {
}); });
nodeGroupEnter.append('rect') nodeGroupEnter.append('rect')
.attr('width', d => d.width) .attr('width', (d: any) => d.width)
.attr('height', d => d.height) .attr('height', (d: any) => d.height)
.attr('rx', 16) .attr('rx', 16)
.attr('fill', 'var(--main-light-color-20)') .attr('fill', 'var(--main-light-color-20)')
.attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)') .attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)')
@ -454,7 +511,6 @@ function renderSvg() {
nodeGroup.select('rect') nodeGroup.select('rect')
.transition() .transition()
.duration(400) .duration(400)
.attr('stroke-width', d => state.selectedNodeId === d.id ? 2 : 1)
.attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)'); .attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)');
// //
@ -530,12 +586,33 @@ onMounted(() => {
function getNodePopupStyle(node: any): any { function getNodePopupStyle(node: any): any {
// svg // svg
// offsetXnode.xnode.y // offsetXnode.xnode.y
const left = (node.x || 0) + (node.width || 160) + 120; // const marginX = 50;
const top = (node.y || 0) + 30; const marginY = 80;
const popupWidth = 300;
const popupHeight = 500;
let left = (node.x || 0) + (node.width || 160) + 100;
let top = (node.y || 0) + 30;
//
const container = svgContainer.value;
let containerWidth = 1200, containerHeight = 800; //
if (container) {
const rect = container.getBoundingClientRect();
containerWidth = rect.width;
containerHeight = rect.height;
}
// left top
left = Math.max(marginX, Math.min(left, containerWidth - popupWidth - marginX));
top = Math.max(marginY, Math.min(top, containerHeight - popupHeight - marginY));
return { return {
position: 'absolute', position: 'absolute',
left: `${left}px`, left: `${left}px`,
top: `${top}px`, top: `${top}px`,
width: `${popupWidth}px`,
height: `${popupHeight}px`
}; };
} }
</script> </script>
@ -556,7 +633,6 @@ function getNodePopupStyle(node: any): any {
position: absolute; position: absolute;
background: var(--background); background: var(--background);
border: 1px solid var(--main-color); border: 1px solid var(--main-color);
width: 240px;
border-radius: 8px; border-radius: 8px;
padding: 8px 12px; padding: 8px 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);

View File

@ -17,12 +17,10 @@
placeholder="请输入 prompt" /> placeholder="请输入 prompt" />
<div style="display: flex; align-items: center; margin-bottom: 8px;"> <div style="display: flex; align-items: center; margin-bottom: 8px;">
<el-switch v-model="enableXmlWrapper" style="margin-right: 8px;" /> <el-switch v-model="enableXmlWrapper" style="margin-right: 8px;" />
<span <span :style="{
:style="{ opacity: enableXmlWrapper ? 1 : 0.7,
opacity: enableXmlWrapper? 1 : 0.7, color: enableXmlWrapper ? 'var(--main-color)' : undefined
color: enableXmlWrapper ? 'var(--main-color)' : undefined }">XML</span>
}"
>XML</span>
</div> </div>
<div style="text-align: right;"> <div style="text-align: right;">
<el-button size="small" @click="testFormVisible = false">{{ t("cancel") }}</el-button> <el-button size="small" @click="testFormVisible = false">{{ t("cancel") }}</el-button>
@ -34,7 +32,7 @@
</div> </div>
</template> </template>
<el-scrollbar height="80vh"> <el-scrollbar height="80vh">
<Diagram /> <Diagram :tab-id="props.tabId" />
</el-scrollbar> </el-scrollbar>
<transition name="main-fade" mode="out-in"> <transition name="main-fade" mode="out-in">
<div class="caption" v-show="showCaption"> <div class="caption" v-show="showCaption">
@ -49,10 +47,10 @@ import { nextTick, provide, ref } from 'vue';
import Diagram from './diagram.vue'; import Diagram from './diagram.vue';
import { makeNodeTest, topoSortParallel, type DiagramContext, type DiagramState } from './diagram'; import { makeNodeTest, topoSortParallel, type DiagramContext, type DiagramState } from './diagram';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { tabs } from '../../panel';
import type { ToolStorage } from '../tools';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { ToolStorage } from '../tools';
import { tabs } from '../../panel';
const showDiagram = ref(true); const showDiagram = ref(true);
const { t } = useI18n(); const { t } = useI18n();
@ -67,14 +65,6 @@ const props = defineProps({
} }
}); });
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ToolStorage;
if (!tabStorage.formData) {
tabStorage.formData = {};
}
function setCaption(text: string) { function setCaption(text: string) {
caption.value = text; caption.value = text;
if (caption.value) { if (caption.value) {
@ -89,14 +79,27 @@ function setCaption(text: string) {
} }
const context: DiagramContext = { const context: DiagramContext = {
reset: () => {}, reset: () => { },
render: () => {}, render: () => { },
state: undefined, state: undefined,
setCaption setCaption
}; };
provide('context', context); provide('context', context);
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ToolStorage;
const autoDetectDiagram = tabStorage.autoDetectDiagram;
if (autoDetectDiagram) {
// ...
} else {
tabStorage.autoDetectDiagram = {
edges: [],
views: []
};
}
// //
const testFormVisible = ref(false); const testFormVisible = ref(false);
const enableXmlWrapper = ref(false); const enableXmlWrapper = ref(false);
@ -106,21 +109,31 @@ async function onTestConfirm() {
testFormVisible.value = false; testFormVisible.value = false;
// enableXmlWrapper.value testPrompt.value // enableXmlWrapper.value testPrompt.value
const state = context.state; const state = context.state;
tabStorage.autoDetectDiagram!.views = [];
if (state) { if (state) {
const dispatches = topoSortParallel(state); const dispatches = topoSortParallel(state);
for (const nodeIds of dispatches) { for (const nodeIds of dispatches) {
await Promise.all( for (const id of nodeIds) {
nodeIds.map(id => { const view = state.dataView.get(id);
const node = state.dataView.get(id); if (view) {
if (node) { await makeNodeTest(view, enableXmlWrapper.value, testPrompt.value, context)
return makeNodeTest(node, enableXmlWrapper.value, testPrompt.value, context) tabStorage.autoDetectDiagram!.views!.push({
} tool: view.tool,
}) status: view.status,
) function: view.function,
result: view.result
});
}
}
} }
} else { } else {
ElMessage.error('error'); ElMessage.error('error');
} }
} }
</script> </script>

View File

@ -1,8 +1,14 @@
import type { ToolCallResponse } from '@/hook/type'; import type { ToolCallResponse } from '@/hook/type';
import type { Edge, Node, NodeDataView } from './auto-detector/diagram';
export interface ToolStorage { export interface ToolStorage {
activeNames: any[]; activeNames: any[];
currentToolName: string; currentToolName: string;
lastToolCallResponse?: ToolCallResponse | string; lastToolCallResponse?: ToolCallResponse | string;
formData: Record<string, any>; formData: Record<string, any>;
autoDetectDiagram?: {
edges?: Edge[];
views?: NodeDataView[];
[key: string]: any;
}
} }