Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
695 changes: 695 additions & 0 deletions api/_lib/memory.ts

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions api/_migrations/001_create_memory_embeddings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
-- Migration: Create memory_embeddings table for long-term memory storage
-- This table stores vector embeddings for semantic search of user memories

-- Enable pgvector extension (required for vector similarity search)
CREATE EXTENSION IF NOT EXISTS vector;

-- Create memory_embeddings table
CREATE TABLE IF NOT EXISTS memory_embeddings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(768), -- Adjust dimension based on your embedding model
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),

-- Indexes for performance
INDEX idx_memory_user_id (user_id),
INDEX idx_memory_created_at (created_at DESC)
);

-- Create index for vector similarity search (HNSW for better performance)
CREATE INDEX IF NOT EXISTS idx_memory_embedding_hnsw
ON memory_embeddings
USING hnsw (embedding vector_cosine_ops);

-- Alternative: IVFFlat index (faster build, slightly slower search)
-- CREATE INDEX IF NOT EXISTS idx_memory_embedding_ivfflat
-- ON memory_embeddings
-- USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

-- Grant permissions (adjust based on your database user)
-- GRANT SELECT, INSERT, UPDATE, DELETE ON memory_embeddings TO your_db_user;

COMMENT ON TABLE memory_embeddings IS 'Stores user memories as vector embeddings for semantic search';
COMMENT ON COLUMN memory_embeddings.embedding IS 'Vector embedding dimension should match your model (768 for bge-base-en-v1.5, 1536 for text-embedding-3-small, etc.)';
96 changes: 96 additions & 0 deletions api/_migrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Database Migrations

## Setup Instructions

### 1. Connect to your Postgres database

```bash
psql $POSTGRES_URL
```

Or use your database provider's console (Neon, Supabase, Vercel Postgres, etc.)

### 2. Run the migration

```sql
\i 001_create_memory_embeddings.sql
```

Or copy-paste the SQL content from the file.

### 3. Verify the table was created

```sql
\d memory_embeddings
```

You should see the table structure with the `embedding` column of type `vector`.

## Important Notes

### Embedding Dimension

The `embedding vector(768)` dimension must match your embedding model:

- **Cloudflare `@cf/baai/bge-base-en-v1.5`**: 768 dimensions
- **OpenAI `text-embedding-3-small`**: 1536 dimensions
- **OpenAI `text-embedding-3-large`**: 3072 dimensions
- **OpenAI `text-embedding-ada-002`**: 1536 dimensions

To change the dimension, alter the table:

```sql
-- Example: Change to 1536 for OpenAI embeddings
ALTER TABLE memory_embeddings
ALTER COLUMN embedding TYPE vector(1536);

-- Recreate the index
DROP INDEX IF EXISTS idx_memory_embedding_hnsw;
CREATE INDEX idx_memory_embedding_hnsw
ON memory_embeddings
USING hnsw (embedding vector_cosine_ops);
```

### Environment Variables

Ensure these are set in your Vercel project:

```env
# Database connection
POSTGRES_URL=postgresql://user:pass@host:5432/dbname

# Long-term memory settings
LONG_TERM_MEMORY_ENABLED=true
LONG_TERM_MEMORY_PROVIDER=postgres-pgvector

# Embedding provider (choose one)
MEMORY_EMBEDDING_PROVIDER=cloudflare
CLOUDFLARE_ACCOUNT_ID=your_account_id
CLOUDFLARE_API_TOKEN=your_api_token
MEMORY_EMBEDDING_MODEL=@cf/baai/bge-base-en-v1.5

# Or use OpenAI
# MEMORY_EMBEDDING_PROVIDER=openai
# MEMORY_EMBEDDING_API_KEY=sk-...
# MEMORY_EMBEDDING_MODEL=text-embedding-3-small
```

## Manual Table Creation (Alternative)

If you prefer to create the table manually without pgvector extension:

```sql
CREATE TABLE memory_embeddings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL,
content TEXT NOT NULL,
embedding TEXT, -- Store as JSON array string
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_memory_user_id ON memory_embeddings(user_id);
CREATE INDEX idx_memory_created_at ON memory_embeddings(created_at DESC);
```

Note: This won't support efficient vector similarity search.
37 changes: 37 additions & 0 deletions api/memory/clear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'

import { clearSession } from '../_lib/memory'

export default async function handler(req: VercelRequest, res: VercelResponse) {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')

if (req.method === 'OPTIONS') {
return res.status(200).end()
}

if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' })
}

try {
const body = req.body as { sessionId?: string }

if (!body?.sessionId || typeof body.sessionId !== 'string') {
return res.status(400).json({ success: false, error: 'sessionId is required' })
}

await clearSession(body.sessionId)

return res.status(200).json({ success: true })
}
catch (error) {
console.error('Error in /api/memory/clear:', error)
return res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
})
}
}
41 changes: 41 additions & 0 deletions api/memory/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'

import { getConfiguration, setConfiguration } from '../_lib/memory'

export default async function handler(req: VercelRequest, res: VercelResponse) {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')

if (req.method === 'OPTIONS') {
return res.status(200).end()
}

try {
if (req.method === 'GET') {
const config = getConfiguration()
return res.status(200).json({ success: true, data: config })
}

if (req.method === 'POST') {
const body = req.body

if (!body) {
return res.status(400).json({ success: false, error: 'Configuration payload is required' })
}

setConfiguration(body)
return res.status(200).json({ success: true })
}

return res.status(405).json({ success: false, error: 'Method not allowed' })
}
catch (error) {
console.error('Error in /api/memory/config:', error)
return res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
})
}
}
53 changes: 53 additions & 0 deletions api/memory/save.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'

import { saveMessage } from '../_lib/memory'

export default async function handler(req: VercelRequest, res: VercelResponse) {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')

if (req.method === 'OPTIONS') {
return res.status(200).end()
}

if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' })
}

try {
const body = req.body as { sessionId?: string, message?: unknown, userId?: string }

console.info('[Memory Debug API] 收到保存请求:', {
sessionId: body?.sessionId,
userId: body?.userId,
messageKeys: body?.message ? Object.keys(body.message) : 'none',
messageRole: (body?.message as any)?.role,
messageContent: typeof (body?.message as any)?.content === 'string' ? `${(body?.message as any).content.substring(0, 100)}...` : 'non-string',
hasPersistLongTerm: !!(body?.message as any)?.metadata?.persistLongTerm,
persistLongTermValue: (body?.message as any)?.metadata?.persistLongTerm,
})

if (!body?.sessionId || typeof body.sessionId !== 'string') {
return res.status(400).json({ success: false, error: 'sessionId is required' })
}

if (!body?.message || typeof body.message !== 'object') {
return res.status(400).json({ success: false, error: 'message payload is required' })
}

await saveMessage(body.sessionId, body.message as any, body.userId)

console.info('[Memory Debug API] 消息保存成功')

return res.status(200).json({ success: true })
}
catch (error) {
console.error('Error in /api/memory/save:', error)
return res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
})
}
}
54 changes: 54 additions & 0 deletions api/memory/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'

import { searchSimilar } from '../_lib/memory'

export default async function handler(req: VercelRequest, res: VercelResponse) {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')

if (req.method === 'OPTIONS') {
return res.status(200).end()
}

if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' })
}

try {
const body = req.body as { query?: string, userId?: string, limit?: number }

console.info('[Memory Debug Search] 收到搜索请求:', {
query: body?.query,
userId: body?.userId,
limit: body?.limit,
})

if (!body?.query || typeof body.query !== 'string') {
return res.status(400).json({ success: false, error: 'query is required' })
}

if (!body?.userId || typeof body.userId !== 'string') {
return res.status(400).json({ success: false, error: 'userId is required' })
}

const results = await searchSimilar(
body.query,
body.userId,
typeof body.limit === 'number' ? body.limit : undefined,
)

console.info('[Memory Debug Search] 搜索结果数量:', results.length)
console.info('[Memory Debug Search] 搜索成功,返回结果')

return res.status(200).json({ success: true, data: results })
}
catch (error) {
console.error('Error in /api/memory/search:', error)
return res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
})
}
}
40 changes: 40 additions & 0 deletions api/memory/session/[sessionId].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'

import { getRecentMessages } from '../../_lib/memory'

export default async function handler(req: VercelRequest, res: VercelResponse) {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')

if (req.method === 'OPTIONS') {
return res.status(200).end()
}

if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' })
}

try {
const { sessionId } = req.query

if (!sessionId || typeof sessionId !== 'string') {
return res.status(400).json({ success: false, error: 'sessionId is required' })
}

const limitParam = req.query.limit as string | undefined
const limit = limitParam ? Number.parseInt(limitParam, 10) : undefined

const messages = await getRecentMessages(sessionId, Number.isNaN(limit) ? undefined : limit)

return res.status(200).json({ success: true, data: messages })
}
catch (error) {
console.error('Error in /api/memory/session/[sessionId]:', error)
return res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
})
}
}
20 changes: 20 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@proj-airi/api",
"version": "0.7.2-beta.3",
"private": true,
"description": "Vercel serverless API functions for AIRI",
"dependencies": {
"@qdrant/js-client-rest": "^1.15.1",
"@upstash/redis": "^1.35.5",
"@vercel/kv": "^1.0.1",
"@vercel/node": "^5.3.26",
"@vercel/postgres": "^0.8.0",
"openai": "^4.95.1",
"pg": "^8.16.3"
},
"devDependencies": {
"@types/node": "^24.6.2",
"@types/pg": "^8.6.6",
"typescript": "~5.9.3"
}
}
Loading