feat: basic api server

This commit is contained in:
Acbox
2026-01-10 20:17:02 +08:00
parent 28aa28e5bb
commit e60c0bb0d7
31 changed files with 1421 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
import Elysia from 'elysia'
export const agentModule = new Elysia({
prefix: '/agent',
})
+3
View File
@@ -0,0 +1,3 @@
export * from './agent'
export * from './model'
export * from './settings'
+40
View File
@@ -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
}
+20
View File
@@ -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
}
+185
View File
@@ -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)
+55
View File
@@ -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'),
}),
}
+64
View File
@@ -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
}