-
Notifications
You must be signed in to change notification settings - Fork 43
feat: add agent config #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,140 @@ | ||||||||||||||
| import { | ||||||||||||||
| ConfigSourceQualifier, | ||||||||||||||
| Context, | ||||||||||||||
| HTTPBody, | ||||||||||||||
| HTTPController, | ||||||||||||||
| HTTPMethod, | ||||||||||||||
| HTTPMethodEnum, | ||||||||||||||
| LifecycleHook, | ||||||||||||||
| } from '@eggjs/tegg'; | ||||||||||||||
| import { ClassProtoDescriptor, EggContainerFactory, EggPrototypeCreatorFactory, EggPrototypeFactory, ProtoDescriptorHelper } from '@eggjs/tegg/helper'; | ||||||||||||||
| import type { LoadUnit, LoadUnitLifecycleContext } from '@eggjs/tegg-metadata'; | ||||||||||||||
| import { ModuleConfig, ModuleReference } from 'egg'; | ||||||||||||||
| import { LangChainConfigSchemaType } from 'typings'; | ||||||||||||||
| import { Readable, Transform } from 'stream'; | ||||||||||||||
| import { CompiledStateGraph } from '@langchain/langgraph'; | ||||||||||||||
| import { AIMessage, HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages'; | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| export interface ModuleConfigHolder { | ||||||||||||||
| name: string; | ||||||||||||||
| config: ModuleConfig; | ||||||||||||||
| reference: ModuleReference; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| type ValueOf<T> = T[keyof T]; | ||||||||||||||
|
|
||||||||||||||
| export class AgentHttpLoadUnitLifecycleHook implements LifecycleHook<LoadUnitLifecycleContext, LoadUnit> { | ||||||||||||||
| readonly moduleConfigs: Record<string, ModuleConfigHolder>; | ||||||||||||||
|
|
||||||||||||||
| constructor(moduleConfigs: Record<string, ModuleConfigHolder>) { | ||||||||||||||
| this.moduleConfigs = moduleConfigs; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| async preCreate(_: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise<void> { | ||||||||||||||
| const moduleConfigs = this.#getModuleConfig(loadUnit); | ||||||||||||||
| if (moduleConfigs.length > 0) { | ||||||||||||||
| for (const [ graphName, config ] of moduleConfigs) { | ||||||||||||||
| if (config?.type === 'http') { | ||||||||||||||
| const GraphHttpController = this.#createGraphHttpControllerClass(loadUnit, graphName, config); | ||||||||||||||
| const protoDescriptor = ProtoDescriptorHelper.createByInstanceClazz(GraphHttpController, { | ||||||||||||||
| moduleName: loadUnit.name, | ||||||||||||||
| unitPath: loadUnit.unitPath, | ||||||||||||||
| }) as ClassProtoDescriptor; | ||||||||||||||
|
|
||||||||||||||
| const proto = await EggPrototypeCreatorFactory.createProtoByDescriptor(protoDescriptor, loadUnit); | ||||||||||||||
| EggPrototypeFactory.instance.registerPrototype(proto, loadUnit); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #createGraphHttpControllerClass(loadUnit: LoadUnit, graphName: string, config: ValueOf<LangChainConfigSchemaType['agents']>) { | ||||||||||||||
| class GraphHttpController { | ||||||||||||||
| @HTTPMethod({ | ||||||||||||||
| path: config.path!, | ||||||||||||||
| method: HTTPMethodEnum.POST, | ||||||||||||||
| timeout: config.timeout, | ||||||||||||||
| }) | ||||||||||||||
| async invoke(@Context() ctx, @HTTPBody() args) { | ||||||||||||||
| const eggObj = await EggContainerFactory.getOrCreateEggObjectFromName(`compiled${graphName}`); | ||||||||||||||
| const invokeFunc = (eggObj.obj as CompiledStateGraph<any, any>).invoke; | ||||||||||||||
| const streamFunc = (eggObj.obj as CompiledStateGraph<any, any>).stream; | ||||||||||||||
| const genArgs = Object.entries(args).reduce((acc, [ key, value ]) => { | ||||||||||||||
| if (Array.isArray(value) && typeof value[0] === 'object') { | ||||||||||||||
| acc[key] = value.map(obj => { | ||||||||||||||
| switch (obj.role) { | ||||||||||||||
| case 'human': | ||||||||||||||
| return new HumanMessage(obj); | ||||||||||||||
| case 'ai': | ||||||||||||||
| return new AIMessage(obj); | ||||||||||||||
| case 'system': | ||||||||||||||
| return new SystemMessage(obj); | ||||||||||||||
| case 'tool': | ||||||||||||||
| return new ToolMessage(obj); | ||||||||||||||
| default: | ||||||||||||||
| throw new Error('unknown message type'); | ||||||||||||||
| } | ||||||||||||||
| }); | ||||||||||||||
| } else { | ||||||||||||||
| acc[key] = value; | ||||||||||||||
| } | ||||||||||||||
| return acc; | ||||||||||||||
| }, {}); | ||||||||||||||
|
|
||||||||||||||
| const defaultConfig = { | ||||||||||||||
| configurable: { | ||||||||||||||
| thread_id: process.pid.toString(), | ||||||||||||||
| }, | ||||||||||||||
| }; | ||||||||||||||
|
Comment on lines
+85
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Using const defaultConfig = {
configurable: {
- thread_id: process.pid.toString(),
+ thread_id: ctx.request.get('x-thread-id') || ctx.traceId || crypto.randomUUID(),
},
};Add the import at the top of the file: import crypto from 'node:crypto';🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| const res = await Reflect.apply(config.stream ? streamFunc : invokeFunc, (eggObj.obj as CompiledStateGraph<any, any>), [ genArgs, defaultConfig ]); | ||||||||||||||
|
|
||||||||||||||
| if (config.stream) { | ||||||||||||||
| ctx.set({ | ||||||||||||||
| 'content-type': 'text/event-stream', | ||||||||||||||
| 'cache-control': 'no-cache', | ||||||||||||||
| 'transfer-encoding': 'chunked', | ||||||||||||||
| 'X-Accel-Buffering': 'no', | ||||||||||||||
| }); | ||||||||||||||
| const transformStream = new Transform({ | ||||||||||||||
| objectMode: true, | ||||||||||||||
| transform(chunk: any, _encoding: string, callback) { | ||||||||||||||
| try { | ||||||||||||||
| // 如果 chunk 是对象,转换为 JSON | ||||||||||||||
| let data: string; | ||||||||||||||
| if (typeof chunk === 'string') { | ||||||||||||||
| data = chunk; | ||||||||||||||
| } else if (typeof chunk === 'object') { | ||||||||||||||
| data = JSON.stringify(chunk); | ||||||||||||||
| } else { | ||||||||||||||
| data = String(chunk); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // 格式化为 SSE 格式 | ||||||||||||||
| const sseFormatted = `data: ${data}\n\n`; | ||||||||||||||
| callback(null, sseFormatted); | ||||||||||||||
| } catch (error) { | ||||||||||||||
| callback(error); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+117
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TypeScript type error: The } catch (error) {
- callback(error);
+ callback(error instanceof Error ? error : new Error(String(error)));
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| }, | ||||||||||||||
| }); | ||||||||||||||
| return Readable.fromWeb(res as any, { objectMode: true }).pipe(transformStream); | ||||||||||||||
| } | ||||||||||||||
| return res; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| HTTPController({ controllerName: `${graphName}HttpController`, protoName: `${graphName}HttpController` })(GraphHttpController); | ||||||||||||||
| ConfigSourceQualifier(loadUnit.name)(GraphHttpController.prototype, 'moduleConfig'); | ||||||||||||||
|
|
||||||||||||||
| return GraphHttpController; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #getModuleConfig(loadUnit: LoadUnit) { | ||||||||||||||
| const moduleConfig: LangChainConfigSchemaType = (this.moduleConfigs[loadUnit.name]?.config as any)?.langchain; | ||||||||||||||
| if (moduleConfig && Object.keys(moduleConfig?.agents || {}).length > 0) { | ||||||||||||||
| return Object.entries(moduleConfig.agents); | ||||||||||||||
| } | ||||||||||||||
| return []; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,7 @@ import { EggPrototype } from '@eggjs/tegg-metadata'; | |||||
| import { ChatCheckpointSaverInjectName, ChatCheckpointSaverQualifierAttribute, GRAPH_EDGE_METADATA, GRAPH_NODE_METADATA, GraphEdgeMetadata, GraphMetadata, GraphNodeMetadata, IGraph, IGraphEdge, IGraphNode, TeggToolNode } from '@eggjs/tegg-langchain-decorator'; | ||||||
| import { LangGraphTracer } from '../tracing/LangGraphTracer'; | ||||||
| import { BaseCheckpointSaver, CompiledStateGraph } from '@langchain/langgraph'; | ||||||
| import { Application } from 'egg'; | ||||||
|
|
||||||
| export class CompiledStateGraphObject implements EggObject { | ||||||
| private status: EggObjectStatus = EggObjectStatus.PENDING; | ||||||
|
|
@@ -19,17 +20,19 @@ export class CompiledStateGraphObject implements EggObject { | |||||
| readonly proto: CompiledStateGraphProto; | ||||||
| readonly ctx: EggContext; | ||||||
| readonly daoName: string; | ||||||
| private _obj: object; | ||||||
| _obj: object; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| readonly graphMetadata: GraphMetadata; | ||||||
| readonly graphName: string; | ||||||
| readonly app: Application; | ||||||
|
|
||||||
| constructor(name: EggObjectName, proto: CompiledStateGraphProto) { | ||||||
| constructor(name: EggObjectName, proto: CompiledStateGraphProto, app: Application) { | ||||||
| this.name = name; | ||||||
| this.proto = proto; | ||||||
| this.ctx = ContextHandler.getContext()!; | ||||||
| this.id = IdenticalUtil.createObjectId(this.proto.id, this.ctx?.id); | ||||||
| this.graphMetadata = proto.graphMetadata; | ||||||
| this.graphName = proto.graphName; | ||||||
| this.app = app; | ||||||
| } | ||||||
|
|
||||||
| async init() { | ||||||
|
|
@@ -122,9 +125,11 @@ export class CompiledStateGraphObject implements EggObject { | |||||
| return this._obj; | ||||||
| } | ||||||
|
|
||||||
| static async createObject(name: EggObjectName, proto: EggPrototype): Promise<CompiledStateGraphObject> { | ||||||
| const compiledStateGraphObject = new CompiledStateGraphObject(name, proto as CompiledStateGraphProto); | ||||||
| await compiledStateGraphObject.init(); | ||||||
| return compiledStateGraphObject; | ||||||
| static createObject(app: Application) { | ||||||
| return async function(name: EggObjectName, proto: EggPrototype): Promise<CompiledStateGraphObject> { | ||||||
| const compiledStateGraphObject = new CompiledStateGraphObject(name, proto as CompiledStateGraphProto, app); | ||||||
| await compiledStateGraphObject.init(); | ||||||
| return compiledStateGraphObject; | ||||||
| }; | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,9 +8,6 @@ module.exports = function() { | |
| enable: false, | ||
| }, | ||
| }, | ||
| bodyParser: { | ||
| enable: false, | ||
| }, | ||
| }; | ||
| return config; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -108,11 +108,34 @@ export const ChatModelConfigModuleConfigSchema = Type.Object({ | |||||
| name: 'ChatModel', | ||||||
| }); | ||||||
|
|
||||||
|
|
||||||
| export const LangChainConfigSchema = Type.Object({ | ||||||
| agents: Type.Record(Type.String(), Type.Object({ | ||||||
| path: Type.Optional(Type.String({ | ||||||
| description: 'http path', | ||||||
| })), | ||||||
| stream: Type.Optional(Type.Boolean({ | ||||||
| description: '是否流式返回', | ||||||
| })), | ||||||
| type: Type.Optional(Type.String({ | ||||||
| description: 'Http', | ||||||
| })), | ||||||
| timeout: Type.Optional(Type.Number({ | ||||||
| description: '接口超时时间', | ||||||
| })), | ||||||
| })), | ||||||
| }, { | ||||||
| title: 'langchain 设置', | ||||||
| name: 'langchain', | ||||||
| }); | ||||||
|
|
||||||
| export type ChatModelConfigModuleConfigType = Static<typeof ChatModelConfigModuleConfigSchema>; | ||||||
| export type LangChainConfigSchemaType = Static<typeof LangChainConfigSchema>; | ||||||
|
|
||||||
| declare module '@eggjs/tegg' { | ||||||
| export type LangChainModuleConfig = { | ||||||
| ChatModel?: ChatModelConfigModuleConfigType; | ||||||
| langchain?: LangChainConfigSchema; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type error: Use the type alias instead of the schema constant.
export type LangChainModuleConfig = {
ChatModel?: ChatModelConfigModuleConfigType;
- langchain?: LangChainConfigSchema;
+ langchain?: LangChainConfigSchemaType;
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| }; | ||||||
|
|
||||||
| export interface ModuleConfig extends LangChainModuleConfig { | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add validation for
config.pathbefore using non-null assertion.The
pathproperty in the agent configuration is optional, soconfig.pathcan beundefined. Using the non-null assertion (!) will cause a runtime error if the path is not provided. This should be validated before reaching this point.The validation should occur in
preCreatebefore calling#createGraphHttpControllerClass:for (const [ graphName, config ] of moduleConfigs) { - if (config?.type === 'http') { + if (config?.type === 'http' && config.path) { const GraphHttpController = this.#createGraphHttpControllerClass(loadUnit, graphName, config);Or add an explicit error if path is missing:
#createGraphHttpControllerClass(loadUnit: LoadUnit, graphName: string, config: ValueOf<LangChainConfigSchemaType['agents']>) { + if (!config.path) { + throw new Error(`Agent ${graphName} is configured as HTTP but missing 'path' field`); + } class GraphHttpController {🤖 Prompt for AI Agents