diff --git a/packages/ai-graph b/packages/ai-graph index d1e2306..f1346b3 160000 --- a/packages/ai-graph +++ b/packages/ai-graph @@ -1 +1 @@ -Subproject commit d1e2306233fde79b510dec614a95f867d686243d +Subproject commit f1346b3cef6684707bcd4faec20231e535904486 diff --git a/src/app.ts b/src/app.ts index 3129bd1..cb94dc1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,14 +2,14 @@ import { App } from '@abearxiong/router'; import { useConfig } from '@abearxiong/use-config'; import { dynamicImport } from './lib/dynamic-import.ts'; import { redisPublisher, redisSubscriber, redis } from './modules/redis.ts'; -import { neode } from './modules/neo4j.ts'; +import { neode, getSession } from './modules/neo4j.ts'; import { minioClient } from './modules/minio.ts'; import { sequelize } from './modules/sequelize.ts'; useConfig(); export const emit = (channel: string, message?: any) => { redisPublisher.publish(channel, JSON.stringify(message)); }; -export { neode, redis, minioClient, sequelize }; +export { neode, getSession, redis, minioClient, sequelize }; export const app = new App<{ import: any; emit: typeof emit }>({ serverOptions: { diff --git a/src/models/code.ts b/src/models/code.ts index deb5da5..f673650 100644 --- a/src/models/code.ts +++ b/src/models/code.ts @@ -90,7 +90,7 @@ RouterCodeModel.init( }, { sequelize, - tableName: 'cf_router_code', + tableName: 'cf_router_code', // container flow router code }, ); RouterCodeModel.sync({ alter: true, logging: false }).catch((e) => { diff --git a/src/models/prompt-graph.ts b/src/models/prompt-graph.ts new file mode 100644 index 0000000..2678505 --- /dev/null +++ b/src/models/prompt-graph.ts @@ -0,0 +1,83 @@ +import { neode } from '@/app.ts'; +import { getSession } from '@/modules/neo4j.ts'; +import Neode from 'neode'; + +export const PromptNeo = neode.model('Prompt', { + id: { + type: 'uuid', + primary: true, + }, + title: { + type: 'string', + }, + description: 'string', + // profile: { type: 'object', optional: true }, // 用于存储 JSON 对象 + prompt: 'string', + // inputVariables: { type: 'array', item }, + // tags: { type: 'array', items: 'string', optional: true } // 定义字符串数组 + inputVariables: { type: 'string', default: JSON.stringify([]) }, + localVariables: { type: 'string', default: JSON.stringify([]) }, + + // 定义可单向或双向的关系 + relatedPrompts: { + type: 'relationship', + relationship: 'RELATED_TO', + target: 'Prompt', // 指向自身 + direction: 'out', // 默认是单向的 + properties: { + created_at: 'datetime', + bidirectional: 'boolean', // 用来标记该关系是否为双向 + }, + eager: true, // 自动加载相关的 Prompts + }, +}); +export async function createRelationship(promptA: Neode.Node, promptB: Neode.Node, isBidirectional = false) { + // 创建单向关系 + await promptA.relateTo(promptB, 'RELATED_TO', { created_at: new Date(), bidirectional: isBidirectional }); + + // 如果是双向关系,创建反向关系 + if (isBidirectional) { + await promptB.relateTo(promptA, 'RELATED_TO', { created_at: new Date(), bidirectional: true }); + } +} +export async function createRelationship2(promptId1, promptId2, isBidirectional = false) { + const query = ` + MATCH (p1:Prompt {id: $id1}), (p2:Prompt {id: $id2}) + CREATE (p1)-[r:RELATED_TO {created_at: $createdAt, bidirectional: $bidirectional}]->(p2) + RETURN r + `; + + const result = await getSession().run(query, { + id1: promptId1, + id2: promptId2, + createdAt: new Date().toISOString(), + bidirectional: isBidirectional, + }); + + return result.records[0].get('r'); +} +export async function createPrompt(promptData) { + const session = getSession(); + const query = ` + CREATE (p:Prompt { + id: $id, + title: $title, + description: $description, + prompt: $prompt, + inputVariables: $inputVariables, + localVariables: $localVariables + }) + RETURN p + `; + + const result = await session.run(query, { + id: promptData.id, + title: promptData.title, + description: promptData.description, + prompt: promptData.prompt, + inputVariables: JSON.stringify(promptData.inputVariables || []), + localVariables: JSON.stringify(promptData.localVariables || []), + }); + + return result.records[0].get('p'); +} diff --git a/src/models/prompt.ts b/src/models/prompt.ts index 484a924..414d47c 100644 --- a/src/models/prompt.ts +++ b/src/models/prompt.ts @@ -1,18 +1,65 @@ -import { neode } from '@/app.ts'; +import { sequelize } from '../modules/sequelize.ts'; +import { DataTypes, Model } from 'sequelize'; +import { Variable } from '@kevisual/ai-graph'; -export const PromptNeo = neode.model('Prompt', { - id: { - type: 'uuid', - primary: true, +/** + * 预设数据,定义了请求的内容和验证器 + */ +export type PresetData = { + // 参数 + validator: { + [key: string]: any; // 请求的内容的验证器 + }; + data: { + prompt?: string; // 提前预设值 + inputs: Variable & { operate?: string }[]; // 请求内容的变量和内容 + }; +}; +export class Prompt extends Model { + declare id: string; + declare title: string; + declare description: string; + declare presetData: PresetData; +} + +Prompt.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + presetData: { + type: DataTypes.JSON, + }, + key: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + // inputVariables: { + // type: DataTypes.JSON, + // defaultValue: [], + // }, + // localVariables: { + // type: DataTypes.JSON, + // defaultValue: [], + // }, }, - title: { - type: 'string', + { + sequelize, // 传入 Sequelize 实例 + modelName: 'prompt', // 模型名称 }, - description: 'string', - // profile: { type: 'object', optional: true }, // 用于存储 JSON 对象 - prompt: 'string', - // inputVariables: { type: 'array', item }, - // tags: { type: 'array', items: 'string', optional: true } // 定义字符串数组 - inputVariables: { type: 'string', default: JSON.stringify([]) }, - localVariables: { type: 'string', default: JSON.stringify([]) }, +); + +Prompt.sync({ alter: true, force: false, logging: false }).catch((e) => { + console.error('Prompt sync error', e); }); diff --git a/src/modules/neo4j.ts b/src/modules/neo4j.ts index daddc1c..a1fb616 100644 --- a/src/modules/neo4j.ts +++ b/src/modules/neo4j.ts @@ -1,17 +1,26 @@ import Neode from 'neode'; import { useConfig } from '@abearxiong/use-config'; +import neo4j from 'neo4j-driver'; type NeodeConfig = { uri: string; username: string; password: string; }; -const { neo4j } = useConfig<{ neo4j: NeodeConfig }>('neo4j'); +const { neo4j: neo4jConfig } = useConfig<{ neo4j: NeodeConfig }>('neo4j'); -const { uri, username, password } = neo4j; +const { uri, username, password } = neo4jConfig; // 设置连接配置 // const neode = new Neode('bolt://localhost:7687', 'neo4j', 'your_password'); export const neode = new Neode(uri, username, password); +// 创建与 Neo4j 数据库的连接 +export const neoDriver = neo4j.driver( + uri, // 数据库地址 + neo4j.auth.basic(username, password), // 用户名和密码 +); +export const getSession = () => { + return neoDriver.session(); +}; const testConnect = async () => { // 连接成功 diff --git a/src/modules/ollama.ts b/src/modules/ollama.ts index 93ea2ae..66e2e6a 100644 --- a/src/modules/ollama.ts +++ b/src/modules/ollama.ts @@ -3,7 +3,7 @@ import { Ollama, Message, ChatRequest } from 'ollama'; const config = useConfig<{ ollama: Ollama['config'] & { model: string } }>(); -const { host } = config.ollama; +const { host, model } = config.ollama; export const ollama = new Ollama({ host }); @@ -20,7 +20,7 @@ export const chat = (messages: ChatMessage[], chatOpts?: ChatOpts) => { const { options, stream, ...rest } = chatOpts || {}; return ollama.chat({ messages, - model: config.model, + model: model, options: { temperature: 0, ...chatOpts?.options, diff --git a/src/routes/prompt-graph/ai.ts b/src/routes/prompt-graph/ai.ts new file mode 100644 index 0000000..b523e4f --- /dev/null +++ b/src/routes/prompt-graph/ai.ts @@ -0,0 +1,69 @@ +import { app } from '@/app.ts'; +import { Prompt } from '@/models/prompt.ts'; +import { chat } from '@/modules/ollama.ts'; +import { CustomError } from '@abearxiong/router'; +import { PromptTemplate } from '@kevisual/ai-graph'; + +app + .route('ai', 'run', { nextRoute: { id: 'runOllama' } }) + .define({ + validator: { + key: { + type: 'string', + required: true, + message: 'Prompt key is required', + }, + }, + }) + .define(async (ctx) => { + // ctx.currentRoute?.verify(ctx, true); + + const { key, inputs = [] } = ctx.query.data || {}; + if (!key) { + throw new CustomError('Prompt key is required'); + } + const prompt = await Prompt.findOne({ where: { key } }); + if (!prompt) { + throw new CustomError('Prompt not found'); + } + const { presetData } = prompt; + const { data, validator } = presetData || {}; + // const { inputs = [] } = data; + // TODO: 获取validator和inputs的内容 + const promptTemplate = new PromptTemplate({ + prompt: data.prompt, + inputVariables: inputs.map((item) => { + return { + key: item.key, + value: item.value, + }; + }), + localVariables: [], + }); + const result = await promptTemplate.getTemplate(); + ctx.state = { + prompt: result, + }; + ctx.body = result; + }) + .addTo(app); + +app + .route('ai', 'runOllama', { + id: 'runOllama', + }) + .define(async (ctx) => { + const prompt = ctx.state.prompt; + if (!prompt) { + throw new CustomError('Prompt not found'); + } + console.log('prompt', typeof prompt, prompt); + const res = await chat([ + { + role: 'user', + content: prompt, + }, + ]); + ctx.body = res; + }) + .addTo(app); diff --git a/src/routes/prompt-graph/d3/get-graph.ts b/src/routes/prompt-graph/d3/get-graph.ts new file mode 100644 index 0000000..e5f58e2 --- /dev/null +++ b/src/routes/prompt-graph/d3/get-graph.ts @@ -0,0 +1,73 @@ +import { getSession } from '@/app.ts'; +export async function fetchData() { + const session = getSession(); + try { + const query = `MATCH (n) +OPTIONAL MATCH (n)-[r]->(m) +RETURN n, r, m`; + const queryConnect = 'MATCH (n)-[r]->(m) RETURN n, r, m LIMIT 25'; + const result = await session.run(query); + + const graphData = { nodes: [], links: [] }; + const nodeMap = new Map(); + // n和n的关系用 relatedPrompts 进行关联 + result.records.forEach((record) => { + const node = record.get('n'); + const relation = record.get('r'); + const target = record.get('m'); + if (!nodeMap.has(node.identity)) { + nodeMap.set(node.identity, { + id: node.identity.toString(), + label: node.labels[0], + properties: node.properties, + }); + graphData.nodes.push(nodeMap.get(node.identity)); + } + + if (relation && !nodeMap.has(relation.identity)) { + nodeMap.set(relation.identity, { + id: relation.identity.toString(), + label: relation.type, + properties: relation.properties, + }); + graphData.nodes.push(nodeMap.get(relation.identity)); + } + + if (target && !nodeMap.has(target.identity)) { + nodeMap.set(target.identity, { + id: target.identity.toString(), + label: target.labels[0], + properties: target.properties, + }); + graphData.nodes.push(nodeMap.get(target.identity)); + } + + if (relation) { + graphData.links.push({ + source: node.identity.toString(), + target: relation.identity.toString(), + type: relation.type, + properties: relation.properties, + }); + } + + if (target) { + graphData.links.push({ + source: node.identity.toString(), + target: target.identity.toString(), + type: 'RELATED_TO', + properties: {}, + }); + } + }); + + return graphData; + } finally { + await session.close(); + } +} + +// fetchData().then((graphData) => { +// console.log(graphData); // 用于验证获取的数据 +// drawGraph(graphData); // 调用 D3 绘制函数 +// }); diff --git a/src/routes/prompt-graph/index.ts b/src/routes/prompt-graph/index.ts index 9166f9d..ab5fb90 100644 --- a/src/routes/prompt-graph/index.ts +++ b/src/routes/prompt-graph/index.ts @@ -1 +1,3 @@ -import './list.ts' \ No newline at end of file +import './list-graph.ts'; +import './list.ts'; +import './ai.ts'; diff --git a/src/routes/prompt-graph/list-graph.ts b/src/routes/prompt-graph/list-graph.ts new file mode 100644 index 0000000..861f971 --- /dev/null +++ b/src/routes/prompt-graph/list-graph.ts @@ -0,0 +1,97 @@ +import { PromptNeo, createRelationship, createRelationship2 } from '@/models/prompt-graph.ts'; +import { app } from '@/app.ts'; +import { v4 } from 'uuid'; +import { fetchData } from './d3/get-graph.ts'; +app + .route('prompt-graph', 'list') + .define(async (ctx) => { + const prompts = await PromptNeo.all(); + const json = await prompts.toJson(); + // console.log('json', json); + ctx.body = json; + }) + .addTo(app); + +app + .route('prompt-graph', 'update') + .define(async (ctx) => { + const { id, title, description, prompt, inputVariables, localVariables } = ctx.query; + const promptNode = await PromptNeo.first('id', id); + + if (!promptNode) { + const promptData = { + id: v4(), + title, + description, + prompt, + inputVariables: JSON.stringify(inputVariables), + localVariables: JSON.stringify(localVariables), + }; + const _prompt = await PromptNeo.create(promptData); + ctx.body = await _prompt.toJson(); + return; + } + + await promptNode.update({ title, description, prompt, inputVariables, localVariables }); + ctx.body = await promptNode.toJson(); + }) + .addTo(app); + +app + .route('prompt-graph', 'delete') + .define(async (ctx) => { + const { id, title } = ctx.query; + const promptNode = await PromptNeo.first('id', id); + if (!promptNode) { + ctx.body = 'prompt not found'; + return; + } + await promptNode.delete(); + ctx.body = 'delete success'; + }) + .addTo(app); +app + .route('prompt-graph', 'deleteAll') + .define(async (ctx) => { + const prompts = await PromptNeo.all(); + for (const prompt of prompts) { + await prompt.delete(); + } + ctx.body = 'delete all success'; + }) + .addTo(app); +app + .route('prompt-graph', 'createDemo') + .define(async (ctx) => { + const promptData = { + id: v4(), + title: 'test-' + v4(), + description: '这是测试保存prompt的数据', + prompt: '这是测试保存prompt的数据', + inputVariables: JSON.stringify([{ key: 'test', value: 'test' }]), + localVariables: JSON.stringify([{ key: 'test', value: 'test' }]), + }; + const f = await PromptNeo.first('id', 'f5288cdb-bfca-4a65-b629-cae590ede719'); + if (!f) { + ctx.body = 'not found f'; + return; + } + const prompt = await PromptNeo.create({ ...promptData }); + // await prompt.relateTo(f, 'RELATED_TO', { createdAt: new Date().toISOString() }); + // f.relateTo(prompt, 'RELATED_TO', { createdAt: new Date().toISOString() }); + // await createRelationship(f, prompt); + const fj = await f.toJson() as any; + const pj = await prompt.toJson() as any; + + await createRelationship2(fj.id, pj.id); + ctx.body = await prompt.toJson(); + }) + .addTo(app); + +app + .route('prompt-graph', 'getD3') + .define(async (ctx) => { + const value = await fetchData(); + ctx.body = value; + }) + .addTo(app); diff --git a/src/routes/prompt-graph/list.ts b/src/routes/prompt-graph/list.ts index 48ea06d..d8ec5f7 100644 --- a/src/routes/prompt-graph/list.ts +++ b/src/routes/prompt-graph/list.ts @@ -1,76 +1,63 @@ -import { PromptNeo } from '@/models/prompt.ts'; +import { Prompt } from '@/models/prompt.ts'; + import { app } from '@/app.ts'; -import { v4 } from 'uuid'; +import { CustomError } from '@abearxiong/router'; + app .route('prompt', 'list') .define(async (ctx) => { - const prompts = await PromptNeo.all(); - const json = await prompts.toJson(); - console.log('json', json); - ctx.body = json; + const prompts = await Prompt.findAll({ + order: [['updatedAt', 'DESC']], + }); + ctx.body = prompts; }) .addTo(app); app .route('prompt', 'update') .define(async (ctx) => { - const { id, title, description, prompt, inputVariables, localVariables } = ctx.query; - const promptNode = await PromptNeo.first('id', id); - - if (!promptNode) { - const promptData = { - id: v4(), + const { id, title, description, presetData, key } = ctx.query.data || {}; + if (!key) { + throw new CustomError('Prompt key is required'); + } + const isEdit = !!id; + const promptKey = await Prompt.findOne({ where: { key } }); + if (promptKey && promptKey.id !== id) { + throw new CustomError(`Prompt key is already exist, use by ${promptKey.id}`); + } + if (!isEdit) { + const prompt = new Prompt({ title, + key, description, - prompt, - inputVariables: JSON.stringify(inputVariables), - localVariables: JSON.stringify(localVariables), - }; - const _prompt = await PromptNeo.create(promptData); - ctx.body = await _prompt.toJson(); + presetData, + }); + await prompt.save(); + ctx.body = prompt; return; } - await promptNode.update({ title, description, prompt, inputVariables, localVariables }); - ctx.body = await promptNode.toJson(); + const prompt = await Prompt.findByPk(id); + if (!prompt) { + throw new CustomError('Prompt not found'); + } + await prompt.update({ title, description, presetData, key }); + ctx.body = prompt; }) .addTo(app); app .route('prompt', 'delete') .define(async (ctx) => { - const { id, title } = ctx.query; - const promptNode = await PromptNeo.first('id', id); - if (!promptNode) { - ctx.body = 'prompt not found'; - return; + const { id } = ctx.query || {}; + if (!id) { + throw new CustomError('Prompt id is required'); } - await promptNode.delete(); + const prompt = await Prompt.findByPk(id); + if (!prompt) { + throw new CustomError('Prompt not found'); + } + await prompt.destroy(); ctx.body = 'delete success'; }) .addTo(app); -app - .route('prompt', 'deleteAll') - .define(async (ctx) => { - const prompts = await PromptNeo.all(); - for (const prompt of prompts) { - await prompt.delete(); - } - ctx.body = 'delete all success'; - }) - .addTo(app); -app - .route('prompt', 'createDemo') - .define(async (ctx) => { - const promptData = { - id: v4(), - title: 'test', - description: '这是测试保存prompt的数据', - prompt: '这是测试保存prompt的数据', - inputVariables: JSON.stringify([{ key: 'test', value: 'test' }]), - localVariables: JSON.stringify([{ key: 'test', value: 'test' }]), - }; - const prompt = await PromptNeo.create(promptData); - ctx.body = await prompt.toJson(); - }) - .addTo(app);