feat: Integrate TypeScript and MCP tools for LangChain pipeline.

This commit is contained in:
uxname committed 2025-05-24 15:52:17 +03:00
1 parent 65a8898d14
commit c348b4902e
3 files changed
+177 -34

No files matched your search

+3
View File
@@ -20,6 +20,7 @@
"lefthook": "^1.11.13", "lefthook": "^1.11.13",
"npm-check-updates": "^18.0.1", "npm-check-updates": "^18.0.1",
"npm-run-all": "^4.1.5", "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=="], "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=="], "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=="], "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
View File
@@ -7,19 +7,22 @@
"@typescript/native-preview": "^7.0.0-dev.20250523.1", "@typescript/native-preview": "^7.0.0-dev.20250523.1",
"lefthook": "^1.11.13", "lefthook": "^1.11.13",
"npm-check-updates": "^18.0.1", "npm-check-updates": "^18.0.1",
"npm-run-all": "^4.1.5" "npm-run-all": "^4.1.5",
"typescript": "^5.8.3"
}, },
"private": true, "private": true,
"scripts": { "scripts": {
"________________ BUILD AND RUN ________________": "", "________________ BUILD AND RUN ________________": "",
"build": "npx tsgo", "build": "npx tsc",
"build:go": "npx tsgo",
"prebuild": "npx rimraf dist", "prebuild": "npx rimraf dist",
"start": "node dist/index.js", "start": "node dist/index.js",
"________________ FORMAT AND LINT ________________": "", "________________ FORMAT AND LINT ________________": "",
"check": "npx run-p ts:check lint", "check": "npx run-p ts:check lint",
"lint": "biome check", "lint": "biome check",
"lint:fix": "biome check --write", "lint:fix": "biome check --write",
"ts:check": "tsgo --noEmit", "ts:check": "tsc --noEmit",
"ts:check:go": "tsgo --noEmit",
"________________ OTHER ________________": "", "________________ OTHER ________________": "",
"postinstall": "npm run prepare", "postinstall": "npm run prepare",
"prepare": "npx lefthook install", "prepare": "npx lefthook install",
+168 -31
View File
@@ -7,9 +7,17 @@ import {
import { Runnable } from '@langchain/core/runnables'; import { Runnable } from '@langchain/core/runnables';
import { DynamicStructuredTool } from '@langchain/core/tools'; import { DynamicStructuredTool } from '@langchain/core/tools';
import { ChatOpenAI } from '@langchain/openai'; import { ChatOpenAI } from '@langchain/openai';
import dotenv from 'dotenv'; // Импортируем dotenv для загрузки переменных окружения
import Handlebars from 'handlebars'; import Handlebars from 'handlebars';
import { z } from 'zod'; 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 { interface PipelineStep {
name: string; name: string;
prompt: string; prompt: string;
@@ -21,41 +29,160 @@ interface Pipeline {
steps: PipelineStep[]; steps: PipelineStep[];
} }
class MockMcpToolsService { /**
async getTools(): Promise<DynamicStructuredTool[]> { * Сервис для взаимодействия с MCP сервером и предоставления инструментов LangChain.
const echoTool = new DynamicStructuredTool({ */
name: 'echo', class McpToolsService {
description: 'Возвращает введенную строку.', private mcpClient: Client | null = null;
schema: z.object({ input: z.string().describe('Строка для эхо.') }), private transport: SSEClientTransport | null = null;
func: async (input: { input: string }) => { private readonly serverUrl: string;
return input.input;
},
});
const getCurrentDateTool = new DynamicStructuredTool({ constructor(serverUrl: string) {
name: 'get_current_date', this.serverUrl = serverUrl;
description: 'Возвращает текущую дату в формате ISO.', }
schema: z.object({}),
func: async () => {
return new Date().toISOString();
},
});
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 { class PipelineExecutor {
private readonly mcpToolsService: MockMcpToolsService; readonly mcpToolsService: McpToolsService;
private cachedTools: DynamicStructuredTool[] | null = null; private cachedTools: DynamicStructuredTool[] | null = null;
constructor() { constructor(mcpServerUrl: string) {
this.mcpToolsService = new MockMcpToolsService(); this.mcpToolsService = new McpToolsService(mcpServerUrl);
} }
private async _getOrLoadTools(): Promise<DynamicStructuredTool[]> { private async _getOrLoadTools(): Promise<DynamicStructuredTool[]> {
if (!this.cachedTools) { if (!this.cachedTools) {
this.cachedTools = await this.mcpToolsService.getTools(); await this.mcpToolsService.connect(); // Устанавливаем соединение с MCP сервером
this.cachedTools = await this.mcpToolsService.getLangchainTools(); // Получаем инструменты LangChain
} }
return this.cachedTools; return this.cachedTools;
} }
@@ -133,11 +260,13 @@ class PipelineExecutor {
for (const toolCall of response.tool_calls) { for (const toolCall of response.tool_calls) {
try { try {
const tools = await this._getOrLoadTools(); const tools = await this._getOrLoadTools(); // Получаем LangChain-совместимые инструменты
const tool = tools.find((t) => t.name === toolCall.name); const tool = tools.find((t) => t.name === toolCall.name);
if (!tool) { if (!tool) {
console.error(`Инструмент ${toolCall.name} не найден.`); console.error(
`Инструмент ${toolCall.name} не найден в списке LangChain инструментов.`,
);
messages.push( messages.push(
new ToolMessage({ new ToolMessage({
tool_call_id: toolCall.id!, tool_call_id: toolCall.id!,
@@ -147,11 +276,12 @@ class PipelineExecutor {
continue; continue;
} }
// Выполняем функцию LangChain инструмента, которая внутри вызывает MCP сервер
const toolResult = await tool.func(toolCall.args); const toolResult = await tool.func(toolCall.args);
messages.push( messages.push(
new ToolMessage({ new ToolMessage({
tool_call_id: toolCall.id!, tool_call_id: toolCall.id!,
content: JSON.stringify(toolResult), content: JSON.stringify(toolResult), // LangChain ожидает строковый контент здесь
}), }),
); );
} catch (error) { } catch (error) {
@@ -174,8 +304,10 @@ class PipelineExecutor {
} }
async function main() { async function main() {
require('dotenv').config();
const openAIApiKey = process.env.OPENAI_API_KEY; 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) { if (!openAIApiKey) {
console.error( console.error(
@@ -184,7 +316,7 @@ async function main() {
process.exit(1); process.exit(1);
} }
const pipelineExecutor = new PipelineExecutor(); const pipelineExecutor = new PipelineExecutor(mcpServerUrl); // Передаем URL сервера
const testPipeline: Pipeline = { const testPipeline: Pipeline = {
description: 'Тестовый пайплайн', description: 'Тестовый пайплайн',
@@ -193,7 +325,7 @@ async function main() {
steps: [ steps: [
{ {
name: 'Приветствие', name: 'Приветствие',
prompt: 'Скажи привет, используя имя {{username}}.', prompt: 'Скажи привет пользователю, его имя - {{username}}.',
}, },
{ {
name: 'Использование инструмента', name: 'Использование инструмента',
@@ -204,12 +336,15 @@ async function main() {
name: 'Получение даты', name: 'Получение даты',
prompt: 'Какая сейчас дата? Используй инструмент `get_current_date`.', prompt: 'Какая сейчас дата? Используй инструмент `get_current_date`.',
}, },
{ name: 'Завершение', prompt: 'Напиши резюме нашего с тобой диалога' }, {
name: 'Завершение',
prompt: 'Напомни как зовут пользователя? (из прошлых сообщений)',
},
], ],
}; };
const variables = { const variables = {
username: 'Пользователь', username: 'Вася',
}; };
const systemPrompts = ['Отвечай кратко и по существу.']; const systemPrompts = ['Отвечай кратко и по существу.'];
@@ -224,6 +359,8 @@ async function main() {
console.log('Результаты выполнения пайплайна:', results); console.log('Результаты выполнения пайплайна:', results);
} catch (error) { } catch (error) {
console.error('Ошибка при выполнении пайплайна:', error); console.error('Ошибка при выполнении пайплайна:', error);
} finally {
await pipelineExecutor.mcpToolsService.disconnect();
} }
} }