mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
refactor: use mem0 as long-memory maneger
This commit is contained in:
@@ -1 +1,2 @@
|
||||
screenshot-*.png
|
||||
screenshot-*.png
|
||||
memory.db
|
||||
@@ -7,11 +7,27 @@ A command-line interface for the personal housekeeper assistant agent.
|
||||
1. Create a `.env` file in the project root (not in this directory) with the following variables:
|
||||
|
||||
```env
|
||||
# Main chat model
|
||||
MODEL=gpt-4o
|
||||
BASE_URL=https://api.openai.com/v1
|
||||
API_KEY=your-api-key-here
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
MODEL_CLIENT_TYPE=openai
|
||||
|
||||
# Embedding model for memory search (using mem0ai)
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
EMBEDDING_BASE_URL=https://api.openai.com/v1
|
||||
EMBEDDING_API_KEY=your-api-key-here
|
||||
EMBEDDING_CLIENT_TYPE=openai
|
||||
EMBEDDING_DIMENSIONS=1536
|
||||
|
||||
# Summary model for memory generation (optional, defaults to main model)
|
||||
SUMMARY_MODEL=gpt-4o-mini
|
||||
SUMMARY_BASE_URL=https://api.openai.com/v1
|
||||
SUMMARY_API_KEY=your-api-key-here
|
||||
SUMMARY_CLIENT_TYPE=openai
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/byte
|
||||
```
|
||||
|
||||
2. Make sure the database is set up and running (required for memory storage).
|
||||
@@ -33,9 +49,9 @@ bun run index.ts
|
||||
## Features
|
||||
|
||||
- **Interactive Chat**: Type your messages and get responses from the AI agent
|
||||
- **Long-term Memory**: Conversations are automatically saved and can be recalled
|
||||
- **Long-term Memory**: Conversations are automatically saved with LLM-generated summaries
|
||||
- **Context Loading**: Automatically loads recent conversations (last 60 minutes)
|
||||
- **Memory Search**: The agent can search through past conversations using natural language
|
||||
- **Memory Search**: The agent can search through past conversations using natural language and embeddings
|
||||
- **Tool Calling**: Supports automatic tool execution with multi-step reasoning
|
||||
- **Multi-Provider Support**: Works with OpenAI, Anthropic, and Google AI (via Vercel AI SDK)
|
||||
|
||||
@@ -46,9 +62,46 @@ bun run index.ts
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `MODEL`: The LLM model ID (e.g., `gpt-4o`, `claude-3-5-sonnet-20241022`, `gemini-pro`)
|
||||
- `BASE_URL`: The API base URL
|
||||
- `API_KEY`: Your API key
|
||||
- `EMBEDDING_MODEL`: The embedding model for memory search (e.g., `text-embedding-3-small`)
|
||||
- `MODEL_CLIENT_TYPE`: The model provider type (default: `openai`, options: `openai`, `anthropic`, `google`)
|
||||
### Required
|
||||
|
||||
- `MODEL`: The main LLM model ID (e.g., `gpt-4o`, `claude-3-5-sonnet-20241022`)
|
||||
- `BASE_URL`: The API base URL for the main model
|
||||
- `API_KEY`: Your API key for the main model
|
||||
- `EMBEDDING_MODEL`: The embedding model for memory search (e.g., `text-embedding-3-small`)
|
||||
- `DATABASE_URL`: PostgreSQL connection string with pgvector extension enabled
|
||||
|
||||
### Optional
|
||||
|
||||
- `MODEL_CLIENT_TYPE`: The model provider type (default: `openai`, options: `openai`, `anthropic`, `google`)
|
||||
- `EMBEDDING_BASE_URL`: Base URL for embedding API (default: same as `BASE_URL`)
|
||||
- `EMBEDDING_API_KEY`: API key for embedding (default: same as `API_KEY`)
|
||||
- `EMBEDDING_CLIENT_TYPE`: Provider type for embedding (default: `openai`)
|
||||
- `EMBEDDING_DIMENSIONS`: The dimensions of the embedding model (default: `1536`)
|
||||
- `SUMMARY_MODEL`: The model used to summarize conversations for memory (default: same as `MODEL`)
|
||||
- `SUMMARY_BASE_URL`: Base URL for summary model (default: same as `BASE_URL`)
|
||||
- `SUMMARY_API_KEY`: API key for summary model (default: same as `API_KEY`)
|
||||
- `SUMMARY_CLIENT_TYPE`: Provider type for summary model (default: same as `MODEL_CLIENT_TYPE`)
|
||||
|
||||
## Memory System
|
||||
|
||||
The agent uses [mem0ai](https://github.com/mem0ai/mem0) for sophisticated memory management:
|
||||
|
||||
1. **Conversation Storage**: After each conversation, mem0ai uses an LLM to extract and store key information
|
||||
2. **Embedding Generation**: The extracted information is converted to embedding vectors using your configured embedding model
|
||||
3. **Vector Storage**: Embeddings are stored in PostgreSQL with pgvector extension
|
||||
4. **Semantic Search**: The agent can search past memories using natural language queries via vector similarity
|
||||
5. **Context Loading**: Recent conversations are automatically loaded from history into context
|
||||
|
||||
### Model Recommendations
|
||||
|
||||
- **Main Model**: Use a powerful model like `gpt-4o` or `claude-3-5-sonnet-20241022` for best conversation quality
|
||||
- **Summary Model**: Use a cheaper/faster model like `gpt-4o-mini` for memory extraction to save costs
|
||||
- **Embedding Model**: Use `text-embedding-3-small` (1536 dims) or `text-embedding-3-large` (3072 dims) for OpenAI
|
||||
|
||||
### Database Requirements
|
||||
|
||||
The memory system requires PostgreSQL with the `pgvector` extension installed. Make sure:
|
||||
1. PostgreSQL is installed and running
|
||||
2. pgvector extension is enabled: `CREATE EXTENSION IF NOT EXISTS vector;`
|
||||
3. mem0ai will automatically create the required tables on first run
|
||||
|
||||
|
||||
@@ -1,36 +1,61 @@
|
||||
import { createInterface } from 'node:readline'
|
||||
import { stdin as input, stdout as output } from 'node:process'
|
||||
import { createAgent } from '../src/agent'
|
||||
import { createMemorySearch, createAddMemory, filterByTimestamp, MemoryUnit } from '@memohome/memory'
|
||||
import { ModelClientType } from '@memohome/shared'
|
||||
import { createMemory, filterByTimestamp, MemoryUnit } from '@memohome/memory'
|
||||
import { ModelClientType, ChatModel, EmbeddingModel } from '@memohome/shared'
|
||||
|
||||
// Load environment variables
|
||||
const MODEL = process.env.MODEL
|
||||
const BASE_URL = process.env.BASE_URL
|
||||
const API_KEY = process.env.API_KEY
|
||||
const EMBEDDING_MODEL = process.env.EMBEDDING_MODEL
|
||||
const MODEL_CLIENT_TYPE = process.env.MODEL_CLIENT_TYPE || 'openai'
|
||||
|
||||
const EMBEDDING_MODEL = process.env.EMBEDDING_MODEL
|
||||
const EMBEDDING_BASE_URL = process.env.EMBEDDING_BASE_URL || BASE_URL
|
||||
const EMBEDDING_API_KEY = process.env.EMBEDDING_API_KEY || API_KEY
|
||||
const EMBEDDING_CLIENT_TYPE = process.env.EMBEDDING_CLIENT_TYPE || 'openai'
|
||||
const EMBEDDING_DIMENSIONS = parseInt(process.env.EMBEDDING_DIMENSIONS || '1536', 10)
|
||||
|
||||
const SUMMARY_MODEL = process.env.SUMMARY_MODEL || MODEL
|
||||
const SUMMARY_BASE_URL = process.env.SUMMARY_BASE_URL || BASE_URL
|
||||
const SUMMARY_API_KEY = process.env.SUMMARY_API_KEY || API_KEY
|
||||
const SUMMARY_CLIENT_TYPE = process.env.SUMMARY_CLIENT_TYPE || MODEL_CLIENT_TYPE
|
||||
|
||||
if (!MODEL || !BASE_URL || !API_KEY || !EMBEDDING_MODEL) {
|
||||
console.error('Error: Missing required environment variables')
|
||||
console.error('Required: MODEL, BASE_URL, API_KEY, EMBEDDING_MODEL')
|
||||
console.error('Optional: MODEL_CLIENT_TYPE (default: openai)')
|
||||
console.error('Optional: SUMMARY_MODEL (default: same as MODEL)')
|
||||
console.error('Optional: SUMMARY_BASE_URL (default: same as BASE_URL)')
|
||||
console.error('Optional: SUMMARY_API_KEY (default: same as API_KEY)')
|
||||
console.error('Optional: SUMMARY_CLIENT_TYPE (default: same as MODEL_CLIENT_TYPE)')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const USER_ID = 'cli-user'
|
||||
|
||||
// Create memory functions
|
||||
const searchMemory = createMemorySearch({
|
||||
model: EMBEDDING_MODEL,
|
||||
apiKey: API_KEY,
|
||||
baseURL: BASE_URL,
|
||||
})
|
||||
// Create model configurations
|
||||
const embeddingModel: EmbeddingModel = {
|
||||
modelId: EMBEDDING_MODEL!,
|
||||
baseUrl: EMBEDDING_BASE_URL!,
|
||||
apiKey: EMBEDDING_API_KEY!,
|
||||
clientType: EMBEDDING_CLIENT_TYPE as ModelClientType,
|
||||
dimensions: EMBEDDING_DIMENSIONS,
|
||||
name: `Embedding: ${EMBEDDING_MODEL}`,
|
||||
}
|
||||
|
||||
const addMemory = createAddMemory({
|
||||
model: EMBEDDING_MODEL,
|
||||
apiKey: API_KEY,
|
||||
baseURL: BASE_URL,
|
||||
const summaryModel: ChatModel = {
|
||||
modelId: SUMMARY_MODEL!,
|
||||
baseUrl: SUMMARY_BASE_URL!,
|
||||
apiKey: SUMMARY_API_KEY!,
|
||||
clientType: SUMMARY_CLIENT_TYPE as ModelClientType,
|
||||
name: `Summary: ${SUMMARY_MODEL}`,
|
||||
}
|
||||
|
||||
// Create memory instance
|
||||
const memoryInstance = createMemory({
|
||||
summaryModel,
|
||||
embeddingModel,
|
||||
})
|
||||
|
||||
// Create agent
|
||||
@@ -48,17 +73,19 @@ const agent = createAgent({
|
||||
return await filterByTimestamp(from, to, USER_ID)
|
||||
},
|
||||
onSearchMemory: async (query: string) => {
|
||||
return await searchMemory({ user: USER_ID, query, maxResults: 5 })
|
||||
const results = await memoryInstance.searchMemory(query, USER_ID)
|
||||
// Transform search results to MemoryUnit format
|
||||
// Note: mem0ai returns semantic search results, not full conversation history
|
||||
return results
|
||||
},
|
||||
onFinish: async (messages) => {
|
||||
// Save conversation to memory - type conversion handled internally
|
||||
// Save conversation to memory
|
||||
const memoryUnit: MemoryUnit = {
|
||||
messages: messages as unknown as MemoryUnit['messages'],
|
||||
timestamp: new Date(),
|
||||
user: USER_ID,
|
||||
raw: '', // will be generated by addMemory
|
||||
}
|
||||
await addMemory({ memory: memoryUnit })
|
||||
await memoryInstance.addMemory(memoryUnit)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -67,7 +94,7 @@ async function main() {
|
||||
console.log('Type your message and press Enter. Type "exit" to quit.\n')
|
||||
|
||||
// Load context
|
||||
await agent.loadContext()
|
||||
// await agent.loadContext()
|
||||
|
||||
const rl = createInterface({ input, output })
|
||||
|
||||
|
||||
@@ -14,11 +14,19 @@
|
||||
"@ai-sdk/anthropic": "^3.0.9",
|
||||
"@ai-sdk/google": "^3.0.6",
|
||||
"@ai-sdk/openai": "^3.0.7",
|
||||
"@memohome/ai-gateway": "workspace:*",
|
||||
"@memohome/memory": "workspace:*",
|
||||
"@memohome/shared": "workspace:*",
|
||||
"ai": "^6.0.25",
|
||||
"dotenv": "^17.2.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"xsai": "^0.4.1",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"sqlite3",
|
||||
"mem0ai"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,17 @@ import { streamText, ModelMessage, stepCountIs } from 'ai'
|
||||
import { AgentParams } from './types'
|
||||
import { system } from './prompts'
|
||||
import { getMemoryTools } from './tools'
|
||||
import { MemoryUnit } from '@memohome/memory'
|
||||
import { createGateway } from './gateway'
|
||||
import { createChatGateway } from '@memohome/ai-gateway'
|
||||
|
||||
export const createAgent = (params: AgentParams) => {
|
||||
const messages: ModelMessage[] = []
|
||||
const memory: MemoryUnit[] = []
|
||||
|
||||
const gateway = createGateway(params.model)
|
||||
const gateway = createChatGateway(params.model)
|
||||
|
||||
const getTools = async () => {
|
||||
return {
|
||||
...getMemoryTools({
|
||||
searchMemory: params.onSearchMemory ?? (() => Promise.resolve([])),
|
||||
onLoadMemory: async (memory) => {
|
||||
memory.push(...memory)
|
||||
},
|
||||
searchMemory: params.onSearchMemory ?? (() => Promise.resolve([]))
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -36,12 +31,11 @@ export const createAgent = (params: AgentParams) => {
|
||||
language: params.language ?? 'Same as user input',
|
||||
locale: params.locale,
|
||||
maxContextLoadTime: params.maxContextLoadTime,
|
||||
memory,
|
||||
})
|
||||
}
|
||||
|
||||
async function* ask(input: string) {
|
||||
await loadContext()
|
||||
// await loadContext()
|
||||
const user = {
|
||||
role: 'user',
|
||||
content: input,
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { MemoryUnit } from '@memohome/memory'
|
||||
import { block, quote } from './utils'
|
||||
import { quote } from './utils'
|
||||
|
||||
export interface SystemParams {
|
||||
date: Date
|
||||
locale?: Intl.LocalesArgument
|
||||
language: string
|
||||
maxContextLoadTime: number
|
||||
memory: MemoryUnit[]
|
||||
}
|
||||
|
||||
export const system = ({ date, locale, language, maxContextLoadTime, memory }: SystemParams) => {
|
||||
export const system = ({ date, locale, language, maxContextLoadTime }: SystemParams) => {
|
||||
return `
|
||||
---
|
||||
date: ${date.toLocaleDateString(locale)}
|
||||
@@ -26,9 +24,5 @@ export const system = ({ date, locale, language, maxContextLoadTime, memory }: S
|
||||
**Memory**
|
||||
- Your context has been loaded from the last ${maxContextLoadTime} minutes.
|
||||
- You can use ${quote('search-memory')} to search for past memories with natural language.
|
||||
- The search result is performed as chat history, load into your system prompt as a context.
|
||||
|
||||
**Past Memory Loaded**
|
||||
${block(memory.map(m => m.raw).join('\n\n'), 'memory')}
|
||||
`.trim()
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import { MemoryUnit } from '@memohome/memory'
|
||||
import { tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
|
||||
export interface GetMemoryToolParams {
|
||||
searchMemory: (query: string) => Promise<MemoryUnit[]>
|
||||
onLoadMemory: (memory: MemoryUnit[]) => Promise<void>
|
||||
searchMemory: (query: string) => Promise<object[]>
|
||||
}
|
||||
|
||||
export const getMemoryTools = ({ searchMemory, onLoadMemory }: GetMemoryToolParams) => {
|
||||
export const getMemoryTools = ({ searchMemory }: GetMemoryToolParams) => {
|
||||
const searchMemoryTool = tool({
|
||||
description: 'Search chat history in the memory',
|
||||
inputSchema: z.object({
|
||||
@@ -15,10 +13,10 @@ export const getMemoryTools = ({ searchMemory, onLoadMemory }: GetMemoryToolPara
|
||||
}),
|
||||
execute: async ({ query }) => {
|
||||
const memory = await searchMemory(query)
|
||||
onLoadMemory(memory)
|
||||
console.log(memory)
|
||||
return {
|
||||
success: true,
|
||||
message: `${memory.length} memories has load into your context`,
|
||||
memories: memory,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MemoryUnit } from '@memohome/memory'
|
||||
import { BaseModel } from '@memohome/shared'
|
||||
import { ChatModel } from '@memohome/shared'
|
||||
import { ModelMessage } from 'ai'
|
||||
|
||||
export interface AgentParams {
|
||||
model: BaseModel
|
||||
model: ChatModel
|
||||
|
||||
/**
|
||||
* Unit: minutes
|
||||
@@ -20,7 +20,7 @@ export interface AgentParams {
|
||||
|
||||
onReadMemory?: (from: Date, to: Date) => Promise<MemoryUnit[]>
|
||||
|
||||
onSearchMemory?: (query: string) => Promise<MemoryUnit[]>
|
||||
onSearchMemory?: (query: string) => Promise<object[]>
|
||||
|
||||
onFinish?: (messages: ModelMessage[]) => Promise<void>
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# @memohome/ai-gateway
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@memohome/ai-gateway",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.9",
|
||||
"@ai-sdk/google": "^3.0.6",
|
||||
"@ai-sdk/openai": "^3.0.7",
|
||||
"@memohome/shared": "workspace:*",
|
||||
"ai": "^6.0.25"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.27.0"
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { createGateway as createAiGateway } from 'ai'
|
||||
import { createOpenAI } from '@ai-sdk/openai'
|
||||
import { createAnthropic } from '@ai-sdk/anthropic'
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google'
|
||||
import { BaseModel, ModelClientType } from '@memohome/shared'
|
||||
import { ChatModel, ModelClientType } from '@memohome/shared'
|
||||
|
||||
export const createGateway = (model: BaseModel) => {
|
||||
export const createChatGateway = (model: ChatModel) => {
|
||||
const clients = {
|
||||
[ModelClientType.OPENAI]: createOpenAI,
|
||||
[ModelClientType.ANTHROPIC]: createAnthropic,
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createOpenAI } from '@ai-sdk/openai'
|
||||
import { EmbeddingModel } from '@memohome/shared'
|
||||
|
||||
export const createEmbeddingGateway = (model: EmbeddingModel) => {
|
||||
return createOpenAI({
|
||||
apiKey: model.apiKey,
|
||||
baseURL: model.baseUrl,
|
||||
}).embedding(model.modelId)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './chat'
|
||||
export * from './embedding'
|
||||
@@ -0,0 +1,11 @@
|
||||
import { pgTable, timestamp, uuid, jsonb, text } from 'drizzle-orm/pg-core'
|
||||
|
||||
export const history = pgTable(
|
||||
'history',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
messages: jsonb('messages').notNull(),
|
||||
timestamp: timestamp('timestamp').notNull(),
|
||||
user: text('user').notNull(),
|
||||
}
|
||||
)
|
||||
+12
-15
@@ -1,16 +1,13 @@
|
||||
import { pgTable, timestamp, uuid, jsonb, text, vector, index } from 'drizzle-orm/pg-core'
|
||||
import { pgTable, text, timestamp, integer } from 'drizzle-orm/pg-core'
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
export const memory = pgTable(
|
||||
'memory',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
messages: jsonb('messages').notNull(),
|
||||
timestamp: timestamp('timestamp').notNull(),
|
||||
user: text('user').notNull(),
|
||||
rawContent: text('raw_content').notNull(),
|
||||
embedding: vector('embedding', { dimensions: 1536 }).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('embedding_index').using('hnsw', table.embedding.op('vector_cosine_ops')),
|
||||
]
|
||||
)
|
||||
export const memory = pgTable('memory', {
|
||||
id: text('id').primaryKey(),
|
||||
memoryId: text('memory_id').notNull(),
|
||||
previousValue: text('previous_value'),
|
||||
newValue: text('new_value'),
|
||||
action: text('action').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).default(sql`timezone('utc', now())`),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }),
|
||||
isDeleted: integer('is_deleted').default(0),
|
||||
})
|
||||
@@ -1 +1,2 @@
|
||||
export * from './history'
|
||||
export * from './memory'
|
||||
@@ -15,9 +15,12 @@
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^3.0.7",
|
||||
"@memohome/ai-gateway": "workspace:*",
|
||||
"@memohome/db": "workspace:*",
|
||||
"@memohome/shared": "workspace:*",
|
||||
"ai": "^6.0.25",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"mem0ai": "^2.2.0",
|
||||
"xsai": "^0.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { embed } from 'xsai'
|
||||
import { EmbedParams } from './types'
|
||||
import { MemoryUnit } from './memory-unit'
|
||||
import { rawMemory } from './raw'
|
||||
import { db } from '@memohome/db'
|
||||
import { memory } from '@memohome/db/schema'
|
||||
|
||||
export interface AddMemoryParams extends EmbedParams {
|
||||
locale?: Intl.LocalesArgument
|
||||
}
|
||||
|
||||
export interface AddMemoryInput {
|
||||
memory: MemoryUnit
|
||||
}
|
||||
|
||||
export const createAddMemory = (params: AddMemoryParams) =>
|
||||
async ({ memory: memoryUnit }: AddMemoryInput) => {
|
||||
const rawContent = rawMemory(memoryUnit, params.locale)
|
||||
const { embedding } = await embed({
|
||||
model: params.model,
|
||||
input: rawContent,
|
||||
apiKey: params.apiKey,
|
||||
baseURL: params.baseURL,
|
||||
})
|
||||
await db.insert(memory)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: memoryUnit.timestamp,
|
||||
user: memoryUnit.user,
|
||||
rawContent,
|
||||
embedding,
|
||||
messages: memoryUnit.messages,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from '@memohome/db'
|
||||
import { memory } from '@memohome/db/schema'
|
||||
import { and, gte, lte, asc, sql, cosineDistance, gt, desc, eq } from 'drizzle-orm'
|
||||
import { history } from '@memohome/db/schema'
|
||||
import { and, gte, lte, asc, eq } from 'drizzle-orm'
|
||||
import { MemoryUnit } from './memory-unit'
|
||||
|
||||
export const filterByTimestamp = async (
|
||||
@@ -10,49 +10,17 @@ export const filterByTimestamp = async (
|
||||
) => {
|
||||
const results = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.from(history)
|
||||
.where(and(
|
||||
gte(memory.timestamp, from),
|
||||
lte(memory.timestamp, to),
|
||||
eq(memory.user, user),
|
||||
gte(history.timestamp, from),
|
||||
lte(history.timestamp, to),
|
||||
eq(history.user, user),
|
||||
))
|
||||
.orderBy(asc(memory.timestamp))
|
||||
.orderBy(asc(history.timestamp))
|
||||
|
||||
return results.map((result) => ({
|
||||
messages: result.messages,
|
||||
timestamp: new Date(result.timestamp),
|
||||
user: result.user,
|
||||
raw: result.rawContent,
|
||||
})) as MemoryUnit[]
|
||||
}
|
||||
|
||||
export const filterByEmbedding = async (
|
||||
embedding: number[],
|
||||
user: string,
|
||||
limit: number = 10,
|
||||
) => {
|
||||
const similarity = sql<number>`1 - (${cosineDistance(memory.embedding, embedding)})`
|
||||
const results = await db
|
||||
.select({
|
||||
similarity,
|
||||
messages: memory.messages,
|
||||
timestamp: memory.timestamp,
|
||||
user: memory.user,
|
||||
rawContent: memory.rawContent,
|
||||
embedding: memory.embedding,
|
||||
id: memory.id,
|
||||
})
|
||||
.from(memory)
|
||||
.where(and(
|
||||
gt(similarity, 0.5),
|
||||
eq(memory.user, user),
|
||||
))
|
||||
.orderBy((t) => desc(t.similarity))
|
||||
.limit(limit)
|
||||
return results.map((result) => ({
|
||||
messages: result.messages,
|
||||
timestamp: new Date(result.timestamp),
|
||||
user: result.user,
|
||||
raw: result.rawContent,
|
||||
})) as MemoryUnit[]
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
export * from './memory-unit'
|
||||
export * from './filter'
|
||||
export * from './add'
|
||||
export * from './search'
|
||||
export * from './types'
|
||||
export * from './raw'
|
||||
export * from './memory'
|
||||
@@ -4,5 +4,4 @@ export interface MemoryUnit {
|
||||
messages: ModelMessage[]
|
||||
timestamp: Date
|
||||
user: string
|
||||
raw: string
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Memory, type Message } from 'mem0ai/oss'
|
||||
import { ChatModel, EmbeddingModel } from '@memohome/shared'
|
||||
import { MemoryUnit } from './memory-unit'
|
||||
import { db } from '@memohome/db'
|
||||
import { history } from '@memohome/db/schema'
|
||||
|
||||
export interface CreateMemoryParams {
|
||||
summaryModel: ChatModel
|
||||
embeddingModel: EmbeddingModel
|
||||
}
|
||||
|
||||
export const createMemory = ({ summaryModel, embeddingModel }: CreateMemoryParams) => {
|
||||
process.env.OPENAI_BASE_URL = embeddingModel.baseUrl
|
||||
const memory = new Memory({
|
||||
version: 'v1.1',
|
||||
embedder: {
|
||||
provider: 'openai',
|
||||
config: {
|
||||
apiKey: embeddingModel.apiKey,
|
||||
model: embeddingModel.modelId,
|
||||
embeddingDims: embeddingModel.dimensions,
|
||||
url: embeddingModel.baseUrl,
|
||||
}
|
||||
},
|
||||
llm: {
|
||||
provider: summaryModel.clientType,
|
||||
config: {
|
||||
apiKey: summaryModel.apiKey,
|
||||
model: summaryModel.modelId,
|
||||
baseURL: summaryModel.baseUrl,
|
||||
}
|
||||
},
|
||||
vectorStore: {
|
||||
provider: 'qdrant',
|
||||
config: {
|
||||
collectionName: 'memory',
|
||||
embeddingModelDims: embeddingModel.dimensions,
|
||||
url: process.env.QDRANT_URL!,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const addMemory = async (memoryUnit: MemoryUnit) => {
|
||||
await memory.add(memoryUnit.messages as Message[], {
|
||||
userId: memoryUnit.user,
|
||||
})
|
||||
await db.insert(history)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: memoryUnit.timestamp,
|
||||
user: memoryUnit.user,
|
||||
messages: memoryUnit.messages,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
}
|
||||
|
||||
const searchMemory = async (query: string, userId: string) => {
|
||||
console.log('Searching memory with query:', query, 'userId:', userId)
|
||||
try {
|
||||
const { results } = await memory.search(query, {
|
||||
userId,
|
||||
})
|
||||
return results.map((result) => ({
|
||||
content: result.memory,
|
||||
metadata: result.metadata,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Memory search error:', error)
|
||||
// Log the full error details if available
|
||||
if (error && typeof error === 'object') {
|
||||
console.error('Error details:', JSON.stringify(error, null, 2))
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
addMemory,
|
||||
searchMemory,
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { ModelMessage } from 'ai'
|
||||
import { MemoryUnit } from './memory-unit'
|
||||
|
||||
export const rawMessages = (messages: ModelMessage[]) => {
|
||||
return messages.map((message) => {
|
||||
if (message.role === 'user') {
|
||||
if (Array.isArray(message.content)) {
|
||||
return `User: ${message.content.filter(c => c.type === 'text').map(c => c.text).join('\n')}`
|
||||
}
|
||||
return `User: ${message.content}`
|
||||
} else if (message.role === 'assistant') {
|
||||
if (Array.isArray(message.content)) {
|
||||
return `You: ${message.content.map(m => {
|
||||
if (m.type === 'text') {
|
||||
return m.text
|
||||
} else if (m.type === 'tool-call') {
|
||||
return `[Tool Call: ${m.toolName}]`
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}).join('\n')}`
|
||||
}
|
||||
return `You: ${message.content}`
|
||||
} else if (message.role === 'tool') {
|
||||
return `Tool Result: ${message.content}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter((message) => message !== null)
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
export const rawMemory = (memory: MemoryUnit, locale?: Intl.LocalesArgument) => {
|
||||
return `
|
||||
---
|
||||
date: ${memory.timestamp.toLocaleDateString(locale)}
|
||||
time: ${memory.timestamp.toLocaleTimeString(locale)}
|
||||
timezone: ${memory.timestamp.getTimezoneOffset()}
|
||||
---
|
||||
${rawMessages(memory.messages)}
|
||||
`.trim()
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { embed } from 'ai'
|
||||
import { filterByEmbedding } from './filter'
|
||||
import { EmbedParams } from './types'
|
||||
import { createOpenAI } from '@ai-sdk/openai'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface MemorySearchParams extends EmbedParams { }
|
||||
|
||||
export interface MemorySearchInput {
|
||||
user: string
|
||||
query: string
|
||||
maxResults?: number
|
||||
}
|
||||
|
||||
export const createMemorySearch = (params: MemorySearchParams) =>
|
||||
async ({ user, query, maxResults = 10 }: MemorySearchInput) => {
|
||||
const { embedding } = await embed({
|
||||
model: createOpenAI({
|
||||
apiKey: params.apiKey,
|
||||
baseURL: params.baseURL,
|
||||
}).embedding(params.model),
|
||||
value: query,
|
||||
})
|
||||
return await filterByEmbedding(embedding, user, maxResults)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface EmbedParams {
|
||||
baseURL: string
|
||||
apiKey: string
|
||||
model: string
|
||||
}
|
||||
@@ -4,6 +4,11 @@ export enum ModelClientType {
|
||||
GOOGLE = 'google',
|
||||
}
|
||||
|
||||
export enum ModelType {
|
||||
CHAT = 'chat',
|
||||
EMBEDDING = 'embedding',
|
||||
}
|
||||
|
||||
export interface BaseModel {
|
||||
/**
|
||||
* @description The unique identifier for the model
|
||||
@@ -34,4 +39,27 @@ export interface BaseModel {
|
||||
* @example 'GPT 4o'
|
||||
*/
|
||||
name?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @description The model type
|
||||
* @enum {ModelType}
|
||||
* @default {ModelType.CHAT}
|
||||
*/
|
||||
type?: ModelType
|
||||
}
|
||||
|
||||
export interface EmbeddingModel extends BaseModel {
|
||||
type?: ModelType.EMBEDDING
|
||||
|
||||
/**
|
||||
* @description The dimensions of the embedding
|
||||
* @example 1536
|
||||
*/
|
||||
dimensions: number
|
||||
}
|
||||
|
||||
export interface ChatModel extends BaseModel {
|
||||
type?: ModelType.CHAT
|
||||
}
|
||||
|
||||
export type Model = EmbeddingModel | ChatModel
|
||||
|
||||
Generated
+2655
-3
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -1,2 +1,5 @@
|
||||
packages:
|
||||
- 'packages/*'
|
||||
- 'packages/*'
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- sqlite3
|
||||
Reference in New Issue
Block a user