Skip to content

Commit 56c23ad

Browse files
authored
feat: add structured tool (#387)
<!-- Thank you for your pull request. Please review below requirements. Bug fixes and new features should include tests and possibly benchmarks. Contributors guide: https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md 感谢您贡献代码。请确认下列 checklist 的完成情况。 Bug 修复和新功能必须包含测试,必要时请附上性能测试。 Contributors guide: https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md --> ##### Checklist <!-- Remove items that do not apply. For completed items, change [ ] to [x]. --> - [ ] `npm test` passes - [ ] tests and/or benchmarks are included - [ ] documentation is changed or added - [ ] commit message follows commit guidelines ##### Affected core subsystem(s) <!-- Provide affected core subsystem(s). --> ##### Description of change <!-- Provide a description of the change below this comment. --> <!-- - any feature? - close https://github.com/eggjs/egg/ISSUE_URL --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Structured graph tool support and new endpoints exposing structured tool metadata and LangChain tools. * **Dependencies** * Added runtime libraries to enable LangChain/MCP tool integration and schema validation. * **Tests** * New tests validating structured tool behavior and LangChain tool integration. * **Enhancements** * Middleware now also records raw HTTP header data for tracing. * **Bug Fixes** * Adjusted graph prototype injection behavior affecting tool injection timing. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 60a4cba commit 56c23ad

File tree

13 files changed

+140
-8
lines changed

13 files changed

+140
-8
lines changed

core/langchain-decorator/src/decorator/GraphTool.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ export interface IGraphTool<ToolSchema = ToolSchemaBase> {
2828
execute: DynamicStructuredTool<ToolSchema>['func'];
2929
}
3030

31+
export type IGraphStructuredTool<T extends IGraphTool> = DynamicStructuredTool<Parameters<T['execute']>[0]>;
32+

core/langchain-decorator/src/util/GraphToolInfoUtil.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@ import { GRAPH_TOOL_METADATA } from '../type/metadataKey';
44
import type { IGraphToolMetadata } from '../model/GraphToolMetadata';
55

66
export class GraphToolInfoUtil {
7+
static graphToolMap = new Map<EggProtoImplClass, IGraphToolMetadata>();
78
static setGraphToolMetadata(metadata: IGraphToolMetadata, clazz: EggProtoImplClass) {
89
MetadataUtil.defineMetaData(GRAPH_TOOL_METADATA, metadata, clazz);
10+
GraphToolInfoUtil.graphToolMap.set(clazz, metadata);
911
}
1012

1113
static getGraphToolMetadata(clazz: EggProtoImplClass): IGraphToolMetadata | undefined {
1214
return MetadataUtil.getMetaData(GRAPH_TOOL_METADATA, clazz);
1315
}
16+
17+
static getAllGraphToolMetadata(): Map<EggProtoImplClass, IGraphToolMetadata> {
18+
return GraphToolInfoUtil.graphToolMap;
19+
}
1420
}

core/mcp-client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
},
3838
"dependencies": {
3939
"@modelcontextprotocol/sdk": "^1.23.0",
40+
"@langchain/mcp-adapters": "^1.0.0",
4041
"sdk-base": "^5.0.1",
4142
"urllib": "^4.6.11"
4243
},

core/mcp-client/src/HttpMCPClient.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
66
import { fetch } from 'urllib';
77
import { mergeHeaders } from './HeaderUtil';
88
import type { Logger } from '@eggjs/tegg';
9+
import { loadMcpTools } from '@langchain/mcp-adapters';
910
export interface BaseHttpClientOptions extends ClientOptions {
1011
logger: Logger;
1112
fetch?: typeof fetch;
@@ -30,12 +31,14 @@ export class HttpMCPClient extends Client {
3031
#transport: SSEClientTransport | StreamableHTTPClientTransport;
3132
#fetch: typeof fetch;
3233
url: string;
34+
clientInfo: Implementation;
3335
constructor(clientInfo: Implementation, options: HttpClientOptions) {
3436
super(clientInfo, options);
3537
this.options = options;
3638
this.#fetch = options.fetch ?? fetch;
3739
this.logger = options.logger;
3840
this.url = options.url;
41+
this.clientInfo = clientInfo;
3942
}
4043
async #buildSSESTransport() {
4144
const self = this;
@@ -113,4 +116,11 @@ export class HttpMCPClient extends Client {
113116
}
114117
await this.connect(this.#transport, this.options.requestOptions);
115118
}
119+
async getLangChainTool() {
120+
return await loadMcpTools(this.clientInfo.name, this as any, {
121+
throwOnLoadError: true,
122+
prefixToolNameWithServerName: false,
123+
additionalToolNamePrefix: '',
124+
});
125+
}
116126
}

plugin/controller/test/fixtures/apps/mcp-app/app/middleware/tracelog.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
module.exports = () => {
44
return async function tracelog(ctx, next) {
55
ctx.req.headers.trace = 'middleware';
6+
ctx.req.rawHeaders.push('trace');
7+
ctx.req.rawHeaders.push('middleware');
68
await next();
79
};
810
};

plugin/controller/test/http/request.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('plugin/controller/test/http/request.test.ts', () => {
3030
});
3131
const [ nodeMajor ] = process.versions.node.split('.').map(v => Number(v));
3232
if (nodeMajor >= 16) {
33-
it.only('Request should work', async () => {
33+
it('Request should work', async () => {
3434
app.mockCsrf();
3535
const param = {
3636
name: 'foo',

plugin/langchain/lib/graph/GraphLoadUnitHook.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
1-
import { EggProtoImplClass, LifecycleHook } from '@eggjs/tegg';
1+
import { AccessLevel, EggProtoImplClass, LifecycleHook, LifecyclePostInject, MCPInfoUtil, SingletonProto } from '@eggjs/tegg';
22
import {
3+
ClassProtoDescriptor,
4+
EggPrototypeCreatorFactory,
35
EggPrototypeFactory,
6+
EggPrototypeWithClazz,
47
LoadUnit,
58
LoadUnitLifecycleContext,
9+
ProtoDescriptorHelper,
610
} from '@eggjs/tegg-metadata';
711
import { CompiledStateGraphProto } from './CompiledStateGraphProto';
8-
import { GraphInfoUtil, IGraphMetadata } from '@eggjs/tegg-langchain-decorator';
12+
import { GraphInfoUtil, GraphToolInfoUtil, IGraphMetadata, IGraphTool, IGraphToolMetadata } from '@eggjs/tegg-langchain-decorator';
913
import assert from 'node:assert';
14+
import { EggContainerFactory } from '@eggjs/tegg-runtime';
15+
import { DynamicStructuredTool } from 'langchain';
16+
import * as z from 'zod/v4';
1017

1118
export class GraphLoadUnitHook implements LifecycleHook<LoadUnitLifecycleContext, LoadUnit> {
1219
private readonly eggPrototypeFactory: EggPrototypeFactory;
1320
clazzMap: Map<EggProtoImplClass, IGraphMetadata>;
1421
graphCompiledNameMap: Map<string, CompiledStateGraphProto> = new Map();
22+
tools: Map<EggProtoImplClass, IGraphToolMetadata>;
1523

1624
constructor(eggPrototypeFactory: EggPrototypeFactory) {
1725
this.eggPrototypeFactory = eggPrototypeFactory;
1826
this.clazzMap = GraphInfoUtil.getAllGraphMetadata();
27+
this.tools = GraphToolInfoUtil.getAllGraphToolMetadata();
1928
}
2029

2130
async preCreate(ctx: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise<void> {
@@ -30,7 +39,41 @@ export class GraphLoadUnitHook implements LifecycleHook<LoadUnitLifecycleContext
3039
this.eggPrototypeFactory.registerPrototype(proto, loadUnit);
3140
this.graphCompiledNameMap.set(protoName, proto);
3241
}
42+
const toolMeta = this.tools.get(clazz as EggProtoImplClass);
43+
if (toolMeta) {
44+
const StructuredTool = this.createStructuredTool(clazz, toolMeta);
45+
const protoDescriptor = ProtoDescriptorHelper.createByInstanceClazz(StructuredTool, {
46+
moduleName: loadUnit.name,
47+
unitPath: loadUnit.unitPath,
48+
}) as ClassProtoDescriptor;
49+
const proto = await EggPrototypeCreatorFactory.createProtoByDescriptor(protoDescriptor, loadUnit);
50+
this.eggPrototypeFactory.registerPrototype(proto, loadUnit);
51+
}
52+
}
53+
}
54+
55+
createStructuredTool(clazz: EggProtoImplClass, toolMeta: IGraphToolMetadata) {
56+
class StructuredTool {
57+
@LifecyclePostInject()
58+
async init() {
59+
const toolsObj = await EggContainerFactory.getOrCreateEggObjectFromClazz(clazz);
60+
const toolMetadata = GraphToolInfoUtil.getGraphToolMetadata((toolsObj.proto as unknown as EggPrototypeWithClazz).clazz!);
61+
const ToolDetail = MCPInfoUtil.getMCPToolArgsIndex((toolsObj.proto as unknown as EggPrototypeWithClazz).clazz!, 'execute');
62+
if (toolMetadata && ToolDetail) {
63+
const tool = new DynamicStructuredTool({
64+
description: toolMetadata.description,
65+
name: toolMetadata.toolName,
66+
func: (toolsObj.obj as unknown as IGraphTool<any>).execute.bind(toolsObj.obj),
67+
schema: z.object(ToolDetail.argsSchema) as any,
68+
});
69+
Object.setPrototypeOf(this, tool);
70+
} else {
71+
throw new Error(`graph tool ${toolMeta.name ?? clazz.name} not found`);
72+
}
73+
}
3374
}
75+
SingletonProto({ name: `structured${toolMeta.name ?? clazz.name}`, accessLevel: AccessLevel.PUBLIC })(StructuredTool);
76+
return StructuredTool;
3477
}
3578

3679
async postCreate(_ctx: LoadUnitLifecycleContext, obj: LoadUnit): Promise<void> {

plugin/langchain/lib/graph/GraphPrototypeHook.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export class GraphPrototypeHook implements LifecycleHook<EggPrototypeLifecycleCo
230230
// 注入到所有依赖这个 graph node 的 graph
231231
for (const [ graphProto, nodeProtos ] of this.graphNodeProtoMap.entries()) {
232232
if (nodeProtos.includes(ctx.clazz)) {
233-
if (!graphProto.injectObjects.find(injectObject => injectObject.refName === `__GRAPH_NODE_${String(ctx.clazz)}__`)) {
233+
if (graphProto.injectObjects.find(injectObject => injectObject.refName === `__GRAPH_NODE_${String(ctx.clazz)}__`)) {
234234
continue;
235235
}
236236
graphProto.injectObjects.push({
@@ -250,7 +250,7 @@ export class GraphPrototypeHook implements LifecycleHook<EggPrototypeLifecycleCo
250250
// 注入到所有依赖这个 graph edge 的 graph
251251
for (const [ graphProto, edgeProtos ] of this.graphEdgeProtoMap.entries()) {
252252
if (edgeProtos.includes(ctx.clazz)) {
253-
if (!graphProto.injectObjects.find(injectObject => injectObject.refName === `__GRAPH_EDGE_${String(ctx.clazz)}__`)) {
253+
if (graphProto.injectObjects.find(injectObject => injectObject.refName === `__GRAPH_EDGE_${String(ctx.clazz)}__`)) {
254254
continue;
255255
}
256256
graphProto.injectObjects.push({

plugin/langchain/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
"koa-compose": "^3.2.1",
7575
"langchain": "^1.1.2",
7676
"sdk-base": "^4.2.0",
77-
"urllib": "^4.4.0"
77+
"urllib": "^4.4.0",
78+
"zod": "^4.0.0"
7879
},
7980
"devDependencies": {
8081
"@eggjs/module-test-util": "^3.71.2",

plugin/langchain/test/fixtures/apps/langchain/app/modules/bar/controller/AppController.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import {
44
HTTPMethodEnum,
55
Inject,
66
} from '@eggjs/tegg';
7-
import { ChatModelQualifier, TeggBoundModel, TeggCompiledStateGraph } from '@eggjs/tegg-langchain-decorator';
7+
import { ChatModelQualifier, IGraphStructuredTool, TeggBoundModel, TeggCompiledStateGraph } from '@eggjs/tegg-langchain-decorator';
88
import { ChatOpenAIModel } from '../../../../../../../../lib/ChatOpenAI';
99
import { BoundChatModel } from '../service/BoundChatModel';
10-
import { FooGraph } from '../service/Graph';
10+
import { FooGraph, FooTool } from '../service/Graph';
1111
import { AIMessage } from 'langchain';
1212

1313
@HTTPController({
@@ -24,6 +24,9 @@ export class AppController {
2424
@Inject()
2525
compiledFooGraph: TeggCompiledStateGraph<FooGraph>;
2626

27+
@Inject()
28+
structuredFooTool: IGraphStructuredTool<FooTool>;
29+
2730
@HTTPMethod({
2831
method: HTTPMethodEnum.GET,
2932
path: '/hello',
@@ -60,4 +63,12 @@ export class AppController {
6063
}, ''),
6164
};
6265
}
66+
67+
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/structured' })
68+
async structured() {
69+
return {
70+
name: this.structuredFooTool.name,
71+
description: this.structuredFooTool.description,
72+
};
73+
}
6374
}

0 commit comments

Comments
 (0)