mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: long-memory
This commit is contained in:
@@ -3,20 +3,15 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Agent package for the phonetutor monorepo",
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"start": "bun run src/client/index.ts"
|
||||
"test": "vitest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Phonetutor",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.7",
|
||||
"@ai-sdk/google": "^3.0.6",
|
||||
"@ai-sdk/openai": "^3.0.7",
|
||||
"@byteagent/shared": "workspace:*",
|
||||
"ai": "^6.0.14",
|
||||
"dotenv": "^17.2.3",
|
||||
"xsai": "^0.4.1",
|
||||
"zod": "^4.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { streamText } from 'xsai'
|
||||
|
||||
streamText({})
|
||||
@@ -0,0 +1 @@
|
||||
export * from './system'
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface SystemParams {
|
||||
date: Date
|
||||
locale: Intl.LocalesArgument
|
||||
}
|
||||
|
||||
export const system = ({ date, locale }: SystemParams) => {
|
||||
return `
|
||||
---
|
||||
date: ${date.toLocaleDateString(locale)}
|
||||
time: ${date.toLocaleTimeString(locale)}
|
||||
language: ${locale}
|
||||
timezone: ${date.getTimezoneOffset()}
|
||||
---
|
||||
You are a personal housekeeper assistant, which able to manage the master's daily affairs.
|
||||
|
||||
Your abilities:
|
||||
- Long memory: You possess long-term memory; conversations from the last 24 hours will be directly loaded into your context. Additionally, you can use tools to search for past memories.
|
||||
- Scheduled tasks: You can create scheduled tasks to automatically remind you to do something.
|
||||
- Messaging: You may allowed to use message software to send messages to the master.
|
||||
`.trim()
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { config } from 'dotenv'
|
||||
|
||||
export const db = drizzle(process.env.DATABASE_URL!)
|
||||
config({ path: '../../' })
|
||||
|
||||
export const db = drizzle(process.env.DATABASE_URL!)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { pgTable, timestamp, uuid, jsonb, text, vector, index } from 'drizzle-orm/pg-core'
|
||||
|
||||
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')),
|
||||
]
|
||||
)
|
||||
@@ -1,10 +0,0 @@
|
||||
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
|
||||
|
||||
export const model = pgTable('model', {
|
||||
id: uuid('id').primaryKey(),
|
||||
modelId: text('model_id').notNull(),
|
||||
name: text('name'),
|
||||
baseUrl: text('base_url').notNull(),
|
||||
apiKey: text('api_key').notNull(),
|
||||
clientType: text('client_type').notNull()
|
||||
})
|
||||
@@ -1 +1 @@
|
||||
export * from './model'
|
||||
export * from './memory'
|
||||
@@ -0,0 +1 @@
|
||||
# @byteagent/memory
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@byteagent/memory",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"dependencies": {
|
||||
"@byteagent/db": "workspace:*",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"xsai": "^0.4.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { embed } from 'xsai'
|
||||
import { EmbedParams } from './types'
|
||||
import { MemoryUnit } from './memory-unit'
|
||||
import { rawMemory } from './raw'
|
||||
import { db } from '@byteagent/db'
|
||||
import { memory } from '@byteagent/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({
|
||||
timestamp: memoryUnit.timestamp,
|
||||
user: memoryUnit.user,
|
||||
rawContent,
|
||||
embedding,
|
||||
messages: memoryUnit.messages,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { db } from '@byteagent/db'
|
||||
import { memory } from '@byteagent/db/schema'
|
||||
import { and, gte, lte, asc, sql, cosineDistance, gt, desc, eq } from 'drizzle-orm'
|
||||
import { MemoryUnit } from './memory-unit'
|
||||
|
||||
export const filterByTimestamp = async (
|
||||
from: Date,
|
||||
to: Date,
|
||||
user: string,
|
||||
) => {
|
||||
const results = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(
|
||||
gte(memory.timestamp, from),
|
||||
lte(memory.timestamp, to),
|
||||
eq(memory.user, user),
|
||||
))
|
||||
.orderBy(asc(memory.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[]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './memory-unit'
|
||||
export * from './filter'
|
||||
export * from './add'
|
||||
export * from './search'
|
||||
export * from './types'
|
||||
export * from './raw'
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Message } from 'xsai'
|
||||
|
||||
export interface MemoryUnit {
|
||||
messages: Message[]
|
||||
timestamp: Date
|
||||
user: string
|
||||
raw: string
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Message } from 'xsai'
|
||||
import { MemoryUnit } from './memory-unit'
|
||||
|
||||
export const rawMessages = (messages: Message[]) => {
|
||||
return messages.map((message) => {
|
||||
if (message.role === 'user') {
|
||||
return `User: ${message.content}`
|
||||
} else if (message.role === 'assistant') {
|
||||
let toolCalls = ''
|
||||
if (message.tool_calls && message.tool_calls.length !== 0) {
|
||||
toolCalls = `Tool Calls: ${message.tool_calls.map(t => t.function.name).join(', ')}`
|
||||
}
|
||||
return `You: ${message.content} \n${toolCalls}`
|
||||
} 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()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { embed } from 'xsai'
|
||||
import { filterByEmbedding } from './filter'
|
||||
import { EmbedParams } from './types'
|
||||
|
||||
// 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: params.model,
|
||||
input: query,
|
||||
apiKey: params.apiKey,
|
||||
baseURL: params.baseURL,
|
||||
})
|
||||
return await filterByEmbedding(embedding, user, maxResults)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface EmbedParams {
|
||||
baseURL: string
|
||||
apiKey: string
|
||||
model: string
|
||||
}
|
||||
Reference in New Issue
Block a user