task loop
This commit is contained in:
parent
be641197b5
commit
4a8385d7ad
@ -39,22 +39,22 @@ export class MessageBridge {
|
|||||||
switch (platform) {
|
switch (platform) {
|
||||||
case 'vscode':
|
case 'vscode':
|
||||||
this.setupVsCodeListener();
|
this.setupVsCodeListener();
|
||||||
pinkLog('当前模式: vscode');
|
pinkLog('current platform: vscode');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'electron':
|
case 'electron':
|
||||||
this.setupElectronListener();
|
this.setupElectronListener();
|
||||||
pinkLog('当前模式: electron');
|
pinkLog('current platform: electron');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'nodejs':
|
case 'nodejs':
|
||||||
this.setupNodejsListener();
|
this.setupNodejsListener();
|
||||||
pinkLog('当前模式: nodejs');
|
pinkLog('current platform: nodejs');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'web':
|
case 'web':
|
||||||
this.setupWebSocket();
|
this.setupWebSocket();
|
||||||
pinkLog('当前模式: web');
|
pinkLog('current platform: web');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export class TaskLoop {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly taskOptions: TaskLoopOptions = {
|
private taskOptions: TaskLoopOptions = {
|
||||||
maxEpochs: 50,
|
maxEpochs: 50,
|
||||||
maxJsonParseRetry: 3,
|
maxJsonParseRetry: 3,
|
||||||
adapter: undefined,
|
adapter: undefined,
|
||||||
@ -99,6 +99,25 @@ export class TaskLoop {
|
|||||||
mcpClientAdapter.addConnectRefreshListener();
|
mcpClientAdapter.addConnectRefreshListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async waitConnection() {
|
||||||
|
await this.nodejsStatus.connectionFut;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTaskLoopOptions(taskOptions: TaskLoopOptions) {
|
||||||
|
const {
|
||||||
|
maxEpochs = 50,
|
||||||
|
maxJsonParseRetry = 3,
|
||||||
|
verbose = 1,
|
||||||
|
} = taskOptions;
|
||||||
|
|
||||||
|
this.taskOptions = {
|
||||||
|
maxEpochs,
|
||||||
|
maxJsonParseRetry,
|
||||||
|
verbose,
|
||||||
|
...this.taskOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 处理 streaming 输出的每一个分块的 content 部分
|
* @description 处理 streaming 输出的每一个分块的 content 部分
|
||||||
* @param chunk
|
* @param chunk
|
||||||
@ -784,9 +803,11 @@ export class TaskLoop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPrompt(promptId: string, args: Record<string, any>) {
|
public async getPrompt(promptId: string, args?: Record<string, any>) {
|
||||||
const prompt = await mcpClientAdapter.readPromptTemplate(promptId, args);
|
const prompt = await mcpClientAdapter.readPromptTemplate(promptId, args);
|
||||||
return prompt;
|
// transform prompt to string
|
||||||
|
const promptString = prompt.messages.map(m => m.content.text).join('\n');
|
||||||
|
return promptString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getResource(resourceUri: string) {
|
public async getResource(resourceUri: string) {
|
||||||
|
@ -610,7 +610,7 @@ class McpClientAdapter {
|
|||||||
|
|
||||||
}, { once: false });
|
}, { once: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchSignature = await this.getLaunchSignature();
|
const launchSignature = await this.getLaunchSignature();
|
||||||
|
|
||||||
let allOk = true;
|
let allOk = true;
|
||||||
@ -677,7 +677,7 @@ class McpClientAdapter {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readPromptTemplate(promptId: string, args: Record<string, any>) {
|
public async readPromptTemplate(promptId: string, args?: Record<string, any>) {
|
||||||
// TODO: 如果遇到不同服务器的同名 tool,请拓展解决方案
|
// TODO: 如果遇到不同服务器的同名 tool,请拓展解决方案
|
||||||
// 目前只找到第一个匹配 toolName 的工具进行调用
|
// 目前只找到第一个匹配 toolName 的工具进行调用
|
||||||
let clientId = this.clients[0].clientId;
|
let clientId = this.clients[0].clientId;
|
||||||
@ -691,6 +691,8 @@ class McpClientAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(args);
|
||||||
|
|
||||||
const bridge = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
const { code, msg } = await bridge.commandRequest<PromptsGetResponse>('prompts/get', { clientId, promptId, args });
|
const { code, msg } = await bridge.commandRequest<PromptsGetResponse>('prompts/get', { clientId, promptId, args });
|
||||||
return msg;
|
return msg;
|
||||||
|
13
resources/openmcp-sdk-release/task-loop.d.ts
vendored
13
resources/openmcp-sdk-release/task-loop.d.ts
vendored
@ -189,6 +189,17 @@ interface ChatSetting {
|
|||||||
export class TaskLoop {
|
export class TaskLoop {
|
||||||
constructor(taskOptions?: TaskLoopOptions);
|
constructor(taskOptions?: TaskLoopOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description wait for connection
|
||||||
|
*/
|
||||||
|
waitConnection(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Set the task loop options
|
||||||
|
* @param taskOptions
|
||||||
|
*/
|
||||||
|
setTaskLoopOptions(taskOptions: TaskLoopOptions): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description make chat data
|
* @description make chat data
|
||||||
* @param tabStorage
|
* @param tabStorage
|
||||||
@ -294,7 +305,7 @@ export class TaskLoop {
|
|||||||
/**
|
/**
|
||||||
* @description Get prompt template from mcp server
|
* @description Get prompt template from mcp server
|
||||||
*/
|
*/
|
||||||
getPrompt(promptId: string, args: Record<string, any>): Promise<string>;
|
getPrompt(promptId: string, args?: Record<string, any>): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Get resource template from mcp server
|
* @description Get resource template from mcp server
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
import { WebSocket } from 'ws';
|
import { WebSocket } from 'ws';
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { routeMessage } from '../common/router.js';
|
|
||||||
import { ConnectionType } from '../mcp/client.dto.js';
|
import { ConnectionType } from '../mcp/client.dto.js';
|
||||||
|
|
||||||
// WebSocket 消息格式
|
// WebSocket 消息格式
|
||||||
@ -80,318 +76,3 @@ export class VSCodeWebViewLike {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class TaskLoopAdapter {
|
|
||||||
public emitter: EventEmitter;
|
|
||||||
private messageHandlers: Set<MessageHandler>;
|
|
||||||
private connectionOptions: IConnectionArgs[] = [];
|
|
||||||
|
|
||||||
constructor(option?: any) {
|
|
||||||
this.emitter = new EventEmitter(option);
|
|
||||||
this.messageHandlers = new Set();
|
|
||||||
|
|
||||||
this.emitter.on('message/renderer', (message: WebSocketMessage) => {
|
|
||||||
this.messageHandlers.forEach((handler) => handler(message));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 默认需要将监听的消息导入到 routeMessage 中
|
|
||||||
this.onDidReceiveMessage((message) => {
|
|
||||||
const { command, data } = message;
|
|
||||||
|
|
||||||
switch (command) {
|
|
||||||
case 'nodejs/launch-signature':
|
|
||||||
this.postMessage({
|
|
||||||
command: 'nodejs/launch-signature',
|
|
||||||
data: {
|
|
||||||
code: 200,
|
|
||||||
msg: this.connectionOptions
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'nodejs/update-connection-signature':
|
|
||||||
// sdk 模式下不需要自动保存连接参数
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
routeMessage(command, data, this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 发送消息
|
|
||||||
* @param message - 包含 command 和 args 的消息
|
|
||||||
*/
|
|
||||||
public postMessage(message: WebSocketMessage): void {
|
|
||||||
this.emitter.emit('message/service', message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 注册接受消息的句柄
|
|
||||||
* @param callback - 消息回调
|
|
||||||
* @returns {{ dispose: () => void }} - 可销毁的监听器
|
|
||||||
*/
|
|
||||||
public onDidReceiveMessage(callback: MessageHandler): { dispose: () => void } {
|
|
||||||
this.messageHandlers.add(callback);
|
|
||||||
return {
|
|
||||||
dispose: () => this.messageHandlers.delete(callback),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 连接到 mcp 服务端
|
|
||||||
* @param mcpOption
|
|
||||||
*/
|
|
||||||
public addMcp(mcpOption: IConnectionArgs) {
|
|
||||||
|
|
||||||
// 0.1.4 新版本开始,此处修改为懒加载连接
|
|
||||||
// 实际的连接移交给前端 mcpAdapter 中进行统一的调度
|
|
||||||
// 调度步骤如下:
|
|
||||||
// getLaunchSignature 先获取访问签名,访问签名通过当前函数 push 到 class 中
|
|
||||||
|
|
||||||
this.connectionOptions.push(mcpOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StdioMCPConfig {
|
|
||||||
command: string;
|
|
||||||
args: string[];
|
|
||||||
env?: {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
description?: string;
|
|
||||||
prompts?: string[];
|
|
||||||
resources?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HttpMCPConfig {
|
|
||||||
url: string;
|
|
||||||
type?: string;
|
|
||||||
env?: {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
description?: string;
|
|
||||||
prompts?: string[];
|
|
||||||
resources?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OmAgentConfiguration {
|
|
||||||
version: string;
|
|
||||||
mcpServers: {
|
|
||||||
[key: string]: StdioMCPConfig | HttpMCPConfig;
|
|
||||||
};
|
|
||||||
defaultLLM: {
|
|
||||||
baseURL: string;
|
|
||||||
apiToken: string;
|
|
||||||
model: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DefaultLLM {
|
|
||||||
baseURL: string;
|
|
||||||
apiToken?: string;
|
|
||||||
model: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
import {
|
|
||||||
MessageState,
|
|
||||||
type TaskLoopOptions,
|
|
||||||
type ChatMessage,
|
|
||||||
type ChatSetting,
|
|
||||||
type TaskLoop,
|
|
||||||
type TextMessage
|
|
||||||
} from '../../task-loop.js';
|
|
||||||
|
|
||||||
export function UserMessage(content: string): TextMessage {
|
|
||||||
return {
|
|
||||||
role: 'user',
|
|
||||||
content,
|
|
||||||
extraInfo: {
|
|
||||||
created: Date.now(),
|
|
||||||
state: MessageState.None,
|
|
||||||
serverName: '',
|
|
||||||
enableXmlWrapper: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AssistantMessage(content: string): TextMessage {
|
|
||||||
return {
|
|
||||||
role: 'assistant',
|
|
||||||
content,
|
|
||||||
extraInfo: {
|
|
||||||
created: Date.now(),
|
|
||||||
state: MessageState.None,
|
|
||||||
serverName: '',
|
|
||||||
enableXmlWrapper: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OmAgent {
|
|
||||||
private _adapter: TaskLoopAdapter;
|
|
||||||
private _loop?: TaskLoop;
|
|
||||||
private _defaultLLM?: DefaultLLM;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this._adapter = new TaskLoopAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Load MCP configuration from file.
|
|
||||||
* Supports multiple MCP backends and a default LLM model configuration.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* Example configuration:
|
|
||||||
* {
|
|
||||||
* "version": "1.0.0",
|
|
||||||
* "mcpServers": {
|
|
||||||
* "openmemory": {
|
|
||||||
* "command": "npx",
|
|
||||||
* "args": ["-y", "openmemory"],
|
|
||||||
* "env": {
|
|
||||||
* "OPENMEMORY_API_KEY": "YOUR_API_KEY",
|
|
||||||
* "CLIENT_NAME": "openmemory"
|
|
||||||
* },
|
|
||||||
* "description": "A MCP for long-term memory support"
|
|
||||||
* }
|
|
||||||
* },
|
|
||||||
* "defaultLLM": {
|
|
||||||
* "baseURL": "https://api.openmemory.ai",
|
|
||||||
* "apiToken": "YOUR_API_KEY",
|
|
||||||
* "model": "deepseek-chat"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @param configPath - Path to the configuration file
|
|
||||||
*/
|
|
||||||
public loadMcpConfig(configPath: string) {
|
|
||||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as OmAgentConfiguration;
|
|
||||||
const { mcpServers, defaultLLM } = config;
|
|
||||||
|
|
||||||
// set default llm
|
|
||||||
this.setDefaultLLM(defaultLLM);
|
|
||||||
|
|
||||||
for (const key in mcpServers) {
|
|
||||||
const mcpConfig = mcpServers[key];
|
|
||||||
if ('command' in mcpConfig) {
|
|
||||||
const commandString = (
|
|
||||||
mcpConfig.command + ' ' + mcpConfig.args.join(' ')
|
|
||||||
).trim();
|
|
||||||
|
|
||||||
this._adapter.addMcp({
|
|
||||||
commandString,
|
|
||||||
connectionType: 'STDIO',
|
|
||||||
env: mcpConfig.env,
|
|
||||||
description: mcpConfig.description,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const connectionType: ConnectionType = mcpConfig.type === 'http' ? 'STREAMABLE_HTTP' : 'SSE';
|
|
||||||
this._adapter.addMcp({
|
|
||||||
url: mcpConfig.url,
|
|
||||||
env: mcpConfig.env,
|
|
||||||
connectionType,
|
|
||||||
description: mcpConfig.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Add MCP server
|
|
||||||
*/
|
|
||||||
public addMcpServer(connectionArgs: IConnectionArgs) {
|
|
||||||
this._adapter.addMcp(connectionArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getLoop(loopOption?: TaskLoopOptions) {
|
|
||||||
if (this._loop) {
|
|
||||||
return this._loop;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
verbose = 1,
|
|
||||||
maxEpochs = 50,
|
|
||||||
maxJsonParseRetry = 3,
|
|
||||||
} = loopOption || {}
|
|
||||||
|
|
||||||
const adapter = this._adapter;
|
|
||||||
const { TaskLoop } = await import('../../task-loop.js');
|
|
||||||
this._loop = new TaskLoop({ adapter, verbose, maxEpochs, maxJsonParseRetry });
|
|
||||||
|
|
||||||
return this._loop;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setDefaultLLM(option: DefaultLLM) {
|
|
||||||
this._defaultLLM = option;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getPrompt(promptId: string, args: Record<string, any>) {
|
|
||||||
const loop = await this.getLoop();
|
|
||||||
const prompt = await loop.getPrompt(promptId, args);
|
|
||||||
return prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Asynchronous invoking agent by string or messages
|
|
||||||
* @param messages Chat message or string
|
|
||||||
* @param settings Chat setting and task loop options
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
public async ainvoke(
|
|
||||||
{ messages, settings }: { messages: ChatMessage[] | string; settings?: ChatSetting & Partial<TaskLoopOptions>; }
|
|
||||||
) {
|
|
||||||
if (messages.length === 0) {
|
|
||||||
throw new Error('messages is empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
// detach taskloop option from settings and set default value
|
|
||||||
const {
|
|
||||||
maxEpochs = 50,
|
|
||||||
maxJsonParseRetry = 3,
|
|
||||||
verbose = 1
|
|
||||||
} = settings || {};
|
|
||||||
|
|
||||||
const loop = await this.getLoop({ maxEpochs, maxJsonParseRetry, verbose });
|
|
||||||
const storage = await loop.createStorage(settings);
|
|
||||||
|
|
||||||
// set input message
|
|
||||||
// user can invoke [UserMessage("CONTENT")] to make messages quickly
|
|
||||||
// or use string directly
|
|
||||||
let userMessage: string;
|
|
||||||
if (typeof messages === 'string') {
|
|
||||||
userMessage = messages;
|
|
||||||
} else {
|
|
||||||
const lastMessageContent = messages.at(-1)?.content;
|
|
||||||
if (typeof lastMessageContent === 'string') {
|
|
||||||
userMessage = lastMessageContent;
|
|
||||||
} else {
|
|
||||||
throw new Error('last message content is undefined');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// select correct llm config
|
|
||||||
// user can set llm config via omagent.setDefaultLLM()
|
|
||||||
// or write "defaultLLM" in mcpconfig.json to specify
|
|
||||||
if (this._defaultLLM) {
|
|
||||||
loop.setLlmConfig({
|
|
||||||
baseUrl: this._defaultLLM.baseURL,
|
|
||||||
userToken: this._defaultLLM.apiToken,
|
|
||||||
userModel: this._defaultLLM.model,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// throw error to user and give the suggestion
|
|
||||||
throw new Error('default LLM is not set, please set it via omagent.setDefaultLLM() or write "defaultLLM" in mcpconfig.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
await loop.start(storage, userMessage);
|
|
||||||
|
|
||||||
// get response from last message in message list
|
|
||||||
const lastMessage = storage.messages.at(-1)?.content;
|
|
||||||
return lastMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
327
service/src/hook/sdk.ts
Normal file
327
service/src/hook/sdk.ts
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { routeMessage } from '../common/router.js';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export class TaskLoopAdapter {
|
||||||
|
public emitter: EventEmitter;
|
||||||
|
private messageHandlers: Set<MessageHandler>;
|
||||||
|
private connectionOptions: IConnectionArgs[] = [];
|
||||||
|
|
||||||
|
constructor(option?: any) {
|
||||||
|
this.emitter = new EventEmitter(option);
|
||||||
|
this.messageHandlers = new Set();
|
||||||
|
|
||||||
|
this.emitter.on('message/renderer', (message: WebSocketMessage) => {
|
||||||
|
this.messageHandlers.forEach((handler) => handler(message));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 默认需要将监听的消息导入到 routeMessage 中
|
||||||
|
this.onDidReceiveMessage((message) => {
|
||||||
|
const { command, data } = message;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'nodejs/launch-signature':
|
||||||
|
this.postMessage({
|
||||||
|
command: 'nodejs/launch-signature',
|
||||||
|
data: {
|
||||||
|
code: 200,
|
||||||
|
msg: this.connectionOptions
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'nodejs/update-connection-signature':
|
||||||
|
// sdk 模式下不需要自动保存连接参数
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
routeMessage(command, data, this);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 发送消息
|
||||||
|
* @param message - 包含 command 和 args 的消息
|
||||||
|
*/
|
||||||
|
public postMessage(message: WebSocketMessage): void {
|
||||||
|
this.emitter.emit('message/service', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 注册接受消息的句柄
|
||||||
|
* @param callback - 消息回调
|
||||||
|
* @returns {{ dispose: () => void }} - 可销毁的监听器
|
||||||
|
*/
|
||||||
|
public onDidReceiveMessage(callback: MessageHandler): { dispose: () => void } {
|
||||||
|
this.messageHandlers.add(callback);
|
||||||
|
return {
|
||||||
|
dispose: () => this.messageHandlers.delete(callback),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 连接到 mcp 服务端
|
||||||
|
* @param mcpOption
|
||||||
|
*/
|
||||||
|
public addMcp(mcpOption: IConnectionArgs) {
|
||||||
|
|
||||||
|
// 0.1.4 新版本开始,此处修改为懒加载连接
|
||||||
|
// 实际的连接移交给前端 mcpAdapter 中进行统一的调度
|
||||||
|
// 调度步骤如下:
|
||||||
|
// getLaunchSignature 先获取访问签名,访问签名通过当前函数 push 到 class 中
|
||||||
|
|
||||||
|
this.connectionOptions.push(mcpOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StdioMCPConfig {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
env?: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
description?: string;
|
||||||
|
prompts?: string[];
|
||||||
|
resources?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HttpMCPConfig {
|
||||||
|
url: string;
|
||||||
|
type?: string;
|
||||||
|
env?: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
description?: string;
|
||||||
|
prompts?: string[];
|
||||||
|
resources?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OmAgentConfiguration {
|
||||||
|
version: string;
|
||||||
|
mcpServers: {
|
||||||
|
[key: string]: StdioMCPConfig | HttpMCPConfig;
|
||||||
|
};
|
||||||
|
defaultLLM: {
|
||||||
|
baseURL: string;
|
||||||
|
apiToken: string;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultLLM {
|
||||||
|
baseURL: string;
|
||||||
|
apiToken?: string;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
import {
|
||||||
|
MessageState,
|
||||||
|
type TaskLoopOptions,
|
||||||
|
type ChatMessage,
|
||||||
|
type ChatSetting,
|
||||||
|
type TaskLoop,
|
||||||
|
type TextMessage
|
||||||
|
} from '../../task-loop.js';
|
||||||
|
import { IConnectionArgs, MessageHandler, WebSocketMessage } from './adapter.js';
|
||||||
|
import { ConnectionType } from 'src/mcp/client.dto.js';
|
||||||
|
|
||||||
|
export function UserMessage(content: string): TextMessage {
|
||||||
|
return {
|
||||||
|
role: 'user',
|
||||||
|
content,
|
||||||
|
extraInfo: {
|
||||||
|
created: Date.now(),
|
||||||
|
state: MessageState.None,
|
||||||
|
serverName: '',
|
||||||
|
enableXmlWrapper: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AssistantMessage(content: string): TextMessage {
|
||||||
|
return {
|
||||||
|
role: 'assistant',
|
||||||
|
content,
|
||||||
|
extraInfo: {
|
||||||
|
created: Date.now(),
|
||||||
|
state: MessageState.None,
|
||||||
|
serverName: '',
|
||||||
|
enableXmlWrapper: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OmAgent {
|
||||||
|
private _adapter: TaskLoopAdapter;
|
||||||
|
private _loop?: TaskLoop;
|
||||||
|
private _defaultLLM?: DefaultLLM;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._adapter = new TaskLoopAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Load MCP configuration from file.
|
||||||
|
* Supports multiple MCP backends and a default LLM model configuration.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Example configuration:
|
||||||
|
* {
|
||||||
|
* "version": "1.0.0",
|
||||||
|
* "mcpServers": {
|
||||||
|
* "openmemory": {
|
||||||
|
* "command": "npx",
|
||||||
|
* "args": ["-y", "openmemory"],
|
||||||
|
* "env": {
|
||||||
|
* "OPENMEMORY_API_KEY": "YOUR_API_KEY",
|
||||||
|
* "CLIENT_NAME": "openmemory"
|
||||||
|
* },
|
||||||
|
* "description": "A MCP for long-term memory support"
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* "defaultLLM": {
|
||||||
|
* "baseURL": "https://api.openmemory.ai",
|
||||||
|
* "apiToken": "YOUR_API_KEY",
|
||||||
|
* "model": "deepseek-chat"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param configPath - Path to the configuration file
|
||||||
|
*/
|
||||||
|
public loadMcpConfig(configPath: string) {
|
||||||
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as OmAgentConfiguration;
|
||||||
|
const { mcpServers, defaultLLM } = config;
|
||||||
|
|
||||||
|
// set default llm
|
||||||
|
this.setDefaultLLM(defaultLLM);
|
||||||
|
|
||||||
|
for (const key in mcpServers) {
|
||||||
|
const mcpConfig = mcpServers[key];
|
||||||
|
if ('command' in mcpConfig) {
|
||||||
|
const commandString = (
|
||||||
|
mcpConfig.command + ' ' + mcpConfig.args.join(' ')
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
this._adapter.addMcp({
|
||||||
|
commandString,
|
||||||
|
connectionType: 'STDIO',
|
||||||
|
env: mcpConfig.env,
|
||||||
|
description: mcpConfig.description,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const connectionType: ConnectionType = mcpConfig.type === 'http' ? 'STREAMABLE_HTTP' : 'SSE';
|
||||||
|
this._adapter.addMcp({
|
||||||
|
url: mcpConfig.url,
|
||||||
|
env: mcpConfig.env,
|
||||||
|
connectionType,
|
||||||
|
description: mcpConfig.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Add MCP server
|
||||||
|
*/
|
||||||
|
public addMcpServer(connectionArgs: IConnectionArgs) {
|
||||||
|
this._adapter.addMcp(connectionArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLoop(loopOption?: TaskLoopOptions) {
|
||||||
|
|
||||||
|
if (this._loop) {
|
||||||
|
if (loopOption) {
|
||||||
|
this._loop.setTaskLoopOptions(loopOption);
|
||||||
|
}
|
||||||
|
return this._loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
verbose = 1,
|
||||||
|
maxEpochs = 50,
|
||||||
|
maxJsonParseRetry = 3,
|
||||||
|
} = loopOption || {}
|
||||||
|
|
||||||
|
const adapter = this._adapter;
|
||||||
|
const { TaskLoop } = await import('../../task-loop.js');
|
||||||
|
|
||||||
|
this._loop = new TaskLoop({ adapter, verbose, maxEpochs, maxJsonParseRetry });
|
||||||
|
await this._loop.waitConnection();
|
||||||
|
|
||||||
|
return this._loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setDefaultLLM(option: DefaultLLM) {
|
||||||
|
this._defaultLLM = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPrompt(promptId: string, args: Record<string, any>) {
|
||||||
|
const loop = await this.getLoop();
|
||||||
|
|
||||||
|
const prompt = await loop.getPrompt(promptId, args);
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Asynchronous invoking agent by string or messages
|
||||||
|
* @param messages Chat message or string
|
||||||
|
* @param settings Chat setting and task loop options
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async ainvoke(
|
||||||
|
{ messages, settings }: { messages: ChatMessage[] | string; settings?: ChatSetting & Partial<TaskLoopOptions>; }
|
||||||
|
) {
|
||||||
|
if (messages.length === 0) {
|
||||||
|
throw new Error('messages is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
// detach taskloop option from settings and set default value
|
||||||
|
const {
|
||||||
|
maxEpochs = 50,
|
||||||
|
maxJsonParseRetry = 3,
|
||||||
|
verbose = 1
|
||||||
|
} = settings || {};
|
||||||
|
|
||||||
|
const loop = await this.getLoop({ maxEpochs, maxJsonParseRetry, verbose });
|
||||||
|
const storage = await loop.createStorage(settings);
|
||||||
|
|
||||||
|
// set input message
|
||||||
|
// user can invoke [UserMessage("CONTENT")] to make messages quickly
|
||||||
|
// or use string directly
|
||||||
|
let userMessage: string;
|
||||||
|
if (typeof messages === 'string') {
|
||||||
|
userMessage = messages;
|
||||||
|
} else {
|
||||||
|
const lastMessageContent = messages.at(-1)?.content;
|
||||||
|
if (typeof lastMessageContent === 'string') {
|
||||||
|
userMessage = lastMessageContent;
|
||||||
|
} else {
|
||||||
|
throw new Error('last message content is undefined');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// select correct llm config
|
||||||
|
// user can set llm config via omagent.setDefaultLLM()
|
||||||
|
// or write "defaultLLM" in mcpconfig.json to specify
|
||||||
|
if (this._defaultLLM) {
|
||||||
|
loop.setLlmConfig({
|
||||||
|
baseUrl: this._defaultLLM.baseURL,
|
||||||
|
userToken: this._defaultLLM.apiToken,
|
||||||
|
userModel: this._defaultLLM.model,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// throw error to user and give the suggestion
|
||||||
|
throw new Error('default LLM is not set, please set it via omagent.setDefaultLLM() or write "defaultLLM" in mcpconfig.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
await loop.start(storage, userMessage);
|
||||||
|
|
||||||
|
// get response from last message in message list
|
||||||
|
const lastMessage = storage.messages.at(-1)?.content;
|
||||||
|
return lastMessage;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
export { routeMessage } from './common/router.js';
|
export { routeMessage } from './common/router.js';
|
||||||
export { VSCodeWebViewLike, TaskLoopAdapter, OmAgent, OmAgentConfiguration, UserMessage, AssistantMessage } from './hook/adapter.js';
|
export { VSCodeWebViewLike } from './hook/adapter.js';
|
||||||
export { setVscodeWorkspace, setRunningCWD } from './hook/setting.js';
|
export { setVscodeWorkspace, setRunningCWD } from './hook/setting.js';
|
||||||
export { clientMap } from './mcp/connect.service.js';
|
export { clientMap } from './mcp/connect.service.js';
|
@ -137,7 +137,7 @@ export class McpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取提示
|
// 获取提示
|
||||||
public async getPrompt(name: string, args: Record<string, any> = {}) {
|
public async getPrompt(name: string, args: Record<string, any> = {}) {
|
||||||
return await this.client.getPrompt({
|
return await this.client.getPrompt({
|
||||||
name, arguments: args
|
name, arguments: args
|
||||||
});
|
});
|
||||||
|
@ -268,24 +268,24 @@ export async function connectService(
|
|||||||
): Promise<RestfulResponse> {
|
): Promise<RestfulResponse> {
|
||||||
try {
|
try {
|
||||||
// 使用cli-table3创建美观的表格
|
// 使用cli-table3创建美观的表格
|
||||||
const table = new Table({
|
// const table = new Table({
|
||||||
head: ['Property', 'Value'],
|
// head: ['Property', 'Value'],
|
||||||
colWidths: [20, 60],
|
// colWidths: [20, 60],
|
||||||
style: {
|
// style: {
|
||||||
head: ['green'],
|
// head: ['green'],
|
||||||
border: ['grey']
|
// border: ['grey']
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
table.push(
|
// table.push(
|
||||||
['Connection Type', option.connectionType],
|
// ['Connection Type', option.connectionType],
|
||||||
['Command', option.command || 'N/A'],
|
// ['Command', option.command || 'N/A'],
|
||||||
['Arguments', option.args?.join(' ') || 'N/A'],
|
// ['Arguments', option.args?.join(' ') || 'N/A'],
|
||||||
['Working Directory', option.cwd || 'N/A'],
|
// ['Working Directory', option.cwd || 'N/A'],
|
||||||
['URL', option.url || 'N/A']
|
// ['URL', option.url || 'N/A']
|
||||||
);
|
// );
|
||||||
|
|
||||||
console.log(table.toString());
|
// console.log(table.toString());
|
||||||
|
|
||||||
// 预处理字符串
|
// 预处理字符串
|
||||||
await preprocessCommand(option, webview);
|
await preprocessCommand(option, webview);
|
||||||
|
1
service/src/sdk.ts
Normal file
1
service/src/sdk.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { TaskLoopAdapter, OmAgent, OmAgentConfiguration, UserMessage, AssistantMessage } from './hook/sdk.js';
|
39
service/task-loop.d.ts
vendored
39
service/task-loop.d.ts
vendored
@ -5,25 +5,25 @@ export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
|||||||
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
|
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
|
||||||
|
|
||||||
interface SchemaProperty {
|
interface SchemaProperty {
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
type: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputSchema {
|
interface InputSchema {
|
||||||
type: string;
|
type: string;
|
||||||
properties: Record<string, SchemaProperty>;
|
properties: Record<string, SchemaProperty>;
|
||||||
required?: string[];
|
required?: string[];
|
||||||
title?: string;
|
title?: string;
|
||||||
$defs?: any;
|
$defs?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToolItem {
|
interface ToolItem {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
inputSchema: InputSchema;
|
inputSchema: InputSchema;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
anyOf?: any;
|
anyOf?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IExtraInfo {
|
interface IExtraInfo {
|
||||||
@ -87,7 +87,7 @@ export interface ToolCall {
|
|||||||
export interface ToolCallContent {
|
export interface ToolCallContent {
|
||||||
type: string;
|
type: string;
|
||||||
text: string;
|
text: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolCallResult {
|
export interface ToolCallResult {
|
||||||
@ -189,6 +189,17 @@ interface ChatSetting {
|
|||||||
export class TaskLoop {
|
export class TaskLoop {
|
||||||
constructor(taskOptions?: TaskLoopOptions);
|
constructor(taskOptions?: TaskLoopOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description wait for connection
|
||||||
|
*/
|
||||||
|
waitConnection(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Set the task loop options
|
||||||
|
* @param taskOptions
|
||||||
|
*/
|
||||||
|
setTaskLoopOptions(taskOptions: TaskLoopOptions): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description make chat data
|
* @description make chat data
|
||||||
* @param tabStorage
|
* @param tabStorage
|
||||||
@ -294,7 +305,7 @@ export class TaskLoop {
|
|||||||
/**
|
/**
|
||||||
* @description Get prompt template from mcp server
|
* @description Get prompt template from mcp server
|
||||||
*/
|
*/
|
||||||
getPrompt(promptId: string, args: Record<string, any>): Promise<string>;
|
getPrompt(promptId: string, args?: Record<string, any>): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Get resource template from mcp server
|
* @description Get resource template from mcp server
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": false,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user