feat: Integrate TypeScript and MCP tools for LangChain pipeline.
This commit is contained in:
1 parent
65a8898d14
commit
c348b4902e
3 files changed
+177
-34
No files matched your search
@@ -20,6 +20,7 @@
|
||||
"lefthook": "^1.11.13",
|
||||
"npm-check-updates": "^18.0.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^5.8.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -568,6 +569,8 @@
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
+6
-3
@@ -7,19 +7,22 @@
|
||||
"@typescript/native-preview": "^7.0.0-dev.20250523.1",
|
||||
"lefthook": "^1.11.13",
|
||||
"npm-check-updates": "^18.0.1",
|
||||
"npm-run-all": "^4.1.5"
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"________________ BUILD AND RUN ________________": "",
|
||||
"build": "npx tsgo",
|
||||
"build": "npx tsc",
|
||||
"build:go": "npx tsgo",
|
||||
"prebuild": "npx rimraf dist",
|
||||
"start": "node dist/index.js",
|
||||
"________________ FORMAT AND LINT ________________": "",
|
||||
"check": "npx run-p ts:check lint",
|
||||
"lint": "biome check",
|
||||
"lint:fix": "biome check --write",
|
||||
"ts:check": "tsgo --noEmit",
|
||||
"ts:check": "tsc --noEmit",
|
||||
"ts:check:go": "tsgo --noEmit",
|
||||
"________________ OTHER ________________": "",
|
||||
"postinstall": "npm run prepare",
|
||||
"prepare": "npx lefthook install",
|
||||
|
||||
+168
-31
@@ -7,9 +7,17 @@ import {
|
||||
import { Runnable } from '@langchain/core/runnables';
|
||||
import { DynamicStructuredTool } from '@langchain/core/tools';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import dotenv from 'dotenv'; // Импортируем dotenv для загрузки переменных окружения
|
||||
import Handlebars from 'handlebars';
|
||||
import { z } from 'zod';
|
||||
|
||||
// MCP SDK imports
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { Tool as McpTool, TextContent } from '@modelcontextprotocol/sdk/types';
|
||||
|
||||
dotenv.config(); // Загружаем переменные окружения из .env файла
|
||||
|
||||
interface PipelineStep {
|
||||
name: string;
|
||||
prompt: string;
|
||||
@@ -21,41 +29,160 @@ interface Pipeline {
|
||||
steps: PipelineStep[];
|
||||
}
|
||||
|
||||
class MockMcpToolsService {
|
||||
async getTools(): Promise<DynamicStructuredTool[]> {
|
||||
const echoTool = new DynamicStructuredTool({
|
||||
name: 'echo',
|
||||
description: 'Возвращает введенную строку.',
|
||||
schema: z.object({ input: z.string().describe('Строка для эхо.') }),
|
||||
func: async (input: { input: string }) => {
|
||||
return input.input;
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Сервис для взаимодействия с MCP сервером и предоставления инструментов LangChain.
|
||||
*/
|
||||
class McpToolsService {
|
||||
private mcpClient: Client | null = null;
|
||||
private transport: SSEClientTransport | null = null;
|
||||
private readonly serverUrl: string;
|
||||
|
||||
const getCurrentDateTool = new DynamicStructuredTool({
|
||||
name: 'get_current_date',
|
||||
description: 'Возвращает текущую дату в формате ISO.',
|
||||
schema: z.object({}),
|
||||
func: async () => {
|
||||
return new Date().toISOString();
|
||||
},
|
||||
});
|
||||
constructor(serverUrl: string) {
|
||||
this.serverUrl = serverUrl;
|
||||
}
|
||||
|
||||
return [echoTool, getCurrentDateTool];
|
||||
/**
|
||||
* Устанавливает соединение с MCP сервером.
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.mcpClient && this.transport) {
|
||||
return; // Уже подключено
|
||||
}
|
||||
try {
|
||||
this.transport = new SSEClientTransport(new URL(this.serverUrl));
|
||||
this.mcpClient = new Client({
|
||||
name: 'my-langchain-mcp-client',
|
||||
version: '1.0.0',
|
||||
});
|
||||
await this.mcpClient.connect(this.transport);
|
||||
console.log(`[MCP Service] Подключено к MCP серверу: ${this.serverUrl}`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[MCP Service] Ошибка подключения к MCP серверу ${this.serverUrl}:`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрывает соединение с MCP сервером.
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.mcpClient) {
|
||||
await this.mcpClient.close();
|
||||
this.mcpClient = null;
|
||||
this.transport = null;
|
||||
console.log(`[MCP Service] Отключено от MCP сервера: ${this.serverUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список инструментов с MCP сервера и преобразует их в формат LangChain.
|
||||
* @returns Массив инструментов LangChain.
|
||||
*/
|
||||
async getLangchainTools(): Promise<DynamicStructuredTool[]> {
|
||||
if (!this.mcpClient) {
|
||||
throw new Error('MCP клиент не подключен. Сначала вызовите .connect()');
|
||||
}
|
||||
|
||||
const mcpTools: McpTool[] = (await this.mcpClient.listTools()).tools;
|
||||
const langchainTools: DynamicStructuredTool[] = [];
|
||||
|
||||
for (const mcpTool of mcpTools) {
|
||||
// Простая конвертация JSON Schema в Zod schema.
|
||||
// Для сложных схем рекомендуется использовать специализированную библиотеку (например, json-schema-to-zod).
|
||||
const properties: Record<string, z.ZodTypeAny> = {};
|
||||
for (const key in mcpTool.inputSchema.properties) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
const prop = mcpTool.inputSchema.properties[key] as any;
|
||||
let schemaType: z.ZodTypeAny;
|
||||
switch (prop.type) {
|
||||
case 'string':
|
||||
schemaType = z.string();
|
||||
break;
|
||||
case 'number':
|
||||
schemaType = z.number();
|
||||
break;
|
||||
case 'boolean':
|
||||
schemaType = z.boolean();
|
||||
break;
|
||||
case 'array':
|
||||
// Для массивов без явного типа элементов, используем z.array(z.any())
|
||||
schemaType = z.array(z.any());
|
||||
break;
|
||||
case 'object':
|
||||
// Для объектов без явной вложенной схемы, используем z.object({})
|
||||
schemaType = z.object({});
|
||||
break;
|
||||
default:
|
||||
schemaType = z.any();
|
||||
}
|
||||
if (!(mcpTool.inputSchema.required || []).includes(key)) {
|
||||
schemaType = schemaType.optional();
|
||||
}
|
||||
properties[key] = schemaType.describe(prop.description || '');
|
||||
}
|
||||
const zodSchema = z.object(properties);
|
||||
|
||||
const langchainTool = new DynamicStructuredTool({
|
||||
name: mcpTool.name,
|
||||
description: mcpTool.description || '',
|
||||
schema: zodSchema,
|
||||
func: async (args: Record<string, unknown>) => {
|
||||
console.log(
|
||||
`[MCP Tool Call] Вызов инструмента MCP: ${mcpTool.name} с аргументами:`,
|
||||
args,
|
||||
);
|
||||
const result = await this.mcpClient!.callTool({
|
||||
name: mcpTool.name,
|
||||
arguments: args,
|
||||
});
|
||||
|
||||
if (result.isError) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
const errorContent = (result.content as any[])
|
||||
.map((c) => (c as TextContent).text || '')
|
||||
.join('\n');
|
||||
throw new Error(
|
||||
`Ошибка выполнения инструмента ${mcpTool.name}: ${errorContent}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Предпочтительно возвращаем структурированный контент, если он есть
|
||||
if (result.structuredContent) {
|
||||
return result.structuredContent;
|
||||
}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
return (result.content as any[])
|
||||
.map((c) => {
|
||||
if (c.type === 'text') return (c as TextContent).text;
|
||||
if (c.type === 'image') return `[Изображение: ${c.mimeType}]`;
|
||||
if (c.type === 'audio') return `[Аудио: ${c.mimeType}]`;
|
||||
if (c.type === 'resource') return `[Ресурс: ${c.uri}]`;
|
||||
return JSON.stringify(c); // Запасной вариант для других типов
|
||||
})
|
||||
.join('\n');
|
||||
},
|
||||
});
|
||||
langchainTools.push(langchainTool);
|
||||
}
|
||||
return langchainTools;
|
||||
}
|
||||
}
|
||||
|
||||
class PipelineExecutor {
|
||||
private readonly mcpToolsService: MockMcpToolsService;
|
||||
readonly mcpToolsService: McpToolsService;
|
||||
private cachedTools: DynamicStructuredTool[] | null = null;
|
||||
|
||||
constructor() {
|
||||
this.mcpToolsService = new MockMcpToolsService();
|
||||
constructor(mcpServerUrl: string) {
|
||||
this.mcpToolsService = new McpToolsService(mcpServerUrl);
|
||||
}
|
||||
|
||||
private async _getOrLoadTools(): Promise<DynamicStructuredTool[]> {
|
||||
if (!this.cachedTools) {
|
||||
this.cachedTools = await this.mcpToolsService.getTools();
|
||||
await this.mcpToolsService.connect(); // Устанавливаем соединение с MCP сервером
|
||||
this.cachedTools = await this.mcpToolsService.getLangchainTools(); // Получаем инструменты LangChain
|
||||
}
|
||||
return this.cachedTools;
|
||||
}
|
||||
@@ -133,11 +260,13 @@ class PipelineExecutor {
|
||||
|
||||
for (const toolCall of response.tool_calls) {
|
||||
try {
|
||||
const tools = await this._getOrLoadTools();
|
||||
const tools = await this._getOrLoadTools(); // Получаем LangChain-совместимые инструменты
|
||||
const tool = tools.find((t) => t.name === toolCall.name);
|
||||
|
||||
if (!tool) {
|
||||
console.error(`Инструмент ${toolCall.name} не найден.`);
|
||||
console.error(
|
||||
`Инструмент ${toolCall.name} не найден в списке LangChain инструментов.`,
|
||||
);
|
||||
messages.push(
|
||||
new ToolMessage({
|
||||
tool_call_id: toolCall.id!,
|
||||
@@ -147,11 +276,12 @@ class PipelineExecutor {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Выполняем функцию LangChain инструмента, которая внутри вызывает MCP сервер
|
||||
const toolResult = await tool.func(toolCall.args);
|
||||
messages.push(
|
||||
new ToolMessage({
|
||||
tool_call_id: toolCall.id!,
|
||||
content: JSON.stringify(toolResult),
|
||||
content: JSON.stringify(toolResult), // LangChain ожидает строковый контент здесь
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -174,8 +304,10 @@ class PipelineExecutor {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
require('dotenv').config();
|
||||
const openAIApiKey = process.env.OPENAI_API_KEY;
|
||||
// Используйте переменную окружения для URL сервера MCP, с запасным вариантом по умолчанию
|
||||
const mcpServerUrl =
|
||||
process.env.MCP_SERVER_URL || 'https://santiment-mcp.dev.mind-dev.com/sse';
|
||||
|
||||
if (!openAIApiKey) {
|
||||
console.error(
|
||||
@@ -184,7 +316,7 @@ async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pipelineExecutor = new PipelineExecutor();
|
||||
const pipelineExecutor = new PipelineExecutor(mcpServerUrl); // Передаем URL сервера
|
||||
|
||||
const testPipeline: Pipeline = {
|
||||
description: 'Тестовый пайплайн',
|
||||
@@ -193,7 +325,7 @@ async function main() {
|
||||
steps: [
|
||||
{
|
||||
name: 'Приветствие',
|
||||
prompt: 'Скажи привет, используя имя {{username}}.',
|
||||
prompt: 'Скажи привет пользователю, его имя - {{username}}.',
|
||||
},
|
||||
{
|
||||
name: 'Использование инструмента',
|
||||
@@ -204,12 +336,15 @@ async function main() {
|
||||
name: 'Получение даты',
|
||||
prompt: 'Какая сейчас дата? Используй инструмент `get_current_date`.',
|
||||
},
|
||||
{ name: 'Завершение', prompt: 'Напиши резюме нашего с тобой диалога' },
|
||||
{
|
||||
name: 'Завершение',
|
||||
prompt: 'Напомни как зовут пользователя? (из прошлых сообщений)',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const variables = {
|
||||
username: 'Пользователь',
|
||||
username: 'Вася',
|
||||
};
|
||||
|
||||
const systemPrompts = ['Отвечай кратко и по существу.'];
|
||||
@@ -224,6 +359,8 @@ async function main() {
|
||||
console.log('Результаты выполнения пайплайна:', results);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при выполнении пайплайна:', error);
|
||||
} finally {
|
||||
await pipelineExecutor.mcpToolsService.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user