diff --git a/core/langchain-decorator/src/decorator/GraphTool.ts b/core/langchain-decorator/src/decorator/GraphTool.ts index a3d24571..3388661e 100644 --- a/core/langchain-decorator/src/decorator/GraphTool.ts +++ b/core/langchain-decorator/src/decorator/GraphTool.ts @@ -28,3 +28,5 @@ export interface IGraphTool { execute: DynamicStructuredTool['func']; } +export type IGraphStructuredTool = DynamicStructuredTool[0]>; + diff --git a/core/langchain-decorator/src/util/GraphToolInfoUtil.ts b/core/langchain-decorator/src/util/GraphToolInfoUtil.ts index 25f433f2..327a4a43 100644 --- a/core/langchain-decorator/src/util/GraphToolInfoUtil.ts +++ b/core/langchain-decorator/src/util/GraphToolInfoUtil.ts @@ -4,11 +4,17 @@ import { GRAPH_TOOL_METADATA } from '../type/metadataKey'; import type { IGraphToolMetadata } from '../model/GraphToolMetadata'; export class GraphToolInfoUtil { + static graphToolMap = new Map(); static setGraphToolMetadata(metadata: IGraphToolMetadata, clazz: EggProtoImplClass) { MetadataUtil.defineMetaData(GRAPH_TOOL_METADATA, metadata, clazz); + GraphToolInfoUtil.graphToolMap.set(clazz, metadata); } static getGraphToolMetadata(clazz: EggProtoImplClass): IGraphToolMetadata | undefined { return MetadataUtil.getMetaData(GRAPH_TOOL_METADATA, clazz); } + + static getAllGraphToolMetadata(): Map { + return GraphToolInfoUtil.graphToolMap; + } } diff --git a/core/mcp-client/package.json b/core/mcp-client/package.json index 23bb93af..38d44fa2 100644 --- a/core/mcp-client/package.json +++ b/core/mcp-client/package.json @@ -37,6 +37,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.23.0", + "@langchain/mcp-adapters": "^1.0.0", "sdk-base": "^5.0.1", "urllib": "^4.6.11" }, diff --git a/core/mcp-client/src/HttpMCPClient.ts b/core/mcp-client/src/HttpMCPClient.ts index 567a40b3..f9ccb980 100644 --- a/core/mcp-client/src/HttpMCPClient.ts +++ b/core/mcp-client/src/HttpMCPClient.ts @@ -6,6 +6,7 @@ import { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js'; import { fetch } from 'urllib'; import { mergeHeaders } from './HeaderUtil'; import type { Logger } from '@eggjs/tegg'; +import { loadMcpTools } from '@langchain/mcp-adapters'; export interface BaseHttpClientOptions extends ClientOptions { logger: Logger; fetch?: typeof fetch; @@ -30,12 +31,14 @@ export class HttpMCPClient extends Client { #transport: SSEClientTransport | StreamableHTTPClientTransport; #fetch: typeof fetch; url: string; + clientInfo: Implementation; constructor(clientInfo: Implementation, options: HttpClientOptions) { super(clientInfo, options); this.options = options; this.#fetch = options.fetch ?? fetch; this.logger = options.logger; this.url = options.url; + this.clientInfo = clientInfo; } async #buildSSESTransport() { const self = this; @@ -113,4 +116,11 @@ export class HttpMCPClient extends Client { } await this.connect(this.#transport, this.options.requestOptions); } + async getLangChainTool() { + return await loadMcpTools(this.clientInfo.name, this as any, { + throwOnLoadError: true, + prefixToolNameWithServerName: false, + additionalToolNamePrefix: '', + }); + } } diff --git a/plugin/controller/test/fixtures/apps/mcp-app/app/middleware/tracelog.js b/plugin/controller/test/fixtures/apps/mcp-app/app/middleware/tracelog.js index c3679a00..799e8203 100644 --- a/plugin/controller/test/fixtures/apps/mcp-app/app/middleware/tracelog.js +++ b/plugin/controller/test/fixtures/apps/mcp-app/app/middleware/tracelog.js @@ -3,6 +3,8 @@ module.exports = () => { return async function tracelog(ctx, next) { ctx.req.headers.trace = 'middleware'; + ctx.req.rawHeaders.push('trace'); + ctx.req.rawHeaders.push('middleware'); await next(); }; }; diff --git a/plugin/controller/test/http/request.test.ts b/plugin/controller/test/http/request.test.ts index b3171ff5..d4701678 100644 --- a/plugin/controller/test/http/request.test.ts +++ b/plugin/controller/test/http/request.test.ts @@ -30,7 +30,7 @@ describe('plugin/controller/test/http/request.test.ts', () => { }); const [ nodeMajor ] = process.versions.node.split('.').map(v => Number(v)); if (nodeMajor >= 16) { - it.only('Request should work', async () => { + it('Request should work', async () => { app.mockCsrf(); const param = { name: 'foo', diff --git a/plugin/langchain/lib/graph/GraphLoadUnitHook.ts b/plugin/langchain/lib/graph/GraphLoadUnitHook.ts index 0a09259f..7c595a59 100644 --- a/plugin/langchain/lib/graph/GraphLoadUnitHook.ts +++ b/plugin/langchain/lib/graph/GraphLoadUnitHook.ts @@ -1,21 +1,30 @@ -import { EggProtoImplClass, LifecycleHook } from '@eggjs/tegg'; +import { AccessLevel, EggProtoImplClass, LifecycleHook, LifecyclePostInject, MCPInfoUtil, SingletonProto } from '@eggjs/tegg'; import { + ClassProtoDescriptor, + EggPrototypeCreatorFactory, EggPrototypeFactory, + EggPrototypeWithClazz, LoadUnit, LoadUnitLifecycleContext, + ProtoDescriptorHelper, } from '@eggjs/tegg-metadata'; import { CompiledStateGraphProto } from './CompiledStateGraphProto'; -import { GraphInfoUtil, IGraphMetadata } from '@eggjs/tegg-langchain-decorator'; +import { GraphInfoUtil, GraphToolInfoUtil, IGraphMetadata, IGraphTool, IGraphToolMetadata } from '@eggjs/tegg-langchain-decorator'; import assert from 'node:assert'; +import { EggContainerFactory } from '@eggjs/tegg-runtime'; +import { DynamicStructuredTool } from 'langchain'; +import * as z from 'zod/v4'; export class GraphLoadUnitHook implements LifecycleHook { private readonly eggPrototypeFactory: EggPrototypeFactory; clazzMap: Map; graphCompiledNameMap: Map = new Map(); + tools: Map; constructor(eggPrototypeFactory: EggPrototypeFactory) { this.eggPrototypeFactory = eggPrototypeFactory; this.clazzMap = GraphInfoUtil.getAllGraphMetadata(); + this.tools = GraphToolInfoUtil.getAllGraphToolMetadata(); } async preCreate(ctx: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise { @@ -30,7 +39,41 @@ export class GraphLoadUnitHook implements LifecycleHook).execute.bind(toolsObj.obj), + schema: z.object(ToolDetail.argsSchema) as any, + }); + Object.setPrototypeOf(this, tool); + } else { + throw new Error(`graph tool ${toolMeta.name ?? clazz.name} not found`); + } + } } + SingletonProto({ name: `structured${toolMeta.name ?? clazz.name}`, accessLevel: AccessLevel.PUBLIC })(StructuredTool); + return StructuredTool; } async postCreate(_ctx: LoadUnitLifecycleContext, obj: LoadUnit): Promise { diff --git a/plugin/langchain/package.json b/plugin/langchain/package.json index da6282f8..d5c5ca65 100644 --- a/plugin/langchain/package.json +++ b/plugin/langchain/package.json @@ -74,7 +74,8 @@ "koa-compose": "^3.2.1", "langchain": "^1.1.2", "sdk-base": "^4.2.0", - "urllib": "^4.4.0" + "urllib": "^4.4.0", + "zod": "^4.0.0" }, "devDependencies": { "@eggjs/module-test-util": "^3.68.0", diff --git a/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts b/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts index 5c20bf76..af48cab2 100644 --- a/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts +++ b/plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts @@ -4,10 +4,10 @@ import { HTTPMethodEnum, Inject, } from '@eggjs/tegg'; -import { ChatModelQualifier, TeggBoundModel, TeggCompiledStateGraph } from '@eggjs/tegg-langchain-decorator'; +import { ChatModelQualifier, IGraphStructuredTool, TeggBoundModel, TeggCompiledStateGraph } from '@eggjs/tegg-langchain-decorator'; import { ChatOpenAIModel } from '../../../../../../../../lib/ChatOpenAI'; import { BoundChatModel } from '../service/BoundChatModel'; -import { FooGraph } from '../service/Graph'; +import { FooGraph, FooTool } from '../service/Graph'; import { AIMessage } from 'langchain'; @HTTPController({ @@ -24,6 +24,9 @@ export class AppController { @Inject() compiledFooGraph: TeggCompiledStateGraph; + @Inject() + structuredFooTool: IGraphStructuredTool; + @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello', @@ -59,4 +62,12 @@ export class AppController { }, ''), }; } + + @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/structured' }) + async structured() { + return { + name: this.structuredFooTool.name, + description: this.structuredFooTool.description, + }; + } } diff --git a/plugin/langchain/test/llm.test.ts b/plugin/langchain/test/llm.test.ts index eca27953..31bc5caa 100644 --- a/plugin/langchain/test/llm.test.ts +++ b/plugin/langchain/test/llm.test.ts @@ -76,5 +76,15 @@ describe('plugin/langchain/test/llm.test.ts', () => { .get('/llm/graph') .expect(200, { value: 'hello graph toolhello world' }); }); + + it('should structured work', async () => { + const res = await app.httpRequest() + .get('/llm/structured') + .expect(200); + assert.deepStrictEqual(res.body, { + name: 'search', + description: 'Call the foo tool', + }); + }); } }); diff --git a/plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts b/plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts index ce5322a8..177e7cdd 100644 --- a/plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts +++ b/plugin/mcp-client/test/fixtures/apps/mcpclient/app/modules/bar/controller/AppController.ts @@ -63,4 +63,20 @@ export class AppController { const res = await client.listTools(); return res; } + + @HTTPMethod({ + method: HTTPMethodEnum.GET, + path: '/hello-langchain-tools', + }) + async langchainTools() { + const tools = await this.mcpClient.getLangChainTool(); + return { + length: tools.length, + tools: tools.map(tool => ({ + name: tool.name, + description: tool.description, + schema: tool.schema, + })), + }; + } } diff --git a/plugin/mcp-client/test/mcpclient.test.ts b/plugin/mcp-client/test/mcpclient.test.ts index 946f0b0a..f0be0049 100644 --- a/plugin/mcp-client/test/mcpclient.test.ts +++ b/plugin/mcp-client/test/mcpclient.test.ts @@ -136,5 +136,36 @@ describe('plugin/mcp-client/test/mcpclient.test.ts', () => { ], }); }); + + it('should langchain tools work', async () => { + const res = await app.httpRequest() + .get('/mcpclient/hello-langchain-tools') + .expect(200); + assert.deepStrictEqual(res.body, { + length: 1, + tools: [ + { + name: 'add', + description: '', + schema: { + type: 'object', + properties: { + a: { + type: 'number', + }, + b: { + type: 'number', + }, + }, + required: [ + 'a', + 'b', + ], + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + ], + }); + }); } });