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>
</div>
<div class="item-desc">{{ props.dataView.tool.description }}</div>
<div class="item-schema">
<span class="item-label">Input Schema:</span>
<pre class="item-json">{{ formatJson(props.dataView.tool.inputSchema) }}</pre>
</div>
<div v-if="props.dataView.result !== undefined" class="item-result">
<span class="item-label">Result:</span>
<pre class="item-json">{{ formatJson(props.dataView.result) }}</pre>
<div v-if="props.dataView.function !== undefined" class="item-result">
<span class="item-label">Function</span>
<pre class="item-json">{{ props.dataView.function.name }}</pre>
<span class="item-label">Arguments</span>
<pre class="item-json">{{ formatJson(props.dataView.function.arguments) }}</pre>
</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 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>
<script setup lang="ts">
@ -48,8 +63,7 @@ function formatJson(obj: any) {
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
font-size: 15px;
color: #222;
max-width: 420px;
max-width: 300px;
word-break: break-all;
}
@ -104,4 +118,10 @@ function formatJson(obj: any) {
.item-result {
margin-top: 6px;
}
.result-block {
margin-bottom: 6px;
border-radius: .5em;
background-color: rgba(245, 108, 108, 0.3);
}
</style>

View File

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

View File

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

View File

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

View File

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