support xml
This commit is contained in:
parent
0df86cfc28
commit
d4ac089a9a
42
package-lock.json
generated
42
package-lock.json
generated
@ -2698,6 +2698,16 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/xml2js": {
|
||||||
|
"version": "0.4.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz",
|
||||||
|
"integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "5.2.4",
|
"version": "5.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||||
@ -9401,6 +9411,12 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sax": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/schema-utils": {
|
"node_modules/schema-utils": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||||
@ -11392,6 +11408,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xml2js": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sax": ">=0.6.0",
|
||||||
|
"xmlbuilder": "~11.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlbuilder": {
|
||||||
|
"version": "11.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||||
|
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
@ -11579,7 +11617,8 @@
|
|||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^11.1.0",
|
"vue-i18n": "^11.1.0",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0",
|
||||||
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.27.1",
|
"@babel/core": "^7.27.1",
|
||||||
@ -11593,6 +11632,7 @@
|
|||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
|
"@types/xml2js": "^0.4.14",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"@vue/babel-plugin-jsx": "^1.4.0",
|
"@vue/babel-plugin-jsx": "^1.4.0",
|
||||||
"@vue/devtools-core": "^7.7.6",
|
"@vue/devtools-core": "^7.7.6",
|
||||||
|
@ -33,7 +33,8 @@
|
|||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^11.1.0",
|
"vue-i18n": "^11.1.0",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0",
|
||||||
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.27.1",
|
"@babel/core": "^7.27.1",
|
||||||
@ -47,6 +48,7 @@
|
|||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
|
"@types/xml2js": "^0.4.14",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"@vue/babel-plugin-jsx": "^1.4.0",
|
"@vue/babel-plugin-jsx": "^1.4.0",
|
||||||
"@vue/devtools-core": "^7.7.6",
|
"@vue/devtools-core": "^7.7.6",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ToolCallContent, ToolItem } from "@/hook/type";
|
import type { InputSchema, ToolCallContent, ToolItem } from "@/hook/type";
|
||||||
import { type Ref, ref } from "vue";
|
import { type Ref, ref } from "vue";
|
||||||
|
|
||||||
import type { OpenAI } from 'openai';
|
import type { OpenAI } from 'openai';
|
||||||
@ -16,6 +16,7 @@ export enum MessageState {
|
|||||||
Success = 'success',
|
Success = 'success',
|
||||||
ParseJsonError = 'parse json error',
|
ParseJsonError = 'parse json error',
|
||||||
NoToolFunction = 'no tool function',
|
NoToolFunction = 'no tool function',
|
||||||
|
InvalidXml = 'invalid xml',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExtraInfo {
|
export interface IExtraInfo {
|
||||||
@ -23,6 +24,7 @@ export interface IExtraInfo {
|
|||||||
state: MessageState,
|
state: MessageState,
|
||||||
serverName: string,
|
serverName: string,
|
||||||
usage?: ChatCompletionChunk['usage'];
|
usage?: ChatCompletionChunk['usage'];
|
||||||
|
enableXmlWrapper: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ export interface EnableToolItem {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
inputSchema: any;
|
inputSchema: InputSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatSetting {
|
export interface ChatSetting {
|
||||||
@ -108,7 +110,7 @@ export interface IToolRenderMessage {
|
|||||||
|
|
||||||
export type IRenderMessage = ICommonRenderMessage | IToolRenderMessage;
|
export type IRenderMessage = ICommonRenderMessage | IToolRenderMessage;
|
||||||
|
|
||||||
export function getToolSchema(enableTools: EnableToolItem[]) {
|
export function getToolSchema(enableTools: EnableToolItem[]): any[] {
|
||||||
const toolsSchema = [];
|
const toolsSchema = [];
|
||||||
for (let i = 0; i < enableTools.length; i++) {
|
for (let i = 0; i < enableTools.length; i++) {
|
||||||
const enableTool = enableTools[i];
|
const enableTool = enableTools[i];
|
||||||
|
@ -16,11 +16,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<span>{{ t('tool-manage') }}</span>
|
<span>{{ t('tool-manage') }}</span>
|
||||||
<el-tooltip :content="t('enable-xml-wrapper')" placement="top" effect="light">
|
<el-tooltip :content="t('enable-xml-wrapper')" placement="top" effect="light">
|
||||||
<span class="xml-tag" :class="{
|
<span class="xml-tag" :class="{
|
||||||
'active': tabStorage.settings.enableXmlWrapper
|
'active': tabStorage.settings.enableXmlWrapper
|
||||||
}"
|
}" @click="tabStorage.settings.enableXmlWrapper = !tabStorage.settings.enableXmlWrapper">xml</span>
|
||||||
@click="tabStorage.settings.enableXmlWrapper = !tabStorage.settings.enableXmlWrapper"
|
|
||||||
>xml</span>
|
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -57,7 +55,7 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import { type ChatStorage, type EnableToolItem, getToolSchema } from '../chat';
|
import { type ChatStorage, type EnableToolItem, getToolSchema } from '../chat';
|
||||||
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
|
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
|
||||||
import { mcpClientAdapter } from '@/views/connect/core';
|
import { mcpClientAdapter } from '@/views/connect/core';
|
||||||
import { toolSchemaToPromptDescription } from '../../core/prompt';
|
import { toolSchemaToPromptDescription } from '../../core/xml-wrapper';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@ -66,37 +64,34 @@ const tabStorage = inject('tabStorage') as ChatStorage;
|
|||||||
const showToolsDialog = ref(false);
|
const showToolsDialog = ref(false);
|
||||||
|
|
||||||
const availableToolsNum = computed(() => {
|
const availableToolsNum = computed(() => {
|
||||||
return tabStorage.settings.enableTools.filter(tool => tool.enabled).length;
|
return tabStorage.settings.enableTools.filter(tool => tool.enabled).length;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 修改 toggleTools 方法
|
// 修改 toggleTools 方法
|
||||||
const toggleTools = () => {
|
const toggleTools = () => {
|
||||||
showToolsDialog.value = true;
|
showToolsDialog.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeToolsSchemaHTML = computed(() => {
|
|
||||||
const toolsSchema = getToolSchema(tabStorage.settings.enableTools);
|
|
||||||
const jsonString = JSON.stringify(toolsSchema, null, 2);
|
|
||||||
|
|
||||||
return markdownToHtml(
|
const activeToolsSchemaHTML = computed(() => {
|
||||||
"```json\n" + jsonString + "\n```"
|
const toolsSchema = getToolSchema(tabStorage.settings.enableTools);
|
||||||
);
|
const jsonString = JSON.stringify(toolsSchema, null, 2);
|
||||||
|
|
||||||
|
return markdownToHtml(
|
||||||
|
"```json\n" + jsonString + "\n```"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeToolsXmlPrompt = computed(() => {
|
const activeToolsXmlPrompt = computed(() => {
|
||||||
if (tabStorage.settings.enableXmlWrapper) {
|
const prompt = toolSchemaToPromptDescription(tabStorage.settings.enableTools);
|
||||||
const prompt = toolSchemaToPromptDescription(tabStorage.settings.enableTools);
|
return markdownToHtml(
|
||||||
return markdownToHtml(
|
"```markdown\n" + prompt + "\n```"
|
||||||
"```markdown\n" + prompt + "\n```"
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 新增方法 - 激活所有工具
|
// 新增方法 - 激活所有工具
|
||||||
const enableAllTools = () => {
|
const enableAllTools = () => {
|
||||||
tabStorage.settings.enableTools.forEach(tool => {
|
tabStorage.settings.enableTools.forEach(tool => {
|
||||||
tool.enabled = true;
|
tool.enabled = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -151,7 +146,6 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.xml-tag {
|
.xml-tag {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
border-radius: .5em;
|
border-radius: .5em;
|
||||||
@ -169,5 +163,4 @@ onMounted(async () => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: var(--animation-3s);
|
transition: var(--animation-3s);
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
@ -2,7 +2,14 @@ import type { ToolCallContent, ToolCallResponse } from "@/hook/type";
|
|||||||
import { MessageState, type ToolCall } from "../chat-box/chat";
|
import { MessageState, type ToolCall } from "../chat-box/chat";
|
||||||
import { mcpClientAdapter } from "@/views/connect/core";
|
import { mcpClientAdapter } from "@/views/connect/core";
|
||||||
import type { BasicLlmDescription } from "@/views/setting/llm";
|
import type { BasicLlmDescription } from "@/views/setting/llm";
|
||||||
import { redLog } from "@/views/setting/util";
|
import type OpenAI from "openai";
|
||||||
|
|
||||||
|
export interface TaskLoopChatOption {
|
||||||
|
id?: string
|
||||||
|
proxyServer?: string
|
||||||
|
enableXmlWrapper?: boolean
|
||||||
|
}
|
||||||
|
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & TaskLoopChatOption;
|
||||||
|
|
||||||
export interface ToolCallResult {
|
export interface ToolCallResult {
|
||||||
state: MessageState;
|
state: MessageState;
|
||||||
@ -60,7 +67,7 @@ function deserializeToolCallResponse(toolArgs: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToolResponse(toolResponse: ToolCallResponse) {
|
export function handleToolResponse(toolResponse: ToolCallResponse) {
|
||||||
if (typeof toolResponse === 'string') {
|
if (typeof toolResponse === 'string') {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -98,7 +105,14 @@ function parseErrorObject(error: any): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function grokIndexAdapter(toolCall: ToolCall, callId2Index: Map<string, number>): IToolCallIndex {
|
|
||||||
|
/**
|
||||||
|
* @description 将工具调用的ID映射为索引
|
||||||
|
* @param toolCall 工具调用对象
|
||||||
|
* @param callId2Index ID到索引的映射表
|
||||||
|
* @returns 映射后的索引值
|
||||||
|
*/
|
||||||
|
export function idAsIndexAdapter(toolCall: ToolCall, callId2Index: Map<string, number>): IToolCallIndex {
|
||||||
// grok 采用 id 作为 index,需要将 id 映射到 zero-based 的 index
|
// grok 采用 id 作为 index,需要将 id 映射到 zero-based 的 index
|
||||||
if (!toolCall.id) {
|
if (!toolCall.id) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -109,24 +123,42 @@ function grokIndexAdapter(toolCall: ToolCall, callId2Index: Map<string, number>)
|
|||||||
return callId2Index.get(toolCall.id)!;
|
return callId2Index.get(toolCall.id)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
function geminiIndexAdapter(toolCall: ToolCall): IToolCallIndex {
|
|
||||||
|
/**
|
||||||
|
* @description 单次调用的索引适配器(暂未实现)
|
||||||
|
* @param toolCall 工具调用对象
|
||||||
|
* @returns 固定返回0
|
||||||
|
*/
|
||||||
|
export function singleCallIndexAdapter(toolCall: ToolCall): IToolCallIndex {
|
||||||
// TODO: 等待后续支持
|
// TODO: 等待后续支持
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultIndexAdapter(toolCall: ToolCall): IToolCallIndex {
|
/**
|
||||||
|
* @description
|
||||||
|
* @param toolCall
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function defaultIndexAdapter(toolCall: ToolCall): IToolCallIndex {
|
||||||
return toolCall.index || 0;
|
return toolCall.index || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getToolCallIndexAdapter(llm: BasicLlmDescription) {
|
export function getToolCallIndexAdapter(llm: BasicLlmDescription, chatData: ChatCompletionCreateParamsBase) {
|
||||||
|
|
||||||
|
// 如果是 xml 模式,那么 index adapter 必须是 idAsIndexAdapter
|
||||||
|
|
||||||
|
if (chatData.enableXmlWrapper) {
|
||||||
|
const callId2Index = new Map<string, number>();
|
||||||
|
return (toolCall: ToolCall) => idAsIndexAdapter(toolCall, callId2Index);
|
||||||
|
}
|
||||||
|
|
||||||
if (llm.userModel.startsWith('gemini')) {
|
if (llm.userModel.startsWith('gemini')) {
|
||||||
return geminiIndexAdapter;
|
return singleCallIndexAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (llm.userModel.startsWith('grok')) {
|
if (llm.userModel.startsWith('grok')) {
|
||||||
const callId2Index = new Map<string, number>();
|
const callId2Index = new Map<string, number>();
|
||||||
return (toolCall: ToolCall) => grokIndexAdapter(toolCall, callId2Index);
|
return (toolCall: ToolCall) => idAsIndexAdapter(toolCall, callId2Index);
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultIndexAdapter;
|
return defaultIndexAdapter;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { ref, type Ref } from "vue";
|
import { ref, type Ref } from "vue";
|
||||||
import { type ToolCall, type ChatStorage, getToolSchema, MessageState } from "../chat-box/chat";
|
import { type ToolCall, type ChatStorage, getToolSchema, MessageState, type ChatMessage } from "../chat-box/chat";
|
||||||
import { useMessageBridge, MessageBridge, createMessageBridge } from "@/api/message-bridge";
|
import { useMessageBridge, MessageBridge, createMessageBridge } from "@/api/message-bridge";
|
||||||
import type { OpenAI } from 'openai';
|
import type { OpenAI } from 'openai';
|
||||||
import { llmManager, llms, type BasicLlmDescription } from "@/views/setting/llm";
|
import { llmManager, llms, type BasicLlmDescription } from "@/views/setting/llm";
|
||||||
@ -13,6 +13,7 @@ import { mcpSetting } from "@/hook/mcp";
|
|||||||
import { mcpClientAdapter } from "@/views/connect/core";
|
import { mcpClientAdapter } from "@/views/connect/core";
|
||||||
import type { ToolItem } from "@/hook/type";
|
import type { ToolItem } from "@/hook/type";
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { getXmlWrapperPrompt, getToolCallFromXmlString, getXmlsFromString, handleXmlWrapperToolcall, toNormaliseToolcall, getXmlResultPrompt } from "./xml-wrapper";
|
||||||
|
|
||||||
export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
||||||
export interface TaskLoopChatOption {
|
export interface TaskLoopChatOption {
|
||||||
@ -90,14 +91,26 @@ export class TaskLoop {
|
|||||||
this.bridge = useMessageBridge();
|
this.bridge = useMessageBridge();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleChunkDeltaContent(chunk: ChatCompletionChunk) {
|
/**
|
||||||
|
* @description 处理 streaming 输出的每一个分块的 content 部分
|
||||||
|
* 值得一提的是,如果开启了 xml 指令包裹,那么 toocall 模块部分也由此处来完成
|
||||||
|
* @param chunk
|
||||||
|
* @param chatData
|
||||||
|
*/
|
||||||
|
private handleChunkDeltaContent(chunk: ChatCompletionChunk, chatData: ChatCompletionCreateParamsBase) {
|
||||||
const content = chunk.choices[0]?.delta?.content || '';
|
const content = chunk.choices[0]?.delta?.content || '';
|
||||||
if (content) {
|
if (content) {
|
||||||
this.streamingContent.value += content;
|
this.streamingContent.value += content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleChunkDeltaToolCalls(chunk: ChatCompletionChunk, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex) {
|
/**
|
||||||
|
* @description 处理 streaming 输出的每一个 chunk 的 tool_calls 部分
|
||||||
|
* @param chunk
|
||||||
|
* @param chatData
|
||||||
|
* @param toolcallIndexAdapter
|
||||||
|
*/
|
||||||
|
private handleChunkDeltaToolCalls(chunk: ChatCompletionChunk, chatData: ChatCompletionCreateParamsBase, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex) {
|
||||||
const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0];
|
const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0];
|
||||||
|
|
||||||
if (toolCall) {
|
if (toolCall) {
|
||||||
@ -154,12 +167,12 @@ export class TaskLoop {
|
|||||||
|
|
||||||
// 处理增量的 content 和 tool_calls
|
// 处理增量的 content 和 tool_calls
|
||||||
if (chatData.enableXmlWrapper) {
|
if (chatData.enableXmlWrapper) {
|
||||||
this.handleChunkDeltaContent(chunk);
|
this.handleChunkDeltaContent(chunk, chatData);
|
||||||
// no tool call in enableXmlWrapper
|
// no tool call in enableXmlWrapper
|
||||||
this.handleChunkUsage(chunk);
|
this.handleChunkUsage(chunk);
|
||||||
} else {
|
} else {
|
||||||
this.handleChunkDeltaContent(chunk);
|
this.handleChunkDeltaContent(chunk, chatData);
|
||||||
this.handleChunkDeltaToolCalls(chunk, toolcallIndexAdapter);
|
this.handleChunkDeltaToolCalls(chunk, chatData, toolcallIndexAdapter);
|
||||||
this.handleChunkUsage(chunk);
|
this.handleChunkUsage(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,24 +231,35 @@ export class TaskLoop {
|
|||||||
|
|
||||||
const model = this.getLlmConfig().userModel;
|
const model = this.getLlmConfig().userModel;
|
||||||
const temperature = tabStorage.settings.temperature;
|
const temperature = tabStorage.settings.temperature;
|
||||||
const tools = getToolSchema(tabStorage.settings.enableTools);
|
|
||||||
const parallelToolCalls = tabStorage.settings.parallelToolCalls;
|
const parallelToolCalls = tabStorage.settings.parallelToolCalls;
|
||||||
const proxyServer = mcpSetting.proxyServer || '';
|
const proxyServer = mcpSetting.proxyServer || '';
|
||||||
|
|
||||||
|
// 如果是 xml 模式,则 tools 为空
|
||||||
const enableXmlWrapper = tabStorage.settings.enableXmlWrapper;
|
const enableXmlWrapper = tabStorage.settings.enableXmlWrapper;
|
||||||
|
const tools = enableXmlWrapper ? []: getToolSchema(tabStorage.settings.enableTools);
|
||||||
|
|
||||||
const userMessages = [];
|
const userMessages = [];
|
||||||
|
|
||||||
// 尝试获取 system prompt,在 api 模式下,systemPrompt 就是目标提词
|
// 尝试获取 system prompt,在 api 模式下,systemPrompt 就是目标提词
|
||||||
// 但是在 UI 模式下,systemPrompt 只是一个 index,需要从后端数据库中获取真实 prompt
|
// 但是在 UI 模式下,systemPrompt 只是一个 index,需要从后端数据库中获取真实 prompt
|
||||||
if (tabStorage.settings.systemPrompt) {
|
|
||||||
const prompt = getSystemPrompt(tabStorage.settings.systemPrompt) || tabStorage.settings.systemPrompt;
|
|
||||||
|
|
||||||
userMessages.push({
|
let prompt = '';
|
||||||
role: 'system',
|
|
||||||
content: prompt
|
// 如果存在系统提示词,则从数据库中获取对应的数据
|
||||||
});
|
if (tabStorage.settings.systemPrompt) {
|
||||||
|
prompt += getSystemPrompt(tabStorage.settings.systemPrompt) || tabStorage.settings.systemPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是 xml 模式,则在开头注入 xml
|
||||||
|
if (enableXmlWrapper) {
|
||||||
|
prompt += getXmlWrapperPrompt(tabStorage.settings.enableTools, tabStorage);
|
||||||
|
}
|
||||||
|
|
||||||
|
userMessages.push({
|
||||||
|
role: 'system',
|
||||||
|
content: prompt
|
||||||
|
});
|
||||||
|
|
||||||
// 如果超出了 tabStorage.settings.contextLength, 则删除最早的消息
|
// 如果超出了 tabStorage.settings.contextLength, 则删除最早的消息
|
||||||
const loadMessages = tabStorage.messages.slice(- tabStorage.settings.contextLength);
|
const loadMessages = tabStorage.messages.slice(- tabStorage.settings.contextLength);
|
||||||
userMessages.push(...loadMessages);
|
userMessages.push(...loadMessages);
|
||||||
@ -253,7 +277,7 @@ export class TaskLoop {
|
|||||||
parallelToolCalls,
|
parallelToolCalls,
|
||||||
messages: userMessages,
|
messages: userMessages,
|
||||||
proxyServer,
|
proxyServer,
|
||||||
enableXmlWrapper
|
enableXmlWrapper,
|
||||||
} as ChatCompletionCreateParamsBase;
|
} as ChatCompletionCreateParamsBase;
|
||||||
|
|
||||||
return chatData;
|
return chatData;
|
||||||
@ -474,6 +498,7 @@ export class TaskLoop {
|
|||||||
// 等待连接完成
|
// 等待连接完成
|
||||||
await this.nodejsStatus.connectionFut;
|
await this.nodejsStatus.connectionFut;
|
||||||
}
|
}
|
||||||
|
const enableXmlWrapper = tabStorage.settings.enableXmlWrapper;
|
||||||
|
|
||||||
// 添加目前的消息
|
// 添加目前的消息
|
||||||
tabStorage.messages.push({
|
tabStorage.messages.push({
|
||||||
@ -482,7 +507,8 @@ export class TaskLoop {
|
|||||||
extraInfo: {
|
extraInfo: {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
state: MessageState.Success,
|
state: MessageState.Success,
|
||||||
serverName: this.getLlmConfig().id || 'unknown'
|
serverName: this.getLlmConfig().id || 'unknown',
|
||||||
|
enableXmlWrapper
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -511,7 +537,7 @@ export class TaskLoop {
|
|||||||
|
|
||||||
this.currentChatId = chatData.id!;
|
this.currentChatId = chatData.id!;
|
||||||
const llm = this.getLlmConfig();
|
const llm = this.getLlmConfig();
|
||||||
const toolcallIndexAdapter = getToolCallIndexAdapter(llm);
|
const toolcallIndexAdapter = getToolCallIndexAdapter(llm, chatData);
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
const doConverationResult = await this.doConversation(chatData, toolcallIndexAdapter);
|
const doConverationResult = await this.doConversation(chatData, toolcallIndexAdapter);
|
||||||
@ -526,7 +552,8 @@ export class TaskLoop {
|
|||||||
extraInfo: {
|
extraInfo: {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
state: MessageState.Success,
|
state: MessageState.Success,
|
||||||
serverName: this.getLlmConfig().id || 'unknown'
|
serverName: this.getLlmConfig().id || 'unknown',
|
||||||
|
enableXmlWrapper
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -563,7 +590,8 @@ export class TaskLoop {
|
|||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
state: toolCallResult.state,
|
state: toolCallResult.state,
|
||||||
serverName: this.getLlmConfig().id || 'unknown',
|
serverName: this.getLlmConfig().id || 'unknown',
|
||||||
usage: undefined
|
usage: undefined,
|
||||||
|
enableXmlWrapper
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -578,7 +606,8 @@ export class TaskLoop {
|
|||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
state: toolCallResult.state,
|
state: toolCallResult.state,
|
||||||
serverName: this.getLlmConfig().id || 'unknown',
|
serverName: this.getLlmConfig().id || 'unknown',
|
||||||
usage: this.completionUsage
|
usage: this.completionUsage,
|
||||||
|
enableXmlWrapper
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (toolCallResult.state === MessageState.ToolCall) {
|
} else if (toolCallResult.state === MessageState.ToolCall) {
|
||||||
@ -592,7 +621,8 @@ export class TaskLoop {
|
|||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
state: toolCallResult.state,
|
state: toolCallResult.state,
|
||||||
serverName: this.getLlmConfig().id || 'unknown',
|
serverName: this.getLlmConfig().id || 'unknown',
|
||||||
usage: this.completionUsage
|
usage: this.completionUsage,
|
||||||
|
enableXmlWrapper
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -606,10 +636,96 @@ export class TaskLoop {
|
|||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
state: MessageState.Success,
|
state: MessageState.Success,
|
||||||
serverName: this.getLlmConfig().id || 'unknown',
|
serverName: this.getLlmConfig().id || 'unknown',
|
||||||
usage: this.completionUsage
|
usage: this.completionUsage,
|
||||||
|
enableXmlWrapper
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
|
||||||
|
// 如果 xml 模型,需要检查内部是否含有有效的 xml 进行调用
|
||||||
|
if (tabStorage.settings.enableXmlWrapper) {
|
||||||
|
const xmls = getXmlsFromString(this.streamingContent.value);
|
||||||
|
if (xmls.length === 0) {
|
||||||
|
// 没有 xml 了,说明对话结束
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 user 作为身份来承载 xml 调用的结果
|
||||||
|
// 并且在 extra 内存储结构化信息
|
||||||
|
const fakeUserMessage = {
|
||||||
|
role: 'user',
|
||||||
|
content: '',
|
||||||
|
extraInfo: {
|
||||||
|
created: Date.now(),
|
||||||
|
state: MessageState.Success,
|
||||||
|
serverName: this.getLlmConfig().id || 'unknown',
|
||||||
|
usage: this.completionUsage,
|
||||||
|
enableXmlWrapper,
|
||||||
|
}
|
||||||
|
} as ChatMessage;
|
||||||
|
|
||||||
|
// 有 xml 了,需要检查 xml 内部是否有有效的 xml 进行调用
|
||||||
|
for (const xml of xmls) {
|
||||||
|
const toolcall = await getToolCallFromXmlString(xml);
|
||||||
|
|
||||||
|
if (!toolcall) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// toolcall 事件
|
||||||
|
// 此处使用的是 xml 使用的 toolcall,为了保持一致性,需要转换成 openai 标准下的 toolcall
|
||||||
|
const normaliseToolcall = toNormaliseToolcall(toolcall, toolcallIndexAdapter);
|
||||||
|
this.consumeToolCalls(normaliseToolcall);
|
||||||
|
|
||||||
|
// 调用 XML 调用,其实可以考虑后续把这个循环改成 Promise.race
|
||||||
|
const toolCallResult = await handleXmlWrapperToolcall(toolcall);
|
||||||
|
|
||||||
|
// toolcalled 事件
|
||||||
|
// 因为是交付给后续进行统一消费的,所以此处的输出满足 openai 接口规范
|
||||||
|
this.consumeToolCalleds(toolCallResult);
|
||||||
|
|
||||||
|
// XML 模式下只存在 assistant 和 user 这两个角色,因此,以 user 为身份来存储
|
||||||
|
if (toolCallResult.state === MessageState.InvalidXml) {
|
||||||
|
// 如果是因为解析 XML 错误,则重新开始
|
||||||
|
tabStorage.messages.pop();
|
||||||
|
jsonParseErrorRetryCount ++;
|
||||||
|
|
||||||
|
redLog('解析 XML 错误 ' + normaliseToolcall?.function?.arguments);
|
||||||
|
|
||||||
|
// 如果因为 XML 错误而失败太多,就只能中断了
|
||||||
|
if (jsonParseErrorRetryCount >= (this.taskOptions.maxJsonParseRetry || 3)) {
|
||||||
|
|
||||||
|
const prompt = getXmlResultPrompt(toolcall.callId, `解析 XML 错误,无法继续调用工具 (累计错误次数 ${this.taskOptions.maxJsonParseRetry})`);
|
||||||
|
|
||||||
|
fakeUserMessage.content += prompt;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (toolCallResult.state === MessageState.Success) {
|
||||||
|
// TODO: xml 目前只支持 text 类型的回复
|
||||||
|
const toolCallResultString = toolCallResult.content
|
||||||
|
.filter(c => c.type === 'text')
|
||||||
|
.map(c => c.text)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
fakeUserMessage.content += getXmlResultPrompt(toolcall.callId, toolCallResultString);
|
||||||
|
|
||||||
|
} else if (toolCallResult.state === MessageState.ToolCall) {
|
||||||
|
// TODO: xml 目前只支持 text 类型的回复
|
||||||
|
const toolCallResultString = toolCallResult.content
|
||||||
|
.filter(c => c.type === 'text')
|
||||||
|
.map(c => c.text)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
fakeUserMessage.content += getXmlResultPrompt(toolcall.callId, toolCallResultString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tabStorage.messages.push(fakeUserMessage);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 普通对话直接结束
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 一些提示
|
// 一些提示
|
||||||
|
@ -1,7 +1,21 @@
|
|||||||
import type { ToolItem } from "@/hook/type";
|
import { parseString } from 'xml2js';
|
||||||
|
import { MessageState, type ToolCall } from '../chat-box/chat';
|
||||||
|
import { mcpClientAdapter } from '@/views/connect/core';
|
||||||
|
import { handleToolResponse, type IToolCallIndex, type ToolCallResult } from './handle-tool-calls';
|
||||||
|
import type { ChatStorage, EnableToolItem } from "../chat-box/chat";
|
||||||
|
|
||||||
export function toolSchemaToPromptDescription(tools: ToolItem[]) {
|
export interface XmlToolCall {
|
||||||
|
server: string;
|
||||||
|
name: string;
|
||||||
|
callId: string;
|
||||||
|
parameters: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function toolSchemaToPromptDescription(enableTools: EnableToolItem[]) {
|
||||||
let prompt = '';
|
let prompt = '';
|
||||||
|
|
||||||
|
const tools = enableTools.filter(tool => tool.enabled);
|
||||||
|
|
||||||
// 无参数的工具
|
// 无参数的工具
|
||||||
const noParamTools = tools.filter(tool =>
|
const noParamTools = tools.filter(tool =>
|
||||||
@ -38,15 +52,32 @@ export function toolSchemaToPromptDescription(tools: ToolItem[]) {
|
|||||||
return prompt;
|
return prompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getXmlWrapperPrompt(tools: ToolItem[]) {
|
export function getXmlWrapperPrompt(tools: EnableToolItem[], tabStorage: ChatStorage) {
|
||||||
|
|
||||||
const toolPrompt = toolSchemaToPromptDescription(tools);
|
const toolPrompt = toolSchemaToPromptDescription(tools);
|
||||||
|
const requests = [
|
||||||
|
`ALWAYS analyze what function calls would be appropriate for the task`,
|
||||||
|
`ALWAYS format your function call usage EXACTLY as specified in the schema`,
|
||||||
|
`NEVER skip required parameters in function calls`,
|
||||||
|
`NEVER invent functions that arent available to you`,
|
||||||
|
`ALWAYS wait for function call execution results before continuing`,
|
||||||
|
`After invoking a function, wait for the output in <function_results> tag and then continue with your response`,
|
||||||
|
`NEVER mock or form <function_results> on your own, it will be provided to you after the execution`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!tabStorage.settings.parallelToolCalls) {
|
||||||
|
requests.push(`NEVER invoke multiple functions in a single response`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestString = requests.map((text, index) => {
|
||||||
|
return `${index + 1}. ${text}`;
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
[Start Fresh Session from here]
|
[Start Fresh Session from here]
|
||||||
|
|
||||||
<SYSTEM>
|
<SYSTEM>
|
||||||
You are SuperAssistant with the capabilities of invoke functions and make the best use of it during your assistance, a knowledgeable assistant focused on answering questions and providing information on any topics.
|
You are OpenMCP Assistant with the capabilities of invoke functions and make the best use of it during your assistance, a knowledgeable assistant focused on answering questions and providing information on any topics.
|
||||||
In this environment you have access to a set of tools you can use to answer the user's question.
|
In this environment you have access to a set of tools you can use to answer the user's question.
|
||||||
You have access to a set of functions you can use to answer the user's question. You do NOT currently have the ability to inspect files or interact with external resources, except by invoking the below functions.
|
You have access to a set of functions you can use to answer the user's question. You do NOT currently have the ability to inspect files or interact with external resources, except by invoking the below functions.
|
||||||
|
|
||||||
@ -94,15 +125,7 @@ You can invoke one or more functions by writing a "<function_calls>" block like
|
|||||||
String and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions.
|
String and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions.
|
||||||
|
|
||||||
When a user makes a request:
|
When a user makes a request:
|
||||||
1. ALWAYS analyze what function calls would be appropriate for the task
|
${requestString}
|
||||||
2. ALWAYS format your function call usage EXACTLY as specified in the schema
|
|
||||||
3. NEVER skip required parameters in function calls
|
|
||||||
4. NEVER invent functions that arent available to you
|
|
||||||
5. ALWAYS wait for function call execution results before continuing
|
|
||||||
6. After invoking a function, wait for the output in <function_results> tag and then continue with your response
|
|
||||||
7. NEVER invoke multiple functions in a single response
|
|
||||||
8. NEVER mock or form <function_results> on your own, it will be provided to you after the execution
|
|
||||||
|
|
||||||
|
|
||||||
Answer the user's request using the relevant tool(s), if they are available. Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. IF there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the user provides a specific value for a parameter (for example provided in quotes), make sure to use that value EXACTLY. DO NOT make up values for or ask about optional parameters. Carefully analyze descriptive terms in the request as they may indicate required parameter values that should be included even if not explicitly quoted.
|
Answer the user's request using the relevant tool(s), if they are available. Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. IF there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the user provides a specific value for a parameter (for example provided in quotes), make sure to use that value EXACTLY. DO NOT make up values for or ask about optional parameters. Carefully analyze descriptive terms in the request as they may indicate required parameter values that should be included even if not explicitly quoted.
|
||||||
|
|
||||||
@ -148,6 +171,90 @@ User Interaction Starts here:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getXmlWrapperPromptCn() {
|
export function getXmlResultPrompt(callId: string, result: string) {
|
||||||
|
return `
|
||||||
|
\`\`\`xml
|
||||||
|
<function_results>
|
||||||
|
<result call_id="${callId}">
|
||||||
|
${result}
|
||||||
|
</result>
|
||||||
|
</function_results>
|
||||||
|
\`\`\`
|
||||||
|
`.trim() + '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getXmlsFromString(content: string) {
|
||||||
|
const matches = content.matchAll(/```xml\n([\s\S]*?)\n```/g);
|
||||||
|
return Array.from(matches).map(match => match[1].trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getToolCallFromXmlString(xmlString: string): Promise<XmlToolCall | null> {
|
||||||
|
try {
|
||||||
|
const result = await new Promise<any>((resolve, reject) => {
|
||||||
|
parseString(xmlString, (err, result) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.function_calls?.invoke) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoke = result.function_calls.invoke[0].$;
|
||||||
|
const parameters: Record<string, any> = {};
|
||||||
|
|
||||||
|
if (result.function_calls.invoke[0].parameter) {
|
||||||
|
result.function_calls.invoke[0].parameter.forEach((param: any) => {
|
||||||
|
const name = param.$.name as string;
|
||||||
|
parameters[name] = param._;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// name 可能是 neo4j-mcp.executeReadOnlyCypherQuery
|
||||||
|
return {
|
||||||
|
server: '',
|
||||||
|
name: invoke.name,
|
||||||
|
callId: invoke.call_id,
|
||||||
|
parameters
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse function calls:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toNormaliseToolcall(xmlToolcall: XmlToolCall, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex): ToolCall {
|
||||||
|
const toolcall = {
|
||||||
|
id: xmlToolcall.callId,
|
||||||
|
index: -1,
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: xmlToolcall.name,
|
||||||
|
arguments: JSON.stringify(xmlToolcall.parameters)
|
||||||
|
}
|
||||||
|
} as ToolCall;
|
||||||
|
|
||||||
|
toolcall.index = toolcallIndexAdapter(toolcall);
|
||||||
|
|
||||||
|
return toolcall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleXmlWrapperToolcall(toolcall: XmlToolCall): Promise<ToolCallResult> {
|
||||||
|
if (!toolcall) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'error',
|
||||||
|
text: 'invalid xml'
|
||||||
|
}],
|
||||||
|
state: MessageState.InvalidXml
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进行调用,根据结果返回不同的值
|
||||||
|
console.log(toolcall);
|
||||||
|
|
||||||
|
const toolResponse = await mcpClientAdapter.callTool(toolcall.name, toolcall.parameters);
|
||||||
|
return handleToolResponse(toolResponse);
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user