diff --git a/schemas/ServerConfig.json b/schemas/ServerConfig.json index 9c6d08a..a21f31b 100644 --- a/schemas/ServerConfig.json +++ b/schemas/ServerConfig.json @@ -11,6 +11,10 @@ "maximum": 65535, "description": "The server port." }, + "public_url": { + "type": "string", + "description": "The public URL of the server." + }, "memory_max_messages": { "type": "integer", "minimum": 1, diff --git a/src/App.ts b/src/App.ts index ac09eac..585aac4 100644 --- a/src/App.ts +++ b/src/App.ts @@ -15,7 +15,7 @@ import { Entities } from './entity/index.js'; import { ServerConfig } from './schemas/ServerConfig.js'; import { LLMApiProxies } from './proxies/index.js'; import { MemoryController } from './controllers/MemoryController.js'; -import { swaggerRouter } from './router.js'; +import { createSwaggerRouter } from './router.js'; import { SwaggerRouter, z } from 'koa-swagger-decorator'; export default class App { @@ -197,7 +197,7 @@ export default class App { this.webApp.context.application = this; this.webApp.context.database = this.db; - this.router = swaggerRouter; + this.router = createSwaggerRouter(this.config); if (this.debug) { this.webApp.use(koaLogger()); @@ -234,7 +234,7 @@ export default class App { ctx.status = 500; // Internal Server Error ctx.body = { success: false, - message: 'Internal server error', + message: 'Internal server error: ' + error.message, }; } } diff --git a/src/controllers/MemoryController.ts b/src/controllers/MemoryController.ts index 12cc5bf..984675d 100644 --- a/src/controllers/MemoryController.ts +++ b/src/controllers/MemoryController.ts @@ -5,7 +5,7 @@ import { ConversationPage } from "#app/entity/ConversationPage.js"; import { Logger } from "#app/utils/logger.js"; import Koa, { Next } from "koa"; import Router from "koa-router"; -import { ActionRes, CreateNewPageReq, CreateNewPageReqType, GetConversationInfoRes, GetConversationInfoResType, GetDialogueInfoRes, GetDialogueInfoResType, GetMessageContentQuery, GetPageInfoRes, GetPageInfoResType, GetPageListRes, GetPageListResType, OverrideDialogueMessagesReq, OverrideDialogueMessagesReqType, UpdateDialogueMessagesReq, UpdateDialogueMessagesReqType, type ActionResType } from "./schemas/MemoryControllerSchemas.js"; +import { ActionRes, CreateNewPageReq, CreateNewPageReqType, GetConversationInfoRes, GetConversationInfoResType, GetDialogueInfoRes, GetDialogueInfoResType, GetMessageContentQuery, GetPageInfoRes, GetPageInfoResType, GetPageListRes, GetPageListResType, GetPageMetadataRes, GetPageMetadataResType, OverrideDialogueMessagesReq, OverrideDialogueMessagesReqType, pageIdSchema, UpdateDialogueMessagesReq, UpdateDialogueMessagesReqType, UpdatePageMetadataReq, UpdatePageMetadataReqType, type ActionResType } from "./schemas/MemoryControllerSchemas.js"; import { body, responses, routeConfig, SwaggerRouter, z } from "koa-swagger-decorator"; type ResponsesConfig = Parameters[0]['responses']; @@ -484,12 +484,33 @@ export class MemoryController { const body = ctx.request.body as CreateNewPageReqType; + let newMetadata: any = body.metadata ?? {}; + if (!(typeof newMetadata === 'object')) { + newMetadata = {}; + } + const conversation = await this.getConversationInfo(ctx); if (!conversation) { return; } - await conversation.createNewPage(ctx.application, body.metadata ?? undefined); + if (body.inheritMetadata) { + const currentPage = await ctx.database!.getRepository(ConversationPage) + .findOneBy({ conversationId: conversation.conversationId, pageId: conversation.currentPage }); + if (currentPage) { + let currentMetadata = currentPage.metadata; + if (!(typeof currentMetadata === 'object')) { + currentMetadata = {}; + } + + newMetadata = { + ...currentMetadata, + ...newMetadata, + }; + } + } + + await conversation.createNewPage(ctx.application, newMetadata ?? undefined); ctx.status = 200; ctx.body = { @@ -506,7 +527,7 @@ export class MemoryController { request: { params: z.object({ conversationId: z.string().describe('The conversation ID to retrieve the page for.'), - pageId: z.number().describe('The page ID to retrieve information for.'), + pageId: pageIdSchema, }), }, responses: DefaultResponses, @@ -545,4 +566,336 @@ export class MemoryController { return pageData; } + + @routeConfig({ + method: 'get', + path: '/memory/v1/{conversationId}/page/{pageId}/metadata', + summary: 'Get metadata of a specific page in a conversation', + operationId: 'getPageMetadata', + request: { + params: z.object({ + conversationId: z.string().describe('The conversation ID to retrieve the page for.'), + pageId: pageIdSchema, + }), + }, + responses: DefaultResponses, + }) + @responses(GetPageMetadataRes) + public async getPageMetadata(ctx: Koa.ParameterizedContext): Promise { + const page = await this.getPageInfo(ctx); + if (!page) { + ctx.status = 404; + ctx.body = { error: 'Page not found' }; + return null; + } + + ctx.status = 200; + ctx.body = { + metadata: page.metadata, + } as GetPageMetadataResType; + + return page.metadata; + } + + @routeConfig({ + method: 'get', + path: '/memory/v1/{conversationId}/page/{pageId}/metadata/{varKey}', + summary: 'Get a specific metadata variable from a page in a conversation', + operationId: 'getPageMetadataVariable', + request: { + params: z.object({ + conversationId: z.string().describe('The conversation ID to retrieve the page for.'), + pageId: pageIdSchema, + varKey: z.string().describe('The key of the metadata variable to retrieve.'), + }), + }, + responses: DefaultResponses, + }) + @responses(z.string() as any) + public async getPageMetadataVariable(ctx: Koa.ParameterizedContext): Promise { + const page = await this.getPageInfo(ctx); + if (!page) { + ctx.status = 404; + ctx.body = { error: 'Page not found' }; + return null; + } + + const varKey = ctx.params.varKey as string; + const metadataValue = page.metadata[varKey]; + + if (metadataValue === undefined) { + ctx.status = 404; + ctx.body = { error: `Metadata variable "${varKey}" not found` }; + return null; + } + + ctx.status = 200; + ctx.body = metadataValue; + + return metadataValue; + } + + @routeConfig({ + method: 'get', + path: '/memory/v1/{conversationId}/page/{pageId}/metadata/{varKey}/json', + summary: 'Get a specific metadata variable from a page in a conversation as JSON', + operationId: 'getPageMetadataVariableJson', + request: { + params: z.object({ + conversationId: z.string().describe('The conversation ID to retrieve the page for.'), + pageId: pageIdSchema, + varKey: z.string().describe('The key of the metadata variable to retrieve.'), + }), + }, + responses: DefaultResponses, + }) + @responses(z.string() as any) + public async getPageMetadataVariableJson(ctx: Koa.ParameterizedContext): Promise { + const metadataValue = await this.getPageMetadataVariable(ctx); + if (metadataValue === null) { + return null; + } + + try { + const jsonValue = JSON.stringify(metadataValue); + ctx.status = 200; + ctx.type = 'application/json'; + ctx.body = jsonValue; + return jsonValue; + } catch (error) { + ctx.status = 500; + ctx.body = { error: `Failed to convert metadata variable "${ctx.params.varKey}" to JSON` }; + return null; + } + } + + @routeConfig({ + method: 'post', + path: '/memory/v1/{conversationId}/page/{pageId}/metadata', + summary: 'Update metadata of a specific page in a conversation', + operationId: 'updatePageMetadata', + request: { + params: z.object({ + conversationId: z.string().describe('The conversation ID to update the page metadata for.'), + pageId: pageIdSchema, + }), + query: z.object({ + override: z.boolean().optional().default(false).describe('Whether to override existing metadata.'), + }), + }, + responses: DefaultResponses, + }) + @body(UpdatePageMetadataReq) + @responses(ActionRes) + public async updatePageMetadata(ctx: Koa.ParameterizedContext): Promise { + if (!this.checkDb(ctx)) { + return; + } + + try { + UpdatePageMetadataReq.parse(ctx.request.body); + } catch (error: any) { + ctx.status = 422; + ctx.body = { + success: false, + message: 'Invalid request body', + error: error.message, + } as ActionResType; + return; + } + const body = ctx.request.body as UpdatePageMetadataReqType; + + const page = await this.getPageInfo(ctx); + if (!page) { + return; + } + + if (typeof page.metadata !== 'object') { + page.metadata = {}; + } + + if (ctx.query.override) { + page.metadata = body.metadata; + } else { + page.metadata = { ...page.metadata, ...body.metadata }; + } + + await ctx.database!.getRepository(ConversationPage) + .save(page); + + ctx.status = 200; + ctx.body = { + success: true, + message: 'OK', + } as ActionResType; + } + + @routeConfig({ + method: 'post', + path: '/memory/v1/{conversationId}/page/{pageId}/metadata/{varKey}', + summary: 'Update a specific metadata variable of a page in a conversation', + operationId: 'updatePageMetadataVariable', + request: { + params: z.object({ + conversationId: z.string().describe('The conversation ID to update the page metadata for.'), + pageId: pageIdSchema, + varKey: z.string().describe('The key of the metadata variable to update.'), + }), + }, + responses: DefaultResponses, + }) + @body(z.string().describe('The new value for the metadata variable.') as any) + @responses(ActionRes) + public async updatePageMetadataVariable(ctx: Koa.ParameterizedContext): Promise { + if (!this.checkDb(ctx)) { + return; + } + + const page = await this.getPageInfo(ctx); + if (!page) { + return; + } + + const varKey = ctx.params.varKey as string; + const newValue = ctx.request.body as string; + + if (typeof page.metadata !== 'object') { + page.metadata = {}; + } + + page.metadata[varKey] = newValue; + + await ctx.database!.getRepository(ConversationPage) + .save(page); + + ctx.status = 200; + ctx.body = { + success: true, + message: `OK`, + } as ActionResType; + } + + @routeConfig({ + method: 'post', + path: '/memory/v1/{conversationId}/page/{pageId}/metadata/{varKey}/json', + summary: 'Update a specific metadata variable of a page in a conversation as JSON', + operationId: 'updatePageMetadataVariableJson', + request: { + params: z.object({ + conversationId: z.string().describe('The conversation ID to update the page metadata for.'), + pageId: pageIdSchema, + varKey: z.string().describe('The key of the metadata variable to update.'), + }), + }, + responses: DefaultResponses, + }) + @body(z.string().describe('The new value for the metadata variable as JSON.') as any) + @responses(ActionRes) + public async updatePageMetadataVariableJson(ctx: Koa.ParameterizedContext): Promise { + if (!this.checkDb(ctx)) { + return; + } + + const page = await this.getPageInfo(ctx); + if (!page) { + return; + } + + const varKey = ctx.params.varKey as string; + const newValue = ctx.request.body as string; + + if (typeof page.metadata !== 'object') { + page.metadata = {}; + } + + page.metadata[varKey] = JSON.parse(newValue); + + await ctx.database!.getRepository(ConversationPage) + .save(page); + + ctx.status = 200; + ctx.body = { + success: true, + message: `OK`, + } as ActionResType; + } + + @routeConfig({ + method: 'delete', + path: '/memory/v1/{conversationId}/page/{pageId}/metadata', + summary: 'Clear metadata of a specific page in a conversation', + operationId: 'deletePageMetadata', + request: { + params: z.object({ + conversationId: z.string().describe('The conversation ID to clear the page metadata for.'), + pageId: pageIdSchema, + }), + }, + responses: DefaultResponses, + }) + @responses(ActionRes) + public async deletePageMetadata(ctx: Koa.ParameterizedContext): Promise { + if (!this.checkDb(ctx)) { + return; + } + + const page = await this.getPageInfo(ctx); + if (!page) { + return; + } + + page.metadata = {}; + + await ctx.database!.getRepository(ConversationPage) + .save(page); + + ctx.status = 200; + ctx.body = { + success: true, + message: `OK`, + } as ActionResType; + } + + @routeConfig({ + method: 'delete', + path: '/memory/v1/{conversationId}/page/{pageId}/metadata/{varKey}', + summary: 'Delete a specific metadata variable from a page in a conversation', + operationId: 'deletePageMetadataVariable', + request: { + params: z.object({ + conversationId: z.string().describe('The conversation ID to delete the page metadata variable from.'), + pageId: pageIdSchema, + varKey: z.string().describe('The key of the metadata variable to delete.'), + }), + }, + responses: DefaultResponses, + }) + @responses(ActionRes) + public async deletePageMetadataVariable(ctx: Koa.ParameterizedContext): Promise { + if (!this.checkDb(ctx)) { + return; + } + + const page = await this.getPageInfo(ctx); + if (!page) { + return; + } + + const varKey = ctx.params.varKey as string; + + if (typeof page.metadata !== 'object' || !page.metadata) { + page.metadata = {}; + } + + delete page.metadata[varKey]; + + await ctx.database!.getRepository(ConversationPage) + .save(page); + + ctx.status = 200; + ctx.body = { + success: true, + message: `OK`, + } as ActionResType; + } } \ No newline at end of file diff --git a/src/controllers/schemas/MemoryControllerSchemas.ts b/src/controllers/schemas/MemoryControllerSchemas.ts index a9b6704..02962e1 100644 --- a/src/controllers/schemas/MemoryControllerSchemas.ts +++ b/src/controllers/schemas/MemoryControllerSchemas.ts @@ -1,6 +1,11 @@ import { SB } from '@serafin/schema-builder'; import { z } from 'koa-swagger-decorator'; +export const pageIdSchema = z.union([ + z.number().int().min(0).describe('The page ID of the conversation.'), + z.enum(['current']).describe('The special value "current" to refer to the current page.'), +]).describe('The page ID of the conversation, either as a number or the special value "current".'); + export const ConversationDataSchema = z.object({ conversationId: z.string().describe('The unique identifier for the conversation.'), llmApi: z.string().describe('The API id of the LLM used in the conversation.'), @@ -113,6 +118,7 @@ export type UpdatePageMetadataItemReqType = z.infer; diff --git a/src/router.ts b/src/router.ts index e1b3ade..768739e 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,27 +1,34 @@ import Router from 'koa-router'; import { SwaggerRouter } from 'koa-swagger-decorator'; +import { ServerConfig } from './schemas/ServerConfig.js'; -export const swaggerRouter = new SwaggerRouter({ - spec: { - info: { - title: 'Memory Proxy for Dify API', - description: 'This is a memory proxy for Dify API, which is used to store and retrieve data in memory.', - version: '1.0.0', +export const createSwaggerRouter = (config: ServerConfig) => { + let publicUrl = config.public_url || `http://localhost:${config.port}`; + + const swaggerRouter = new SwaggerRouter({ + spec: { + info: { + title: 'Memory Proxy for Dify API', + description: 'This is a memory proxy for Dify API, which is used to store and retrieve data in memory.', + version: '1.0.0', + }, + servers: [ + { + url: publicUrl, + description: 'Main server', + } + ], + security: [ + { + BearerAuth: [] + } + ], }, - servers: [ - { - url: 'http://localhost:8616', - description: 'Local server', - } - ], - security: [ - { - BearerAuth: [] - } - ], - }, - swaggerHtmlEndpoint: '/docs', - swaggerJsonEndpoint: '/openapi.json', -}); + swaggerHtmlEndpoint: '/docs', + swaggerJsonEndpoint: '/openapi.json', + }); -swaggerRouter.swagger(); \ No newline at end of file + swaggerRouter.swagger(); + + return swaggerRouter; +}; \ No newline at end of file diff --git a/src/schemas/ServerConfig.ts b/src/schemas/ServerConfig.ts index 3698942..2229e18 100644 --- a/src/schemas/ServerConfig.ts +++ b/src/schemas/ServerConfig.ts @@ -22,6 +22,7 @@ export type PostgresDatabaseConfig = z.infer