完成基础记忆操作API

main
落雨楓 6 months ago
parent b3fb6186e5
commit e6005f3a9f

@ -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,

@ -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,
};
}
}

@ -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<typeof routeConfig>[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<any> {
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<any> {
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<any> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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;
}
}

@ -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<typeof UpdatePageMetadataIte
export const CreateNewPageReq = z.object({
metadata: z.record(z.any()).describe('The metadata for the new page.').nullable().optional(),
inheritMetadata: z.boolean().describe('Whether to inherit metadata from the current page.').default(false),
}).describe('NewPageReq');
export type CreateNewPageReqType = z.infer<typeof CreateNewPageReq>;

@ -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();
swaggerRouter.swagger();
return swaggerRouter;
};

@ -22,6 +22,7 @@ export type PostgresDatabaseConfig = z.infer<typeof PostgresDatabaseConfigSchema
export const ServerConfigSchema = z.object({
debug: z.boolean().optional().describe('Whether to enable debug mode.'),
port: z.number().int().min(1).max(65535).describe('The server port.'),
public_url: z.string().optional().describe('The public URL of the server.'),
memory_max_messages: z.number().int().min(1).describe('The maximum number of messages to remember.'),
db_type: z.enum(['postgres']).optional().describe('The type of database, currently only supports Postgres.'),
db: PostgresDatabaseConfigSchema.optional().describe('Configuration for the Postgres database.'),

Loading…
Cancel
Save