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",
"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
View File
@@ -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
View File
@@ -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();
}
}