mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: basic api server
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import Elysia from 'elysia'
|
||||
|
||||
export const agentModule = new Elysia({
|
||||
prefix: '/agent',
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './agent'
|
||||
export * from './model'
|
||||
export * from './settings'
|
||||
@@ -0,0 +1,40 @@
|
||||
import Elysia from 'elysia'
|
||||
import { messageModule } from './message'
|
||||
import { AddMemoryModel, SearchMemoryModel } from './model'
|
||||
import { addMemory, searchMemory } from './service'
|
||||
import { MemoryUnit } from '@memohome/memory'
|
||||
|
||||
export const memoryModule = new Elysia({
|
||||
prefix: '/memory',
|
||||
})
|
||||
.use(messageModule)
|
||||
// Add memory
|
||||
.post('/', async ({ body }) => {
|
||||
try {
|
||||
const result = await addMemory(body as unknown as MemoryUnit)
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to add memory',
|
||||
}
|
||||
}
|
||||
}, AddMemoryModel)
|
||||
// Search memory
|
||||
.get('/search', async ({ query }) => {
|
||||
try {
|
||||
const results = await searchMemory(query.query, query.userId)
|
||||
return {
|
||||
success: true,
|
||||
data: results,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to search memory',
|
||||
}
|
||||
}
|
||||
}, SearchMemoryModel)
|
||||
@@ -0,0 +1,22 @@
|
||||
import Elysia from 'elysia'
|
||||
import { GetMemoryMessageFilterModel, GetMemoryMessageModel } from './model'
|
||||
import { getMemoryMessages, getMemoryMessagesFilter } from './service'
|
||||
|
||||
export const messageModule = new Elysia({
|
||||
prefix: '/message',
|
||||
})
|
||||
.get('/', async ({ query }) => {
|
||||
const units = await getMemoryMessages(query)
|
||||
return {
|
||||
success: true,
|
||||
units,
|
||||
}
|
||||
}, GetMemoryMessageModel)
|
||||
.get('/filter', async ({ query }) => {
|
||||
const units = await getMemoryMessagesFilter(query)
|
||||
|
||||
return {
|
||||
units,
|
||||
success: true,
|
||||
}
|
||||
}, GetMemoryMessageFilterModel)
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const GetMemoryMessageModel = {
|
||||
query: z.object({
|
||||
limit: z.string().transform(Number).default(10),
|
||||
page: z.string().transform(Number).default(1),
|
||||
userId: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
export const GetMemoryMessageFilterModel = {
|
||||
query: z.object({
|
||||
from: z.date(),
|
||||
to: z.date(),
|
||||
userId: z.string(),
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { db } from '@memohome/db'
|
||||
import { history } from '@memohome/db/schema'
|
||||
import { eq, desc, and, gte, lte, asc } from 'drizzle-orm'
|
||||
|
||||
export const getMemoryMessages = async (
|
||||
query: {
|
||||
limit: number
|
||||
page: number
|
||||
userId: string
|
||||
}
|
||||
) => {
|
||||
const { limit, page, userId } = query
|
||||
const results = await db
|
||||
.select()
|
||||
.from(history)
|
||||
.where(eq(history.user, userId))
|
||||
.orderBy(desc(history.timestamp))
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export const getMemoryMessagesFilter = async (
|
||||
query: {
|
||||
from: Date
|
||||
to: Date
|
||||
userId: string
|
||||
}
|
||||
) => {
|
||||
const { from, to, userId } = query
|
||||
const results = await db
|
||||
.select()
|
||||
.from(history)
|
||||
.where(and(
|
||||
gte(history.timestamp, from),
|
||||
lte(history.timestamp, to),
|
||||
eq(history.user, userId),
|
||||
))
|
||||
.orderBy(asc(history.timestamp))
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// MemoryUnit schema
|
||||
const MemoryUnitSchema = z.object({
|
||||
messages: z.array(z.object()),
|
||||
timestamp: z.coerce.date(),
|
||||
user: z.string(),
|
||||
})
|
||||
|
||||
export const AddMemoryModel = {
|
||||
body: MemoryUnitSchema,
|
||||
}
|
||||
|
||||
export const SearchMemoryModel = {
|
||||
query: z.object({
|
||||
query: z.string().min(1, 'Search query is required'),
|
||||
userId: z.string().min(1, 'User ID is required'),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createMemory, MemoryUnit } from '@memohome/memory'
|
||||
import { getEmbeddingModel, getSummaryModel } from '@/modules/model/service'
|
||||
import { ChatModel, EmbeddingModel } from '@memohome/shared'
|
||||
|
||||
export const addMemory = async (memoryUnit: MemoryUnit) => {
|
||||
const [embeddingModel, summaryModel] = await Promise.all([
|
||||
getEmbeddingModel(memoryUnit.user),
|
||||
getSummaryModel(memoryUnit.user),
|
||||
])
|
||||
if (!embeddingModel || !summaryModel) {
|
||||
throw new Error('Embedding or summary model not found')
|
||||
}
|
||||
const { addMemory } = createMemory({
|
||||
summaryModel: summaryModel.model as ChatModel,
|
||||
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||
})
|
||||
await addMemory(memoryUnit)
|
||||
return memoryUnit
|
||||
}
|
||||
|
||||
export const searchMemory = async (query: string, userId: string) => {
|
||||
const [embeddingModel, summaryModel] = await Promise.all([
|
||||
getEmbeddingModel(userId),
|
||||
getSummaryModel(userId),
|
||||
])
|
||||
if (!embeddingModel || !summaryModel) {
|
||||
throw new Error('Embedding or summary model not found')
|
||||
}
|
||||
const { searchMemory } = createMemory({
|
||||
summaryModel: summaryModel.model as ChatModel,
|
||||
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||
})
|
||||
const results = await searchMemory(query, userId)
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import Elysia from 'elysia'
|
||||
import {
|
||||
CreateModelModel,
|
||||
UpdateModelModel,
|
||||
GetModelByIdModel,
|
||||
DeleteModelModel,
|
||||
GetDefaultModelModel,
|
||||
} from './model'
|
||||
import {
|
||||
getModels,
|
||||
getModelById,
|
||||
createModel,
|
||||
updateModel,
|
||||
deleteModel,
|
||||
getChatModel,
|
||||
getSummaryModel,
|
||||
getEmbeddingModel,
|
||||
} from './service'
|
||||
import { Model } from '@memohome/shared'
|
||||
|
||||
export const modelModule = new Elysia({
|
||||
prefix: '/model',
|
||||
})
|
||||
// Get all models
|
||||
.get('/', async () => {
|
||||
try {
|
||||
const models = await getModels()
|
||||
return {
|
||||
success: true,
|
||||
data: models,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch models',
|
||||
}
|
||||
}
|
||||
})
|
||||
// Get model by ID
|
||||
.get('/:id', async ({ params }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const model = await getModelById(id)
|
||||
if (!model) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Model not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: model,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch model',
|
||||
}
|
||||
}
|
||||
}, GetModelByIdModel)
|
||||
// Create new model
|
||||
.post('/', async ({ body }) => {
|
||||
try {
|
||||
const newModel = await createModel(body as Model)
|
||||
return {
|
||||
success: true,
|
||||
data: newModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create model',
|
||||
}
|
||||
}
|
||||
}, CreateModelModel)
|
||||
// Update model
|
||||
.put('/:id', async ({ params, body }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const updatedModel = await updateModel(id, body as Model)
|
||||
if (!updatedModel) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Model not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: updatedModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update model',
|
||||
}
|
||||
}
|
||||
}, UpdateModelModel)
|
||||
// Delete model
|
||||
.delete('/:id', async ({ params }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const deletedModel = await deleteModel(id)
|
||||
if (!deletedModel) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Model not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: deletedModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete model',
|
||||
}
|
||||
}
|
||||
}, DeleteModelModel)
|
||||
// Get default chat model
|
||||
.get('/chat/default', async ({ query }) => {
|
||||
try {
|
||||
const { userId } = query
|
||||
const chatModel = await getChatModel(userId)
|
||||
if (!chatModel) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Default chat model not found or not set',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: chatModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch default chat model',
|
||||
}
|
||||
}
|
||||
}, GetDefaultModelModel)
|
||||
// Get default summary model
|
||||
.get('/summary/default', async ({ query }) => {
|
||||
try {
|
||||
const { userId } = query
|
||||
const summaryModel = await getSummaryModel(userId)
|
||||
if (!summaryModel) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Default summary model not found or not set',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: summaryModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch default summary model',
|
||||
}
|
||||
}
|
||||
}, GetDefaultModelModel)
|
||||
// Get default embedding model
|
||||
.get('/embedding/default', async ({ query }) => {
|
||||
try {
|
||||
const { userId } = query
|
||||
const embeddingModel = await getEmbeddingModel(userId)
|
||||
if (!embeddingModel) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Default embedding model not found or not set',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: embeddingModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch default embedding model',
|
||||
}
|
||||
}
|
||||
}, GetDefaultModelModel)
|
||||
@@ -0,0 +1,55 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const BaseModelSchema = z.object({
|
||||
modelId: z.string().min(1, 'Model ID is required'),
|
||||
baseUrl: z.string(),
|
||||
apiKey: z.string().min(1, 'API key is required'),
|
||||
clientType: z.string(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
|
||||
// Chat model schema (type is optional and defaults to 'chat')
|
||||
const ChatModelSchema = BaseModelSchema.extend({
|
||||
type: z.enum(['chat']).optional().default('chat'),
|
||||
})
|
||||
|
||||
// Embedding model schema (type must be 'embedding' and dimensions is required)
|
||||
const EmbeddingModelSchema = BaseModelSchema.extend({
|
||||
type: z.literal('embedding'),
|
||||
dimensions: z.number().int().positive('Dimensions must be a positive integer'),
|
||||
})
|
||||
|
||||
// Union of both model types
|
||||
const ModelSchema = z.union([ChatModelSchema, EmbeddingModelSchema])
|
||||
|
||||
// Export the inferred type from the schema
|
||||
export type ModelInput = z.infer<typeof ModelSchema>
|
||||
|
||||
export const CreateModelModel = {
|
||||
body: ModelSchema,
|
||||
}
|
||||
|
||||
export const UpdateModelModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
body: ModelSchema,
|
||||
}
|
||||
|
||||
export const GetModelByIdModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
export const DeleteModelModel = {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
export const GetDefaultModelModel = {
|
||||
query: z.object({
|
||||
userId: z.string().min(1, 'User ID is required'),
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { db } from '@memohome/db'
|
||||
import { model } from '@memohome/db/schema'
|
||||
import { Model } from '@memohome/shared'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { getSettings } from '@/modules/settings/service'
|
||||
|
||||
export const getModels = async () => {
|
||||
const models = await db.select().from(model)
|
||||
return models
|
||||
}
|
||||
|
||||
export const getModelById = async (id: string) => {
|
||||
const [result] = await db.select().from(model).where(eq(model.id, id))
|
||||
return result
|
||||
}
|
||||
|
||||
export const createModel = async (data: Model) => {
|
||||
const [newModel] = await db
|
||||
.insert(model)
|
||||
.values({ model: data })
|
||||
.returning()
|
||||
return newModel
|
||||
}
|
||||
|
||||
export const updateModel = async (id: string, data: Model) => {
|
||||
const [updatedModel] = await db
|
||||
.update(model)
|
||||
.set({ model: data })
|
||||
.where(eq(model.id, id))
|
||||
.returning()
|
||||
return updatedModel
|
||||
}
|
||||
|
||||
export const deleteModel = async (id: string) => {
|
||||
const [deletedModel] = await db
|
||||
.delete(model)
|
||||
.where(eq(model.id, id))
|
||||
.returning()
|
||||
return deletedModel
|
||||
}
|
||||
|
||||
export const getChatModel = async (userId: string) => {
|
||||
const userSettings = await getSettings(userId)
|
||||
if (!userSettings?.defaultChatModel) {
|
||||
return null
|
||||
}
|
||||
return await getModelById(userSettings.defaultChatModel)
|
||||
}
|
||||
|
||||
export const getSummaryModel = async (userId: string) => {
|
||||
const userSettings = await getSettings(userId)
|
||||
if (!userSettings?.defaultSummaryModel) {
|
||||
return null
|
||||
}
|
||||
return await getModelById(userSettings.defaultSummaryModel)
|
||||
}
|
||||
|
||||
export const getEmbeddingModel = async (userId: string) => {
|
||||
const userSettings = await getSettings(userId)
|
||||
if (!userSettings?.defaultEmbeddingModel) {
|
||||
return null
|
||||
}
|
||||
return await getModelById(userSettings.defaultEmbeddingModel)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import Elysia from 'elysia'
|
||||
import {
|
||||
GetSettingsModel,
|
||||
CreateSettingsModel,
|
||||
UpdateSettingsModel,
|
||||
} from './model'
|
||||
import {
|
||||
getSettings,
|
||||
createSettings,
|
||||
updateSettings,
|
||||
upsertSettings,
|
||||
} from './service'
|
||||
|
||||
export const settingsModule = new Elysia({
|
||||
prefix: '/settings',
|
||||
})
|
||||
// Get user settings
|
||||
.get('/:userId', async ({ params }) => {
|
||||
try {
|
||||
const { userId } = params
|
||||
const userSettings = await getSettings(userId)
|
||||
if (!userSettings) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Settings not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: userSettings,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch settings',
|
||||
}
|
||||
}
|
||||
}, GetSettingsModel)
|
||||
// Create new settings
|
||||
.post('/', async ({ body }) => {
|
||||
try {
|
||||
const newSettings = await createSettings(body)
|
||||
return {
|
||||
success: true,
|
||||
data: newSettings,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create settings',
|
||||
}
|
||||
}
|
||||
}, CreateSettingsModel)
|
||||
// Update settings
|
||||
.put('/:userId', async ({ params, body }) => {
|
||||
try {
|
||||
const { userId } = params
|
||||
const updatedSettings = await updateSettings(userId, body)
|
||||
if (!updatedSettings) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Settings not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: updatedSettings,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update settings',
|
||||
}
|
||||
}
|
||||
}, UpdateSettingsModel)
|
||||
// Upsert settings (create or update)
|
||||
.patch('/', async ({ body }) => {
|
||||
try {
|
||||
const result = await upsertSettings(body)
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to upsert settings',
|
||||
}
|
||||
}
|
||||
}, CreateSettingsModel)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const SettingsSchema = z.object({
|
||||
userId: z.string().min(1, 'User ID is required'),
|
||||
defaultChatModel: z.string().uuid().nullable().optional(),
|
||||
defaultEmbeddingModel: z.string().uuid().nullable().optional(),
|
||||
defaultSummaryModel: z.string().uuid().nullable().optional(),
|
||||
})
|
||||
|
||||
export type SettingsInput = z.infer<typeof SettingsSchema>
|
||||
|
||||
export const GetSettingsModel = {
|
||||
params: z.object({
|
||||
userId: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
export const CreateSettingsModel = {
|
||||
body: SettingsSchema,
|
||||
}
|
||||
|
||||
export const UpdateSettingsModel = {
|
||||
params: z.object({
|
||||
userId: z.string(),
|
||||
}),
|
||||
body: z.object({
|
||||
defaultChatModel: z.string().uuid().nullable().optional(),
|
||||
defaultEmbeddingModel: z.string().uuid().nullable().optional(),
|
||||
defaultSummaryModel: z.string().uuid().nullable().optional(),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { db } from '@memohome/db'
|
||||
import { settings } from '@memohome/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { SettingsInput } from './model'
|
||||
|
||||
export const getSettings = async (userId: string) => {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(settings)
|
||||
.where(eq(settings.userId, userId))
|
||||
return result
|
||||
}
|
||||
|
||||
export const createSettings = async (data: SettingsInput) => {
|
||||
const [newSettings] = await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
userId: data.userId,
|
||||
defaultChatModel: data.defaultChatModel || null,
|
||||
defaultEmbeddingModel: data.defaultEmbeddingModel || null,
|
||||
defaultSummaryModel: data.defaultSummaryModel || null,
|
||||
})
|
||||
.returning()
|
||||
return newSettings
|
||||
}
|
||||
|
||||
export const updateSettings = async (
|
||||
userId: string,
|
||||
data: Partial<Omit<SettingsInput, 'userId'>>
|
||||
) => {
|
||||
const [updatedSettings] = await db
|
||||
.update(settings)
|
||||
.set({
|
||||
defaultChatModel: data.defaultChatModel,
|
||||
defaultEmbeddingModel: data.defaultEmbeddingModel,
|
||||
defaultSummaryModel: data.defaultSummaryModel,
|
||||
})
|
||||
.where(eq(settings.userId, userId))
|
||||
.returning()
|
||||
return updatedSettings
|
||||
}
|
||||
|
||||
export const upsertSettings = async (data: SettingsInput) => {
|
||||
const [result] = await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
userId: data.userId,
|
||||
defaultChatModel: data.defaultChatModel || null,
|
||||
defaultEmbeddingModel: data.defaultEmbeddingModel || null,
|
||||
defaultSummaryModel: data.defaultSummaryModel || null,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: settings.userId,
|
||||
set: {
|
||||
defaultChatModel: data.defaultChatModel,
|
||||
defaultEmbeddingModel: data.defaultEmbeddingModel,
|
||||
defaultSummaryModel: data.defaultSummaryModel,
|
||||
},
|
||||
})
|
||||
.returning()
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user