diff --git a/packages/ai-lang/package.json b/packages/ai-lang/package.json index 95984ff..e8913a0 100644 --- a/packages/ai-lang/package.json +++ b/packages/ai-lang/package.json @@ -21,6 +21,7 @@ "@langchain/ollama": "^0.1.0", "@langchain/openai": "^0.3.2", "mongodb": "^6.9.0", + "nanoid": "^5.0.7", "ws": "^8.18.0" }, "devDependencies": { diff --git a/packages/ai-lang/src/agent/index.ts b/packages/ai-lang/src/agent/index.ts deleted file mode 100644 index 3ce193c..0000000 --- a/packages/ai-lang/src/agent/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createReactAgent } from '@langchain/langgraph/prebuilt'; -import { MemorySaver } from '@langchain/langgraph'; -import { ChatOllama } from '@langchain/ollama'; -import { ChatOpenAI } from '@langchain/openai'; -import { checkpointer } from '../module/save.ts'; -import { HumanMessage } from '@langchain/core/messages'; -export { HumanMessage }; -// const agentModel = new ChatOllama({ temperature: 0, model: 'llama3.1:8b', baseUrl: 'http://mz.zxj.im:11434' }); -export const agentModelBakllava = new ChatOllama({ temperature: 0, model: 'bakllava:latest', baseUrl: 'http://mz.zxj.im:11434' }); -export const agentModel = new ChatOllama({ temperature: 0, model: 'qwen2.5:14b', baseUrl: 'http://mz.zxj.im:11434' }); -export const agentModelOpenAI = new ChatOpenAI( - { temperature: 0, model: 'gpt-4o', apiKey: 'sk-GJE6I8OJWDr2ErFBD4C4706a65Ad4cD9B596Cf7c76943e45' }, - { - baseURL: 'https://oneapi.on-ai.ai/v1', - }, -); - -const agentCheckpointer = checkpointer; - -export const agent = createReactAgent({ - llm: agentModel, - tools: [], - checkpointSaver: agentCheckpointer, -}); -export const agentLlava = createReactAgent({ - llm: agentModelBakllava, - tools: [], - checkpointSaver: agentCheckpointer, -}); - -export const agentOpenAI = createReactAgent({ llm: agentModelOpenAI, tools: [], checkpointSaver: agentCheckpointer }); diff --git a/packages/ai-lang/src/app.ts b/packages/ai-lang/src/app.ts index d97c8a4..f08fde2 100644 --- a/packages/ai-lang/src/app.ts +++ b/packages/ai-lang/src/app.ts @@ -2,7 +2,6 @@ import { App } from '@abearxiong/router'; import { useConfig } from '@abearxiong/use-config'; const config = useConfig(); -console.log('config in ai-lang', config); export const app = new App({ serverOptions: { diff --git a/packages/ai-lang/src/index.ts b/packages/ai-lang/src/index.ts index 349bda6..fd86be4 100644 --- a/packages/ai-lang/src/index.ts +++ b/packages/ai-lang/src/index.ts @@ -1 +1,4 @@ -export * from './app.ts' \ No newline at end of file +export * from './app.ts'; +import './routes/agent.ts'; +import { agentManger } from './module/agent.ts'; +export { agentManger }; diff --git a/packages/ai-lang/src/module/agent.ts b/packages/ai-lang/src/module/agent.ts new file mode 100644 index 0000000..0435672 --- /dev/null +++ b/packages/ai-lang/src/module/agent.ts @@ -0,0 +1,51 @@ +import { AiAgent, AiAgentOpts } from './create-agent.ts'; +export enum AgentMangerStatus { + init = 'i', + ready = 'r', + error = 'e', +} +export class AgentManger { + agents: AiAgent[] = []; + staus: AgentMangerStatus = AgentMangerStatus.init; + constructor() {} + addAgent(agent: AiAgent) { + this.agents.push(agent); + } + getAgent(id: string) { + const agent = this.agents.find((agent) => agent.id === id); + return agent; + } + removeAgent(id: string) { + this.agents = this.agents.filter((agent) => agent.id !== id); + } + createAgent(opts: AiAgentOpts) { + if (!opts.id) { + const agent = new AiAgent(opts); + this.addAgent(agent); + return agent; + } + const agent = this.agents.find((agent) => agent.id === opts.id); + if (!agent) { + const agent = new AiAgent(opts); + this.addAgent(agent); + return agent; + } + return agent; + } + /** + * 临时创建一个agent + * @param opts + * @returns + */ + newAgent(opts: AiAgentOpts) { + return new AiAgent(opts); + } + createAgentList(opts: AiAgentOpts[]) { + if (this.staus === AgentMangerStatus.init) { + return; + } + this.staus = AgentMangerStatus.ready; + return opts.map((opt) => this.createAgent(opt)); + } +} +export const agentManger = new AgentManger(); diff --git a/packages/ai-lang/src/module/create-agent.ts b/packages/ai-lang/src/module/create-agent.ts new file mode 100644 index 0000000..cdecfab --- /dev/null +++ b/packages/ai-lang/src/module/create-agent.ts @@ -0,0 +1,142 @@ +import { createReactAgent } from '@langchain/langgraph/prebuilt'; +import { MemorySaver } from '@langchain/langgraph'; +import { ChatOllama } from '@langchain/ollama'; +import { ChatOpenAI } from '@langchain/openai'; +import { client } from './mongo.ts'; +import { MongoDBSaver } from '@langchain/langgraph-checkpoint-mongodb'; +import { nanoid } from 'nanoid'; +import { HumanMessage } from '@langchain/core/messages'; +export { HumanMessage }; + +export const agentModelList = ['qwen2.5:14b', 'qwen2.5-coder:7b', 'llama3.1:8b', 'bakllava:latest', 'gpt-4o'] as const; +export type AiAgentModel = (typeof agentModelList)[number]; +export type AiAgentCache = 'memory' | 'mongodb'; +export type AiAgentOpts = { + id: string; + type: 'ollama' | 'openai'; + model: AiAgentModel; + baseUrl: string; + apiKey?: string; + temperature?: number; + cache?: AiAgentCache; + cacheName?: string; +}; +export type AiAgentStatus = 'ready' | 'loading' | 'error'; +// export const CreateAgent = (opts: CreateAgentOptions) => { +// const; +// }; + +export class AiAgent { + agent: ReturnType; + agentModel: ChatOllama | ChatOpenAI; + memorySaver: MemorySaver | MongoDBSaver; + id: string; + baseUrl: string; + type: 'ollama' | 'openai'; + model: AiAgentModel; + apiKey: string; + temperature = 0; + cache?: AiAgentCache; + cacheName?: string; + status?: 'ready' | 'loading' | 'error'; + constructor(opts?: AiAgentOpts) { + this.type = opts?.type || 'ollama'; + this.baseUrl = opts?.baseUrl || 'http://localhost:11434'; + this.model = opts?.model; + this.apiKey = opts?.apiKey; + this.temperature = opts?.temperature || 0; + this.cache = opts?.cache || 'mongodb'; + this.cacheName = opts?.cacheName || 'checkpointer'; + this.id = opts?.id || nanoid(8); + if (this.type === 'openai') { + if (!this.apiKey) { + throw new Error('apiKey is required for openai agent'); + } + } + this.status = 'loading'; + this.createAgent(); + } + createAgent() { + this.createAgentModel(); + this.createMemoerSaver(); + if (this.status === 'error') { + return; + } + + const agentModel = this.agentModel; + const memoerSaver = this.memorySaver; + this.agent = createReactAgent({ + llm: agentModel, + tools: [], + checkpointSaver: memoerSaver, + }); + this.status = 'ready'; + } + createAgentModel() { + const type = this.type; + const model = this.model; + const temperature = this.temperature; + const apiKey = this.apiKey; + const baseUrl = this.baseUrl; + let agentModel; + try { + if (type === 'ollama') { + agentModel = new ChatOllama({ temperature, model, baseUrl }); + } else if (type === 'openai') { + agentModel = new ChatOpenAI( + { temperature, model, apiKey }, + { + baseURL: baseUrl, + }, + ); + } + } catch (e) { + console.error('loading model error', e); + this.status = 'error'; + return; + } + this.agentModel = agentModel; + return this; + } + createMemoerSaver() { + const cache = this.cache; + const cacheName = this.cacheName; + let memorySaver; + try { + if (cache === 'memory') { + memorySaver = new MemorySaver(); + } else if (cache === 'mongodb') { + memorySaver = new MongoDBSaver({ client, dbName: cacheName }); + } + } catch (e) { + console.error(e); + this.status = 'error'; + return; + } + this.memorySaver = memorySaver; + } + sendHumanMessage(message: string, opts?: { thread_id: string }) { + const mesage = new HumanMessage(message); + return this.agent.invoke({ messages: [mesage] }, { configurable: { thread_id: 'test_human', ...opts } }); + } + close() { + // 清除 memory saver + this.memorySaver = null; + this.agentModel = null; + this.agent = null; + } + async testQuery() { + const id = this.id; + try { + const agent = this.agent; + const message = new HumanMessage('你好'); + const res = await agent.invoke({ messages: [message] }, { configurable: { thread_id: 'test_ping' } }); + if (res) { + return res; + } + } catch (e) { + console.error(`test query [${id}]:`, e); + this.status = 'error'; + } + } +} diff --git a/packages/ai-lang/src/module/index.ts b/packages/ai-lang/src/module/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/ai-lang/src/module/mongo.ts b/packages/ai-lang/src/module/mongo.ts index 528cab2..677eb1c 100644 --- a/packages/ai-lang/src/module/mongo.ts +++ b/packages/ai-lang/src/module/mongo.ts @@ -2,3 +2,13 @@ import { MongoClient } from 'mongodb'; import { useConfig } from '@abearxiong/use-config'; const { mongo } = useConfig<{ host: string; password: string; username: string }>(); export const client = new MongoClient(`mongodb://${mongo.username}:${mongo.password}@${mongo.host}`, {}); + +// 当连接成功时,打印出连接成功的信息 +client + .connect() + .then(() => { + console.log('mongo Connected successfully to server'); + }) + .catch((err) => { + console.error(err); + }); diff --git a/packages/ai-lang/src/module/save.ts b/packages/ai-lang/src/module/save.ts deleted file mode 100644 index c643a98..0000000 --- a/packages/ai-lang/src/module/save.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { MongoDBSaver } from '@langchain/langgraph-checkpoint-mongodb'; -import { client } from './mongo.ts'; - -export const checkpointer = new MongoDBSaver({ client }); diff --git a/packages/ai-lang/src/routes/agent.ts b/packages/ai-lang/src/routes/agent.ts index 8525051..d60cd0b 100644 --- a/packages/ai-lang/src/routes/agent.ts +++ b/packages/ai-lang/src/routes/agent.ts @@ -1,10 +1,19 @@ +import { CustomError } from '@abearxiong/router'; import { app } from '../app.ts'; -import { agent, HumanMessage } from '../agent/index.ts'; +// import { agent, HumanMessage } from '../agent/index.ts'; +import { agentManger } from '../module/agent.ts'; app .route('ai', 'chat') .define(async (ctx) => { - const { message } = ctx.query; - const response = await agent.invoke({ messages: [new HumanMessage(message)] }, { configurable: { thread_id: '44' } }); - ctx.body = response; + const { message, agentId, chatId } = ctx.query.data; + // const response = await agent.invoke({ messages: [new HumanMessage(message)] }, { configurable: { thread_id: '44' } }); + // ctx.body = response; + // + const agent = agentManger.getAgent(agentId); + if (!agent) { + throw new CustomError('agent not found'); + } }) .addTo(app); + +// app.router.parse({}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cff59b2..2708c31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,6 +191,9 @@ importers: mongodb: specifier: ^6.9.0 version: 6.9.0 + nanoid: + specifier: ^5.0.7 + version: 5.0.7 ws: specifier: ^8.18.0 version: 8.18.0 diff --git a/src/index.ts b/src/index.ts index 50a43af..8f0cf02 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,5 +7,5 @@ import { app as aiApp } from '@kevisual/ai-lang/src/index.ts'; export { aiApp }; export { app }; app.listen(config.port, () => { - console.log(`server is running at http://localhost:${config.port}`); + console.log(`server2 is running at http://localhost:${config.port}`); }); diff --git a/src/models/agent.ts b/src/models/agent.ts new file mode 100644 index 0000000..b2eaab7 --- /dev/null +++ b/src/models/agent.ts @@ -0,0 +1,85 @@ +import { sequelize } from '@/modules/sequelize.ts'; +import { DataTypes, Model } from 'sequelize'; + +export class AiAgent extends Model { + id: string; + type: string; + model: string; + baseUrl: string; + apiKey: string; + temperature: number; + cache: string; + cacheName: string; + status: string; + data: any; + key: string; +} + +// 获取AIAgent的属性 +export type AiProperties = { + id: string; + type: string; + model: string; + baseUrl: string; + apiKey?: string; + temperature?: number; + cache?: string; + cacheName?: string; + data?: any; +}; +AiAgent.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + type: { + type: DataTypes.STRING, + allowNull: false, + }, + status: { + type: DataTypes.STRING, + defaultValue: 'open', + }, + model: { + type: DataTypes.STRING, + allowNull: false, + }, + baseUrl: { + type: DataTypes.STRING, + allowNull: false, + }, + apiKey: { + type: DataTypes.STRING, + allowNull: false, + }, + key: { + type: DataTypes.STRING, + }, + temperature: { + type: DataTypes.FLOAT, + allowNull: true, + }, + cache: { + type: DataTypes.STRING, + allowNull: true, + }, + cacheName: { + type: DataTypes.STRING, + allowNull: true, + }, + data: { + type: DataTypes.JSON, + allowNull: true, + defaultValue: {}, + }, + }, + { + sequelize, + tableName: 'ai_agent', + }, +); +AiAgent.sync({ alter: true, logging: false }).catch((e) => { + console.error('AiAgent sync error', e); +}); diff --git a/src/routes/agent/index.ts b/src/routes/agent/index.ts new file mode 100644 index 0000000..83ec5cd --- /dev/null +++ b/src/routes/agent/index.ts @@ -0,0 +1 @@ +import './list.ts'; diff --git a/src/routes/agent/list.ts b/src/routes/agent/list.ts new file mode 100644 index 0000000..b4a6257 --- /dev/null +++ b/src/routes/agent/list.ts @@ -0,0 +1,132 @@ +import { app } from '@/app.ts'; +import { AiAgent, AiProperties } from '@/models/agent.ts'; +import { CustomError } from '@abearxiong/router'; +import { agentManger } from '@kevisual/ai-lang'; +import { v4 } from 'uuid'; +app + .route('agent', 'list') + .define(async (ctx) => { + const agents = await AiAgent.findAll({ + order: [['updatedAt', 'DESC']], + // 返回的内容,不包含apiKey的字段 + attributes: { exclude: ['apiKey'] }, + }); + ctx.body = agents; + }) + .addTo(app); + +app + .route('agent', 'get') + .define(async (ctx) => { + const id = ctx.query.id; + if (!id) { + throw new CustomError('id is required'); + } + ctx.body = await AiAgent.findByPk(id, { + attributes: { exclude: ['apiKey'] }, + }); + return ctx; + }) + .addTo(app); + +app + .route('agent', 'update') + .define(async (ctx) => { + const { id, ...rest } = ctx.query.data; + let agent = await AiAgent.findByPk(id); + if (!agent) { + agent = await AiAgent.create(rest); + ctx.body = agent; + return ctx; + } + await agent.update(rest); + ctx.body = agent; + return ctx; + }) + .addTo(app); + +app + .route('agent', 'delete') + .define(async (ctx) => { + const id = ctx.query.id; + if (!id) { + throw new CustomError('id is required'); + } + const agent = await AiAgent.findByPk(id); + if (!agent) { + throw new CustomError('agent not found'); + } + await agent.destroy(); + ctx.body = agent; + return ctx; + }) + .addTo(app); + +app + .route('agent', 'test') + .define(async (ctx) => { + const { message } = ctx.query; + const data: AiProperties = { + type: 'ollama', + id: 'test', + model: 'qwen2.5:14b', + baseUrl: 'http://mz.zxj.im:11434', + cache: 'memory', + }; + const agent = agentManger.createAgent(data as any); + const res = await agent.sendHumanMessage(message); + // agent.close(); + agentManger.removeAgent(agent.id); + ctx.body = res; + return ctx; + }) + .addTo(app); + +export const agentModelList = ['qwen2.5:14b', 'qwen2.5-coder:7b', 'llama3.1:8b', 'bakllava:latest'] as const; +export const openAiModels = ['gpt-4o']; +const demoData: AiProperties[] = [ + { + id: v4(), + type: 'openai', + model: 'gpt-4o', + baseUrl: 'https://oneapi.on-ai.ai/v1', + apiKey: 'sk-GJE6I8OJWDr2ErFBD4C4706a65Ad4cD9B596Cf7c76943e45', + }, + ...agentModelList.map((item) => { + return { + id: v4(), + type: 'ollama', + model: item, + baseUrl: 'http://mz.zxj.im:11434', + apiKey: 'sk-GJE6I8OJWDr2ErFBD4C4706a65Ad4cD9B596Cf7c76943e45', + }; + }), +]; + +// AiAgent.bulkCreate(demoData, { ignoreDuplicates: true }).then(() => { +// console.log('create demo data success'); +// }); +const initManager = async () => { + // const list = await AiAgent.findAll(); + const list = await AiAgent.findAll({ + where: { + status: 'open', + }, + }); + const data = list.map((item) => { + return { + id: item.id, + type: item.type as any, + model: item.model as any, + baseUrl: item.baseUrl, + apiKey: item.apiKey, + temperature: item.temperature, + cache: item.cache as any, + cacheName: item.cacheName, + }; + }); + agentManger.createAgentList(data); +}; +setTimeout(() => { + initManager(); +}, 1000); diff --git a/src/routes/index.ts b/src/routes/index.ts index 97a14bc..c440a99 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -4,4 +4,6 @@ import './page/index.ts'; import './resource/index.ts'; -import './prompt-graph/index.ts'; \ No newline at end of file +import './prompt-graph/index.ts'; + +import './agent/index.ts';