@cherrystudio/ai-core
Version:
Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK
518 lines (484 loc) • 17.1 kB
JavaScript
const require_factory = require('../../factory-BRe13dSv.js');
const __ai_sdk_anthropic = require_factory.__toESM(require("@ai-sdk/anthropic"));
const __ai_sdk_google = require_factory.__toESM(require("@ai-sdk/google"));
const __ai_sdk_openai = require_factory.__toESM(require("@ai-sdk/openai"));
//#region src/core/plugins/built-in/logging.ts
/**
* 创建日志插件
*/
function createLoggingPlugin(config = {}) {
const { level = "info", logParams = true, logResult = false, logPerformance = true, logger = console.log } = config;
const startTimes = new Map();
return require_factory.definePlugin({
name: "built-in:logging",
onRequestStart: (context) => {
const requestId = context.requestId;
startTimes.set(requestId, Date.now());
logger(level, `🚀 AI Request Started`, {
requestId,
providerId: context.providerId,
modelId: context.modelId,
originalParams: logParams ? context.originalParams : "[hidden]"
});
},
onRequestEnd: (context, result) => {
const requestId = context.requestId;
const startTime = startTimes.get(requestId);
const duration = startTime ? Date.now() - startTime : void 0;
startTimes.delete(requestId);
const logData = {
requestId,
providerId: context.providerId,
modelId: context.modelId
};
if (logPerformance && duration) logData.duration = `${duration}ms`;
if (logResult) logData.result = result;
logger(level, `✅ AI Request Completed`, logData);
},
onError: (error, context) => {
const requestId = context.requestId;
const startTime = startTimes.get(requestId);
const duration = startTime ? Date.now() - startTime : void 0;
startTimes.delete(requestId);
logger("error", `❌ AI Request Failed`, {
requestId,
providerId: context.providerId,
modelId: context.modelId,
duration: duration ? `${duration}ms` : void 0,
error: {
name: error.name,
message: error.message,
stack: error.stack
}
});
}
});
}
//#endregion
//#region src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts
/**
* 使用 AI SDK 的 Tool 类型,更通用
*/
/**
* 默认系统提示符模板(提取自 Cherry Studio)
*/
const DEFAULT_SYSTEM_PROMPT = `In this environment you have access to a set of tools you can use to answer the user's question. \\
You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
## Tool Use Formatting
Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure:
<tool_use>
<name>{tool_name}</name>
<arguments>{json_arguments}</arguments>
</tool_use>
The tool name should be the exact name of the tool you are using, and the arguments should be a JSON object containing the parameters required by that tool. For example:
<tool_use>
<name>python_interpreter</name>
<arguments>{"code": "5 + 3 + 1294.678"}</arguments>
</tool_use>
The user will respond with the result of the tool use, which should be formatted as follows:
<tool_use_result>
<name>{tool_name}</name>
<result>{result}</result>
</tool_use_result>
The result should be a string, which can represent a file or any other output type. You can use this result as input for the next action.
For example, if the result of the tool use is an image file, you can use it in the next action like this:
<tool_use>
<name>image_transformer</name>
<arguments>{"image": "image_1.jpg"}</arguments>
</tool_use>
Always adhere to this format for the tool use to ensure proper parsing and execution.
## Tool Use Examples
{{ TOOL_USE_EXAMPLES }}
## Tool Use Available Tools
Above example were using notional tools that might not exist for you. You only have access to these tools:
{{ AVAILABLE_TOOLS }}
## Tool Use Rules
Here are the rules you should always follow to solve your task:
1. Always use the right arguments for the tools. Never use variable names as the action arguments, use the value instead.
2. Call a tool only when needed: do not call the search agent if you do not need information, try to solve the task yourself.
3. If no tool call is needed, just answer the question directly.
4. Never re-do a tool call that you previously did with the exact same parameters.
5. For tool use, MAKE SURE use XML tag format as shown in the examples above. Do not use any other format.
# User Instructions
{{ USER_SYSTEM_PROMPT }}
Now Begin! If you solve the task correctly, you will receive a reward of $1,000,000.`;
/**
* 默认工具使用示例(提取自 Cherry Studio)
*/
const DEFAULT_TOOL_USE_EXAMPLES = `
Here are a few examples using notional tools:
---
User: Generate an image of the oldest person in this document.
A: I can use the document_qa tool to find out who the oldest person is in the document.
<tool_use>
<name>document_qa</name>
<arguments>{"document": "document.pdf", "question": "Who is the oldest person mentioned?"}</arguments>
</tool_use>
User: <tool_use_result>
<name>document_qa</name>
<result>John Doe, a 55 year old lumberjack living in Newfoundland.</result>
</tool_use_result>
A: I can use the image_generator tool to create a portrait of John Doe.
<tool_use>
<name>image_generator</name>
<arguments>{"prompt": "A portrait of John Doe, a 55-year-old man living in Canada."}</arguments>
</tool_use>
User: <tool_use_result>
<name>image_generator</name>
<result>image.png</result>
</tool_use_result>
A: the image is generated as image.png
---
User: "What is the result of the following operation: 5 + 3 + 1294.678?"
A: I can use the python_interpreter tool to calculate the result of the operation.
<tool_use>
<name>python_interpreter</name>
<arguments>{"code": "5 + 3 + 1294.678"}</arguments>
</tool_use>
User: <tool_use_result>
<name>python_interpreter</name>
<result>1302.678</result>
</tool_use_result>
A: The result of the operation is 1302.678.
---
User: "Which city has the highest population , Guangzhou or Shanghai?"
A: I can use the search tool to find the population of Guangzhou.
<tool_use>
<name>search</name>
<arguments>{"query": "Population Guangzhou"}</arguments>
</tool_use>
User: <tool_use_result>
<name>search</name>
<result>Guangzhou has a population of 15 million inhabitants as of 2021.</result>
</tool_use_result>
A: I can use the search tool to find the population of Shanghai.
<tool_use>
<name>search</name>
<arguments>{"query": "Population Shanghai"}</arguments>
</tool_use>
User: <tool_use_result>
<name>search</name>
<result>26 million (2019)</result>
</tool_use_result>
Assistant: The population of Shanghai is 26 million, while Guangzhou has a population of 15 million. Therefore, Shanghai has the highest population.`;
/**
* 构建可用工具部分(提取自 Cherry Studio)
*/
function buildAvailableTools(tools) {
const availableTools = Object.keys(tools).map((toolName) => {
const tool = tools[toolName];
return `
<tool>
<name>${toolName}</name>
<description>${tool.description || ""}</description>
<arguments>
${tool.inputSchema ? JSON.stringify(tool.inputSchema) : ""}
</arguments>
</tool>
`;
}).join("\n");
return `<tools>
${availableTools}
</tools>`;
}
/**
* 默认的系统提示符构建函数(提取自 Cherry Studio)
*/
function defaultBuildSystemPrompt(userSystemPrompt, tools) {
const availableTools = buildAvailableTools(tools);
const fullPrompt = DEFAULT_SYSTEM_PROMPT.replace("{{ TOOL_USE_EXAMPLES }}", DEFAULT_TOOL_USE_EXAMPLES).replace("{{ AVAILABLE_TOOLS }}", availableTools).replace("{{ USER_SYSTEM_PROMPT }}", userSystemPrompt || "");
return fullPrompt;
}
/**
* 默认工具解析函数(提取自 Cherry Studio)
* 解析 XML 格式的工具调用
*/
function defaultParseToolUse(content, tools) {
if (!content || !tools || Object.keys(tools).length === 0) return [];
let contentToProcess = content;
if (!content.includes("<tool_use>")) contentToProcess = `<tool_use>\n${content}\n</tool_use>`;
const toolUsePattern = /<tool_use>([\s\S]*?)<name>([\s\S]*?)<\/name>([\s\S]*?)<arguments>([\s\S]*?)<\/arguments>([\s\S]*?)<\/tool_use>/g;
const results = [];
let match;
let idx = 0;
while ((match = toolUsePattern.exec(contentToProcess)) !== null) {
const toolName = match[2].trim();
const toolArgs = match[4].trim();
let parsedArgs;
try {
parsedArgs = JSON.parse(toolArgs);
} catch (error) {
parsedArgs = toolArgs;
}
const tool = tools[toolName];
if (!tool) {
console.warn(`Tool "${toolName}" not found in available tools`);
continue;
}
results.push({
id: `${toolName}-${idx++}`,
toolName,
arguments: parsedArgs,
status: "pending"
});
}
return results;
}
const createPromptToolUsePlugin = (config = {}) => {
const { enabled = true, buildSystemPrompt = defaultBuildSystemPrompt, parseToolUse = defaultParseToolUse } = config;
return require_factory.definePlugin({
name: "built-in:prompt-tool-use",
transformParams: (params, context) => {
if (!enabled || !params.tools || typeof params.tools !== "object") return params;
context.mcpTools = params.tools;
console.log("tools stored in context", params.tools);
const userSystemPrompt = typeof params.system === "string" ? params.system : "";
const systemPrompt = buildSystemPrompt(userSystemPrompt, params.tools);
let systemMessage = systemPrompt;
console.log("config.context", context);
if (config.createSystemMessage) systemMessage = config.createSystemMessage(systemPrompt, params, context);
const transformedParams = {
...params,
...systemMessage ? { system: systemMessage } : {},
tools: void 0
};
context.originalParams = transformedParams;
console.log("transformedParams", transformedParams);
return transformedParams;
},
transformStream: (_, context) => () => {
let textBuffer = "";
let stepId = "";
let executedResults = [];
if (!context.mcpTools) throw new Error("No tools available");
return new TransformStream({
async transform(chunk, controller) {
if (chunk.type === "text") {
textBuffer += chunk.text || "";
stepId = chunk.id || "";
controller.enqueue(chunk);
return;
}
if (chunk.type === "finish-step") {
const tools = context.mcpTools;
if (!tools || Object.keys(tools).length === 0) {
controller.enqueue(chunk);
return;
}
const parsedTools = parseToolUse(textBuffer, tools);
const validToolUses = parsedTools.filter((t) => t.status === "pending");
if (validToolUses.length === 0) {
controller.enqueue(chunk);
return;
}
controller.enqueue({
type: "start-step",
request: {},
warnings: []
});
executedResults = [];
for (const toolUse of validToolUses) try {
const tool = tools[toolUse.toolName];
if (!tool || typeof tool.execute !== "function") throw new Error(`Tool "${toolUse.toolName}" has no execute method`);
console.log(`[MCP Prompt Stream] Executing tool: ${toolUse.toolName}`, toolUse.arguments);
controller.enqueue({
type: "tool-call",
toolCallId: toolUse.id,
toolName: toolUse.toolName,
input: tool.inputSchema
});
const result = await tool.execute(toolUse.arguments, {
toolCallId: toolUse.id,
messages: [],
abortSignal: new AbortController().signal
});
controller.enqueue({
type: "tool-result",
toolCallId: toolUse.id,
toolName: toolUse.toolName,
input: toolUse.arguments,
output: result
});
executedResults.push({
toolCallId: toolUse.id,
toolName: toolUse.toolName,
result,
isError: false
});
} catch (error) {
console.error(`[MCP Prompt Stream] Tool execution failed: ${toolUse.toolName}`, error);
const toolError = {
type: "tool-error",
toolCallId: toolUse.id,
toolName: toolUse.toolName,
input: toolUse.arguments,
error: error instanceof Error ? error.message : String(error)
};
controller.enqueue(toolError);
controller.enqueue({
type: "error",
error: toolError.error
});
executedResults.push({
toolCallId: toolUse.id,
toolName: toolUse.toolName,
result: toolError.error,
isError: true
});
}
controller.enqueue({
type: "finish-step",
finishReason: "tool-calls",
response: chunk.response,
usage: chunk.usage,
providerMetadata: chunk.providerMetadata
});
if (validToolUses.length > 0) {
const toolResultsText = executedResults.map((tr) => {
if (!tr.isError) return `<tool_use_result>\n <name>${tr.toolName}</name>\n <result>${JSON.stringify(tr.result)}</result>\n</tool_use_result>`;
else {
const error = tr.result || "Unknown error";
return `<tool_use_result>\n <name>${tr.toolName}</name>\n <error>${error}</error>\n</tool_use_result>`;
}
}).join("\n\n");
const newMessages = [
...context.originalParams.messages || [],
{
role: "assistant",
content: textBuffer
},
{
role: "user",
content: toolResultsText
}
];
const recursiveParams = {
...context.originalParams,
messages: newMessages,
tools
};
context.originalParams.messages = newMessages;
try {
const recursiveResult = await context.recursiveCall(recursiveParams);
if (recursiveResult && recursiveResult.fullStream) {
const reader = recursiveResult.fullStream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value.type === "finish") break;
controller.enqueue(value);
}
} finally {
reader.releaseLock();
}
} else console.warn("[MCP Prompt] No fullstream found in recursive result:", recursiveResult);
} catch (error) {
console.error("[MCP Prompt] Recursive call failed:", error);
controller.enqueue({
type: "error",
error: {
message: error instanceof Error ? error.message : String(error),
name: error instanceof Error ? error.name : "RecursiveCallError"
}
});
controller.enqueue({
type: "text",
id: stepId,
text: "\n\n[工具执行后递归调用失败,继续对话...]"
});
}
}
textBuffer = "";
executedResults = [];
return;
}
controller.enqueue(chunk);
},
flush() {
console.log("[MCP Prompt] Stream ended, cleaning up...");
}
});
}
});
};
//#endregion
//#region src/core/plugins/built-in/webSearchPlugin/helper.ts
/**
* 插件的默认配置
*/
const DEFAULT_WEB_SEARCH_CONFIG = {
google: {},
"google-vertex": {},
openai: {},
xai: {
mode: "on",
returnCitations: true,
maxSearchResults: 5,
sources: [
{ type: "web" },
{ type: "x" },
{ type: "news" }
]
},
anthropic: { maxUses: 5 }
};
//#endregion
//#region src/core/plugins/built-in/webSearchPlugin/index.ts
/**
* 网络搜索插件
*
* @param config - 在插件初始化时传入的静态配置
*/
const webSearchPlugin = (config = DEFAULT_WEB_SEARCH_CONFIG) => require_factory.definePlugin({
name: "webSearch",
enforce: "pre",
transformParams: async (params, context) => {
const { providerId } = context;
console.log("providerId", providerId);
switch (providerId) {
case "openai": {
if (config.openai) {
if (!params.tools) params.tools = {};
params.tools.web_search_preview = __ai_sdk_openai.openai.tools.webSearchPreview(config.openai);
}
break;
}
case "anthropic": {
if (config.anthropic) {
if (!params.tools) params.tools = {};
params.tools.web_search = __ai_sdk_anthropic.anthropic.tools.webSearch_20250305(config.anthropic);
}
break;
}
case "google": {
if (!params.tools) params.tools = {};
params.tools.web_search = __ai_sdk_google.google.tools.googleSearch(config.google || {});
break;
}
case "xai": {
if (config.xai) {
const searchOptions = require_factory.createXaiOptions({ searchParameters: {
...config.xai,
mode: "on"
} });
params.providerOptions = require_factory.mergeProviderOptions(params.providerOptions, searchOptions);
}
break;
}
}
return params;
}
});
//#endregion
//#region src/core/plugins/built-in/index.ts
/**
* 内置插件命名空间
* 所有内置插件都以 'built-in:' 为前缀
*/
const BUILT_IN_PLUGIN_PREFIX = "built-in:";
//#endregion
exports.BUILT_IN_PLUGIN_PREFIX = BUILT_IN_PLUGIN_PREFIX;
exports.createLoggingPlugin = createLoggingPlugin;
exports.createPromptToolUsePlugin = createPromptToolUsePlugin;
exports.webSearchPlugin = webSearchPlugin;