From 4c07070806ee348160c4e26feb45a77fa26be465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o?= Date: Wed, 30 Oct 2024 18:24:58 +0000 Subject: [PATCH] add postgres support and schema --- package-lock.json | 125 +++++ package.json | 2 + src/adapters/postgres.ts | 845 ++++++++++++++++++++++++++++++++++ src/adapters/sqlite.ts | 24 +- src/clients/twitter/search.ts | 63 +-- src/core/types.ts | 38 +- src/index.ts | 130 +++--- supabase/README.md | 5 + supabase/postgres-schema.sql | 101 ++++ 9 files changed, 1220 insertions(+), 113 deletions(-) create mode 100644 src/adapters/postgres.ts create mode 100644 supabase/README.md create mode 100644 supabase/postgres-schema.sql diff --git a/package-lock.json b/package-lock.json index 6c2705a9..ca954d3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "onnxruntime-node": "1.19.2", "openai": "^4.56.0", "pdfjs-dist": "4.2.67", + "pg": "^8.13.1", "playwright": "1.47.0", "pm2": "5.4.2", "prism-media": "1.3.5", @@ -14350,6 +14351,87 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "license": "MIT" }, + "node_modules/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -14708,6 +14790,41 @@ "node": ">=0.10.0" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -16893,6 +17010,14 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", diff --git a/package.json b/package.json index bccbbc0f..b5289bb8 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "build": "tsc", "start": "node --loader ts-node/esm src/index.ts", + "start:arok": "node --loader ts-node/esm src/index.ts --characters=\"characters/arok.character.json\"", "start:service:ruby": "pm2 start npm --name=\"ruby\" --restart-delay=3000 --max-restarts=10 -- run start:ruby", "stop:service:ruby": "pm2 stop ruby", "start:ruby": "node --loader ts-node/esm src/index.ts --characters=\"characters/ruby.character.json\"", @@ -127,6 +128,7 @@ "onnxruntime-node": "1.19.2", "openai": "^4.56.0", "pdfjs-dist": "4.2.67", + "pg": "^8.13.1", "playwright": "1.47.0", "pm2": "5.4.2", "prism-media": "1.3.5", diff --git a/src/adapters/postgres.ts b/src/adapters/postgres.ts new file mode 100644 index 00000000..28c99979 --- /dev/null +++ b/src/adapters/postgres.ts @@ -0,0 +1,845 @@ +import { v4 } from "uuid"; +import pg from "pg"; +import { + Account, + Actor, + GoalStatus, + type Goal, + type Memory, + type Relationship, + type UUID, + Participant +} from "../core/types.ts"; +import { DatabaseAdapter } from "../core/database.ts"; +const { Pool } = pg; + +export class PostgresDatabaseAdapter extends DatabaseAdapter { + private pool: typeof Pool; + + constructor(connectionConfig: any) { + super(); + + this.pool = new Pool({ + ...connectionConfig, + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000 + }); + + // Register error handler for pool + this.pool.on("error", (err) => { + console.error("Unexpected error on idle client", err); + }); + + this.testConnection(); + } + + async testConnection(): Promise { + let client; + try { + // Attempt to get a client from the pool + client = await this.pool.connect(); + + // Test the connection with a simple query + const result = await client.query("SELECT NOW()"); + console.log("Database connection test successful:", result.rows[0]); + + return true; + } catch (error) { + console.error("Database connection test failed:", error); + throw new Error(`Failed to connect to database: ${error.message}`); + } finally { + // Make sure to release the client back to the pool + if (client) { + client.release(); + } + } + } + + async getRoom(roomId: UUID): Promise { + const client = await this.pool.connect(); + try { + const { rows } = await client.query( + "SELECT id FROM rooms WHERE id = $1", + [roomId] + ); + return rows.length > 0 ? (rows[0].id as UUID) : null; + } finally { + client.release(); + } + } + + async getParticipantsForAccount(userId: UUID): Promise { + const client = await this.pool.connect(); + try { + const { rows } = await client.query( + `SELECT id, "userId", "roomId", last_message_read + FROM participants + WHERE "userId" = $1`, + [userId] + ); + return rows as Participant[]; + } finally { + client.release(); + } + } + + async getParticipantUserState( + roomId: UUID, + userId: UUID + ): Promise<"FOLLOWED" | "MUTED" | null> { + const client = await this.pool.connect(); + try { + const { rows } = await client.query( + `SELECT userState FROM participants WHERE "roomId" = $1 AND userId = $2`, + [roomId, userId] + ); + return rows.length > 0 ? rows[0].userState : null; + } finally { + client.release(); + } + } + + async getMemoriesByRoomIds(params: { + roomIds: UUID[]; + tableName: string; + }): Promise { + const client = await this.pool.connect(); + try { + const placeholders = params.roomIds.map((_, i) => `$${i + 2}`).join(", "); + const { rows } = await client.query( + `SELECT * FROM memories + WHERE type = $1 AND "roomId" IN (${placeholders})`, + [params.tableName, ...params.roomIds] + ); + return rows.map((row) => ({ + ...row, + content: JSON.parse(row.content) + })); + } finally { + client.release(); + } + } + + async setParticipantUserState( + roomId: UUID, + userId: UUID, + state: "FOLLOWED" | "MUTED" | null + ): Promise { + const client = await this.pool.connect(); + try { + await client.query( + `UPDATE participants SET "userState" = $1 WHERE "roomId" = $2 AND "userId" = $3`, + [state, roomId, userId] + ); + } finally { + client.release(); + } + } + + async getParticipantsForRoom(roomId: UUID): Promise { + const client = await this.pool.connect(); + try { + const { rows } = await client.query( + 'SELECT "userId" FROM participants WHERE "roomId" = $1', + [roomId] + ); + return rows.map((row) => row.userId); + } finally { + client.release(); + } + } + + async getAccountById(userId: UUID): Promise { + const client = await this.pool.connect(); + try { + const { rows } = await client.query( + "SELECT * FROM accounts WHERE id = $1", + [userId] + ); + if (rows.length === 0) return null; + + const account = rows[0]; + + console.log("account", account); + return { + ...account, + details: + typeof account.details === "string" + ? JSON.parse(account.details) + : account.details + }; + } finally { + client.release(); + } + } + + async createAccount(account: Account): Promise { + const client = await this.pool.connect(); + try { + await client.query( + `INSERT INTO accounts (id, name, username, email, "avatarUrl", details) + VALUES ($1, $2, $3, $4, $5, $6)`, + [ + account.id ?? v4(), + account.name, + account.username || "", + account.email || "", + account.avatarUrl || "", + JSON.stringify(account.details) + ] + ); + return true; + } catch (error) { + console.log("Error creating account", error); + return false; + } finally { + client.release(); + } + } + + async getActorById(params: { roomId: UUID }): Promise { + const client = await this.pool.connect(); + try { + const { rows } = await client.query( + `SELECT a.id, a.name, a.username, a.details + FROM participants p + LEFT JOIN accounts a ON p."userId" = a.id + WHERE p."roomId" = $1`, + [params.roomId] + ); + return rows.map((row) => ({ + ...row, + details: + typeof row.details === "string" + ? JSON.parse(row.details) + : row.details + })); + } finally { + client.release(); + } + } + + async getMemoryById(id: UUID): Promise { + const client = await this.pool.connect(); + try { + const { rows } = await client.query( + "SELECT * FROM memories WHERE id = $1", + [id] + ); + if (rows.length === 0) return null; + + return { + ...rows[0], + content: + typeof rows[0].content === "string" + ? JSON.parse(rows[0].content) + : rows[0].content + }; + } finally { + client.release(); + } + } + + async createMemory(memory: Memory, tableName: string): Promise { + const client = await this.pool.connect(); + try { + let isUnique = true; + if (memory.embedding) { + const similarMemories = await this.searchMemoriesByEmbedding( + memory.embedding, + { + tableName, + roomId: memory.roomId, + match_threshold: 0.95, + count: 1 + } + ); + isUnique = similarMemories.length === 0; + } + + await client.query( + `INSERT INTO memories ( + id, type, content, embedding, "userId", "roomId", "unique", "createdAt" + ) VALUES ($1, $2, $3, $4::vector, $5::uuid, $6::uuid, $7, to_timestamp($8/1000.0))`, + [ + memory.id ?? v4(), + tableName, + JSON.stringify(memory.content), + `[${memory.embedding.join(",")}]`, + memory.userId, + memory.roomId, + memory.unique ?? true, + Date.now() + ] + ); + } finally { + client.release(); + } + } + + async searchMemories(params: { + tableName: string; + roomId: UUID; + embedding: number[]; + match_threshold: number; + match_count: number; + unique: boolean; + }): Promise { + const client = await this.pool.connect(); + try { + let sql = ` + SELECT *, + 1 - (embedding <-> $3) as similarity + FROM memories + WHERE type = $1 AND "roomId" = $2 + `; + + if (params.unique) { + sql += " AND unique = true"; + } + + sql += ` AND 1 - (embedding <-> $3) >= $4 + ORDER BY embedding <-> $3 + LIMIT $5`; + + const { rows } = await client.query(sql, [ + params.tableName, + params.roomId, + params.embedding, + params.match_threshold, + params.match_count + ]); + + return rows.map((row) => ({ + ...row, + content: JSON.parse(row.content), + similarity: row.similarity + })); + } finally { + client.release(); + } + } + + async getMemories(params: { + roomId: UUID; + count?: number; + unique?: boolean; + tableName: string; + userIds?: UUID[]; + start?: number; + end?: number; + }): Promise { + if (!params.tableName) throw new Error("tableName is required"); + if (!params.roomId) throw new Error("roomId is required"); + + const client = await this.pool.connect(); + try { + let sql = `SELECT * FROM memories WHERE type = $1 AND "roomId" = $2`; + const values: any[] = [params.tableName, params.roomId]; + let paramCount = 2; + + if (params.start) { + paramCount++; + sql += ` AND "createdAt" >= to_timestamp($${paramCount / 1000})`; + values.push(params.start); + } + + if (params.end) { + paramCount++; + sql += ` AND "createdAt" <= to_timestamp($${paramCount / 1000})`; + values.push(params.end); + } + + if (params.unique) { + sql += " AND unique = true"; + } + + if (params.userIds?.length) { + const userPlaceholders = params.userIds + .map((_, i) => `$${paramCount + 1 + i}`) + .join(","); + sql += ` AND "userId" IN (${userPlaceholders})`; + values.push(...params.userIds); + paramCount += params.userIds.length; + } + + sql += ' ORDER BY "createdAt" DESC'; + + if (params.count) { + paramCount++; + sql += ` LIMIT $${paramCount}`; + values.push(params.count); + } + + console.log("sql", sql, values); + + const { rows } = await client.query(sql, values); + return rows.map((row) => ({ + ...row, + content: + typeof rows.content === "string" + ? JSON.parse(rows.content) + : rows.content + })); + } finally { + client.release(); + } + } + + async getGoals(params: { + roomId: UUID; + userId?: UUID | null; + onlyInProgress?: boolean; + count?: number; + }): Promise { + const client = await this.pool.connect(); + try { + let sql = `SELECT * FROM goals WHERE "roomId" = $1`; + const values: any[] = [params.roomId]; + let paramCount = 1; + + if (params.userId) { + paramCount++; + sql += ` AND "userId" = $${paramCount}`; + values.push(params.userId); + } + + if (params.onlyInProgress) { + sql += " AND status = 'IN_PROGRESS'"; + } + + if (params.count) { + paramCount++; + sql += ` LIMIT $${paramCount}`; + values.push(params.count); + } + + const { rows } = await client.query(sql, values); + return rows.map((row) => ({ + ...row, + objectives: + typeof row.objectives === "string" + ? JSON.parse(row.objectives) + : row.objectives + })); + } finally { + client.release(); + } + } + + async updateGoal(goal: Goal): Promise { + const client = await this.pool.connect(); + try { + await client.query( + "UPDATE goals SET name = $1, status = $2, objectives = $3 WHERE id = $4", + [goal.name, goal.status, JSON.stringify(goal.objectives), goal.id] + ); + } finally { + client.release(); + } + } + + async createGoal(goal: Goal): Promise { + const client = await this.pool.connect(); + try { + await client.query( + `INSERT INTO goals (id, "roomId", "userId", name, status, objectives) + VALUES ($1, $2, $3, $4, $5, $6)`, + [ + goal.id ?? v4(), + goal.roomId, + goal.userId, + goal.name, + goal.status, + JSON.stringify(goal.objectives) + ] + ); + } finally { + client.release(); + } + } + + async removeGoal(goalId: UUID): Promise { + const client = await this.pool.connect(); + try { + await client.query("DELETE FROM goals WHERE id = $1", [goalId]); + } finally { + client.release(); + } + } + + async createRoom(roomId?: UUID): Promise { + const client = await this.pool.connect(); + try { + const newRoomId = roomId || v4(); + await client.query("INSERT INTO rooms (id) VALUES ($1)", [newRoomId]); + return newRoomId as UUID; + } finally { + client.release(); + } + } + + async removeRoom(roomId: UUID): Promise { + const client = await this.pool.connect(); + try { + await client.query("DELETE FROM rooms WHERE id = $1", [roomId]); + } finally { + client.release(); + } + } + + async createRelationship(params: { + userA: UUID; + userB: UUID; + }): Promise { + if (!params.userA || !params.userB) { + throw new Error("userA and userB are required"); + } + + const client = await this.pool.connect(); + try { + await client.query( + `INSERT INTO relationships (id, "userA", "userB", "userId") + VALUES ($1, $2, $3, $4)`, + [v4(), params.userA, params.userB, params.userA] + ); + return true; + } catch (error) { + console.log("Error creating relationship", error); + return false; + } finally { + client.release(); + } + } + + async getRelationship(params: { + userA: UUID; + userB: UUID; + }): Promise { + const client = await this.pool.connect(); + try { + const { rows } = await client.query( + `SELECT * FROM relationships + WHERE ("userA" = $1 AND "userB" = $2) OR ("userA" = $2 AND "userB" = $1)`, + [params.userA, params.userB] + ); + return rows.length > 0 ? rows[0] : null; + } finally { + client.release(); + } + } + + async getRelationships(params: { userId: UUID }): Promise { + const client = await this.pool.connect(); + try { + const { rows } = await client.query( + `SELECT * FROM relationships WHERE "userA" = $1 OR "userB" = $1`, + [params.userId] + ); + return rows; + } finally { + client.release(); + } + } + + async getCachedEmbeddings(opts: { + query_table_name: string; + query_threshold: number; + query_input: string; + query_field_name: string; + query_field_sub_name: string; + query_match_count: number; + }): Promise<{ embedding: number[]; levenshtein_score: number }[]> { + const client = await this.pool.connect(); + try { + const sql = ` + SELECT embedding, + levenshtein($1, content->$2->$3) as levenshtein_score + FROM memories + WHERE type = $4 + ORDER BY levenshtein_score + LIMIT $5 + `; + + const { rows } = await client.query(sql, [ + opts.query_input, + opts.query_field_name, + opts.query_field_sub_name, + opts.query_table_name, + opts.query_match_count + ]); + + return rows.map((row) => ({ + embedding: row.embedding, + levenshtein_score: row.levenshtein_score + })); + } finally { + client.release(); + } + } + + async log(params: { + body: { [key: string]: unknown }; + userId: UUID; + roomId: UUID; + type: string; + }): Promise { + const client = await this.pool.connect(); + try { + await client.query( + 'INSERT INTO logs (body, "userId", "roomId", type) VALUES ($1, $2, $3, $4)', + [params.body, params.userId, params.roomId, params.type] + ); + } finally { + client.release(); + } + } + + async searchMemoriesByEmbedding( + embedding: number[], + params: { + match_threshold?: number; + count?: number; + roomId?: UUID; + unique?: boolean; + tableName: string; + } + ): Promise { + const client = await this.pool.connect(); + try { + // Format the embedding array as a proper vector string + const vectorStr = `[${embedding.join(",")}]`; + + let sql = ` + SELECT *, + 1 - (embedding <-> $1::vector) as similarity + FROM memories + WHERE type = $2 + `; + + const values: any[] = [vectorStr, params.tableName]; + let paramCount = 2; + + if (params.unique) { + sql += ` AND "unique" = true`; + } + + if (params.roomId) { + paramCount++; + sql += ` AND "roomId" = $${paramCount}::uuid`; + values.push(params.roomId); + } + + if (params.match_threshold) { + paramCount++; + sql += ` AND 1 - (embedding <-> $1::vector) >= $${paramCount}`; + values.push(params.match_threshold); + } + + sql += ` ORDER BY embedding <-> $1::vector`; + + if (params.count) { + paramCount++; + sql += ` LIMIT $${paramCount}`; + values.push(params.count); + } + + const { rows } = await client.query(sql, values); + return rows.map((row) => ({ + ...row, + content: + typeof row.content === "string" + ? JSON.parse(row.content) + : row.content, + similarity: row.similarity + })); + } finally { + client.release(); + } + } + + async addParticipant(userId: UUID, roomId: UUID): Promise { + const client = await this.pool.connect(); + try { + await client.query( + 'INSERT INTO participants (id, "userId", "roomId") VALUES ($1, $2, $3)', + [v4(), userId, roomId] + ); + return true; + } catch (error) { + console.log("Error adding participant", error); + return false; + } finally { + client.release(); + } + } + + async removeParticipant(userId: UUID, roomId: UUID): Promise { + const client = await this.pool.connect(); + try { + await client.query( + 'DELETE FROM participants WHERE "userId" = $1 AND "roomId" = $2', + [userId, roomId] + ); + return true; + } catch (error) { + console.log("Error removing participant", error); + return false; + } finally { + client.release(); + } + } + + async updateGoalStatus(params: { + goalId: UUID; + status: GoalStatus; + }): Promise { + const client = await this.pool.connect(); + try { + await client.query("UPDATE goals SET status = $1 WHERE id = $2", [ + params.status, + params.goalId + ]); + } finally { + client.release(); + } + } + + async removeMemory(memoryId: UUID, tableName: string): Promise { + const client = await this.pool.connect(); + try { + await client.query("DELETE FROM memories WHERE type = $1 AND id = $2", [ + tableName, + memoryId + ]); + } finally { + client.release(); + } + } + + async removeAllMemories(roomId: UUID, tableName: string): Promise { + const client = await this.pool.connect(); + try { + await client.query( + "DELETE FROM memories WHERE type = $1 AND roomId = $2", + [tableName, roomId] + ); + } finally { + client.release(); + } + } + + async countMemories( + roomId: UUID, + unique = true, + tableName = "" + ): Promise { + if (!tableName) throw new Error("tableName is required"); + + const client = await this.pool.connect(); + try { + let sql = `SELECT COUNT(*) as count FROM memories WHERE type = $1 AND "roomId" = $2`; + if (unique) { + sql += " AND unique = true"; + } + + const { rows } = await client.query(sql, [tableName, roomId]); + return parseInt(rows[0].count); + } finally { + client.release(); + } + } + + async removeAllGoals(roomId: UUID): Promise { + const client = await this.pool.connect(); + try { + await client.query(`DELETE FROM goals WHERE "roomId" = $1`, [roomId]); + } finally { + client.release(); + } + } + + async getRoomsForParticipant(userId: UUID): Promise { + const client = await this.pool.connect(); + try { + const { rows } = await client.query( + `SELECT "roomId" FROM participants WHERE "userId" = $1`, + [userId] + ); + return rows.map((row) => row.roomId); + } finally { + client.release(); + } + } + + async getRoomsForParticipants(userIds: UUID[]): Promise { + const client = await this.pool.connect(); + try { + const placeholders = userIds.map((_, i) => `${i + 1}`).join(", "); + const { rows } = await client.query( + `SELECT DISTINCT "roomId" FROM participants WHERE "userId" IN (${placeholders})`, + userIds + ); + return rows.map((row) => row.roomId); + } finally { + client.release(); + } + } + async getActorDetails(params: { roomId: string }): Promise { + const sql = ` + SELECT + a.id, + a.name, + a.username, + COALESCE(a.details::jsonb, '{}'::jsonb) as details + FROM participants p + LEFT JOIN accounts a ON p.userId = a.id + WHERE p.roomId = $1 + `; + + try { + const result = await this.pool.query(sql, [params.roomId]); + + return result.rows.map((row) => ({ + ...row, + details: row.details // PostgreSQL automatically handles JSON parsing + })); + } catch (error) { + console.error("Error fetching actor details:", error); + throw new Error("Failed to fetch actor details"); + } + } +} + +export function createLoggingDatabaseAdapter( + adapter: DatabaseAdapter +): DatabaseAdapter { + return new Proxy(adapter, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + + if (typeof value === "function") { + return async function (...args: any[]) { + const methodName = prop.toString(); + console.log(`Calling method: ${methodName}`, { + arguments: args.map((arg) => + typeof arg === "object" ? JSON.stringify(arg) : arg + ) + }); + + try { + const result = await value.apply(this, args); + console.log(`Method ${methodName} completed successfully`); + return result; + } catch (error) { + console.error(`Method ${methodName} failed:`, error); + throw error; + } + }; + } + + return value; + } + }); +} diff --git a/src/adapters/sqlite.ts b/src/adapters/sqlite.ts index 628d2a62..ace51b5f 100644 --- a/src/adapters/sqlite.ts +++ b/src/adapters/sqlite.ts @@ -10,7 +10,7 @@ import { type Memory, type Relationship, type UUID, - Participant, + Participant } from "../core/types.ts"; import { sqliteTables } from "./sqlite/sqliteTables.ts"; @@ -135,7 +135,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { details: typeof row.details === "string" ? JSON.parse(row.details) - : row.details, + : row.details }; }) .filter((row): row is Actor => row !== null); @@ -159,7 +159,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { rows.forEach((row) => { memories.push({ ...row, - content: JSON.parse(row.content), + content: JSON.parse(row.content) }); }); @@ -175,7 +175,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { if (memory) { return { ...memory, - content: JSON.parse(memory.content as unknown as string), + content: JSON.parse(memory.content as unknown as string) }; } @@ -197,7 +197,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { tableName, roomId: memory.roomId, match_threshold: 0.95, // 5% similarity threshold - count: 1, + count: 1 } ); @@ -233,7 +233,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { new Float32Array(params.embedding), // Ensure embedding is Float32Array params.tableName, params.roomId, - params.match_count, + params.match_count ]; let sql = ` @@ -257,7 +257,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { typeof memory.createdAt === "string" ? Date.parse(memory.createdAt as string) : memory.createdAt, - content: JSON.parse(memory.content as unknown as string), + content: JSON.parse(memory.content as unknown as string) })); } @@ -274,7 +274,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { const queryParams = [ // JSON.stringify(embedding), new Float32Array(embedding), - params.tableName, + params.tableName ]; let sql = ` @@ -305,7 +305,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { typeof memory.createdAt === "string" ? Date.parse(memory.createdAt as string) : memory.createdAt, - content: JSON.parse(memory.content as unknown as string), + content: JSON.parse(memory.content as unknown as string) })); } @@ -341,7 +341,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { embedding: Array.from( new Float32Array(memory.embedding as unknown as Buffer) ), // Convert Buffer to number[] - levenshtein_score: 0, + levenshtein_score: 0 })); } @@ -424,7 +424,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { typeof memory.createdAt === "string" ? Date.parse(memory.createdAt as string) : memory.createdAt, - content: JSON.parse(memory.content as unknown as string), + content: JSON.parse(memory.content as unknown as string) })); } @@ -488,7 +488,7 @@ export class SqliteDatabaseAdapter extends DatabaseAdapter { objectives: typeof goal.objectives === "string" ? JSON.parse(goal.objectives) - : goal.objectives, + : goal.objectives })); } diff --git a/src/clients/twitter/search.ts b/src/clients/twitter/search.ts index 887f318c..be11ec16 100644 --- a/src/clients/twitter/search.ts +++ b/src/clients/twitter/search.ts @@ -7,7 +7,7 @@ import { Content, HandlerCallback, IAgentRuntime, - State, + State } from "../../core/types.ts"; import { stringToUuid } from "../../core/uuid.ts"; import { ClientBase } from "./base.ts"; @@ -15,7 +15,7 @@ import { buildConversationThread, isValidTweet, sendTweetChunks, - wait, + wait } from "./utils.ts"; const messageHandlerTemplate = @@ -53,7 +53,7 @@ export class TwitterSearchClient extends ClientBase { constructor(runtime: IAgentRuntime) { // Initialize the client and pass an optional callback to be called when the client is ready super({ - runtime, + runtime }); } @@ -65,7 +65,7 @@ export class TwitterSearchClient extends ClientBase { this.engageWithSearchTerms(); setTimeout( () => this.engageWithSearchTermsLoop(), - (Math.floor(Math.random() * (120 - 60 + 1)) + 60) * 60 * 1000, + (Math.floor(Math.random() * (120 - 60 + 1)) + 60) * 60 * 1000 ); } @@ -82,13 +82,17 @@ export class TwitterSearchClient extends ClientBase { console.log("Fetching search tweets"); // TODO: we wait 5 seconds here to avoid getting rate limited on startup, but we should queue await new Promise((resolve) => setTimeout(resolve, 5000)); - const recentTweets = await this.fetchSearchTweets(searchTerm, 20, SearchMode.Top); + const recentTweets = await this.fetchSearchTweets( + searchTerm, + 20, + SearchMode.Top + ); console.log("Search tweets fetched"); const homeTimeline = await this.fetchHomeTimeline(50); fs.writeFileSync( "tweetcache/home_timeline.json", - JSON.stringify(homeTimeline, null, 2), + JSON.stringify(homeTimeline, null, 2) ); const formattedHomeTimeline = @@ -117,7 +121,7 @@ export class TwitterSearchClient extends ClientBase { // ignore tweets where any of the thread tweets contain a tweet by the bot const thread = tweet.thread; const botTweet = thread.find( - (t) => t.username === this.runtime.getSetting("TWITTER_USERNAME"), + (t) => t.username === this.runtime.getSetting("TWITTER_USERNAME") ); return !botTweet; }) @@ -126,7 +130,7 @@ export class TwitterSearchClient extends ClientBase { ID: ${tweet.id}${tweet.inReplyToStatusId ? ` In reply to: ${tweet.inReplyToStatusId}` : ""} From: ${tweet.name} (@${tweet.username}) Text: ${tweet.text} - `, + ` ) .join("\n")} @@ -146,7 +150,7 @@ export class TwitterSearchClient extends ClientBase { model: "gpt-4o-mini", context: prompt, stop: [], - temperature: this.temperature, + temperature: this.temperature }); const responseLogName = `${this.runtime.character.name}_search_${datestr}_result`; @@ -156,7 +160,7 @@ export class TwitterSearchClient extends ClientBase { const selectedTweet = slicedTweets.find( (tweet) => tweet.id.toString().includes(tweetId) || - tweetId.includes(tweet.id.toString()), + tweetId.includes(tweet.id.toString()) ); if (!selectedTweet) { @@ -183,19 +187,19 @@ export class TwitterSearchClient extends ClientBase { this.runtime.agentId, this.runtime.getSetting("TWITTER_USERNAME"), this.runtime.character.name, - "twitter", + "twitter" ), this.runtime.ensureUserExists( userIdUUID, selectedTweet.username, selectedTweet.name, - "twitter", - ), + "twitter" + ) ]); await Promise.all([ this.runtime.ensureParticipantInRoom(userIdUUID, roomId), - this.runtime.ensureParticipantInRoom(this.runtime.agentId, roomId), + this.runtime.ensureParticipantInRoom(this.runtime.agentId, roomId) ]); // crawl additional conversation tweets, if there are any @@ -208,12 +212,12 @@ export class TwitterSearchClient extends ClientBase { url: selectedTweet.permanentUrl, inReplyTo: selectedTweet.inReplyToStatusId ? stringToUuid(selectedTweet.inReplyToStatusId) - : undefined, + : undefined }, userId: userIdUUID, roomId, // Timestamps are in seconds, but we need them in milliseconds - createdAt: selectedTweet.timestamp * 1000, + createdAt: selectedTweet.timestamp * 1000 }; if (!message.content.text) { @@ -225,7 +229,7 @@ export class TwitterSearchClient extends ClientBase { const replyContext = replies .filter( (reply) => - reply.username !== this.runtime.getSetting("TWITTER_USERNAME"), + reply.username !== this.runtime.getSetting("TWITTER_USERNAME") ) .map((reply) => `@${reply.username}: ${reply.text}`) .join("\n"); @@ -233,7 +237,7 @@ export class TwitterSearchClient extends ClientBase { let tweetBackground = ""; if (selectedTweet.isRetweet) { const originalTweet = await this.requestQueue.add(() => - this.twitterClient.getTweet(selectedTweet.id), + this.twitterClient.getTweet(selectedTweet.id) ); tweetBackground = `Retweeting @${originalTweet.username}: ${originalTweet.text}`; } @@ -257,20 +261,20 @@ export class TwitterSearchClient extends ClientBase { ${selectedTweet.text}${replyContext.length > 0 && `\nReplies to original post:\n${replyContext}`} ${`Original post text: ${selectedTweet.text}`} ${selectedTweet.urls.length > 0 ? `URLs: ${selectedTweet.urls.join(", ")}\n` : ""}${imageDescriptions.length > 0 ? `\nImages in Post (Described): ${imageDescriptions.join(", ")}\n` : ""} - `, + ` }); await this.saveRequestMessage(message, state as State); const context = composeContext({ state, - template: messageHandlerTemplate, + template: messageHandlerTemplate }); // log context to file log_to_file( `${this.runtime.getSetting("TWITTER_USERNAME")}_${datestr}_search_context`, - context, + context ); const responseContent = await this.runtime.messageCompletion({ @@ -279,16 +283,19 @@ export class TwitterSearchClient extends ClientBase { temperature: this.temperature, frequency_penalty: 1.2, presence_penalty: 1.3, - serverUrl: this.runtime.getSetting("X_SERVER_URL") ?? this.runtime.serverUrl, + serverUrl: + this.runtime.getSetting("X_SERVER_URL") ?? this.runtime.serverUrl, token: this.runtime.getSetting("XAI_API_KEY") ?? this.runtime.token, - model: this.runtime.getSetting("XAI_MODEL") ? this.runtime.getSetting("XAI_MODEL") : "gpt-4o-mini", + model: this.runtime.getSetting("XAI_MODEL") + ? this.runtime.getSetting("XAI_MODEL") + : "gpt-4o-mini" }); responseContent.inReplyTo = message.id; log_to_file( `${this.runtime.getSetting("TWITTER_USERNAME")}_${datestr}_search_response`, - JSON.stringify(responseContent), + JSON.stringify(responseContent) ); const response = responseContent; @@ -299,7 +306,7 @@ export class TwitterSearchClient extends ClientBase { } console.log( - `Bot would respond to tweet ${selectedTweet.id} with: ${response.text}`, + `Bot would respond to tweet ${selectedTweet.id} with: ${response.text}` ); try { if (!this.dryRun) { @@ -309,7 +316,7 @@ export class TwitterSearchClient extends ClientBase { response, message.roomId, this.runtime.getSetting("TWITTER_USERNAME"), - tweetId, + tweetId ); return memories; }; @@ -321,7 +328,7 @@ export class TwitterSearchClient extends ClientBase { for (const responseMessage of responseMessages) { await this.runtime.messageManager.createMemory( responseMessage, - false, + false ); } @@ -333,7 +340,7 @@ export class TwitterSearchClient extends ClientBase { message, responseMessages, state, - callback, + callback ); } else { console.log("Dry run, not sending post:", response.text); diff --git a/src/core/types.ts b/src/core/types.ts index 085cd488..34ec93ae 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -56,7 +56,7 @@ export interface Objective { export enum GoalStatus { DONE = "DONE", FAILED = "FAILED", - IN_PROGRESS = "IN_PROGRESS", + IN_PROGRESS = "IN_PROGRESS" } /** @@ -133,13 +133,13 @@ export type Handler = ( message: Memory, state?: State, options?: { [key: string]: unknown }, // additional options can be used for things like tests or state-passing on a chain - callback?: HandlerCallback, + callback?: HandlerCallback ) => Promise; // export type HandlerCallback = ( response: Content, - files?: any, + files?: any ) => Promise; /** @@ -148,7 +148,7 @@ export type HandlerCallback = ( export type Validator = ( runtime: IAgentRuntime, message: Memory, - state?: State, + state?: State ) => Promise; /** @@ -316,19 +316,19 @@ export interface IDatabaseAdapter { roomId?: UUID; unique?: boolean; tableName: string; - }, + } ): Promise; createMemory( memory: Memory, tableName: string, - unique?: boolean, + unique?: boolean ): Promise; removeMemory(memoryId: UUID, tableName: string): Promise; removeAllMemories(roomId: UUID, tableName: string): Promise; countMemories( roomId: UUID, unique?: boolean, - tableName?: string, + tableName?: string ): Promise; getGoals(params: { roomId: UUID; @@ -351,12 +351,12 @@ export interface IDatabaseAdapter { getParticipantsForRoom(roomId: UUID): Promise; getParticipantUserState( roomId: UUID, - userId: UUID, + userId: UUID ): Promise<"FOLLOWED" | "MUTED" | null>; setParticipantUserState( roomId: UUID, userId: UUID, - state: "FOLLOWED" | "MUTED" | null, + state: "FOLLOWED" | "MUTED" | null ): Promise; createRelationship(params: { userA: UUID; userB: UUID }): Promise; getRelationship(params: { @@ -382,7 +382,7 @@ export interface IMemoryManager { end?: number; }): Promise; getCachedEmbeddings( - content: string, + content: string ): Promise<{ embedding: number[]; levenshtein_score: number }[]>; getMemoryById(id: UUID): Promise; getMemoriesByRoomIds(params: { roomIds: UUID[] }): Promise; @@ -393,7 +393,7 @@ export interface IMemoryManager { count?: number; roomId: UUID; unique?: boolean; - }, + } ): Promise; createMemory(memory: Memory, unique?: boolean): Promise; removeMemory(memoryId: UUID): Promise; @@ -430,7 +430,7 @@ export interface IAgentRuntime { content: string, chunkSize: number, bleed: number, - model: string, + model: string ): Promise; getSetting(key: string): string | null; @@ -512,7 +512,7 @@ export interface IAgentRuntime { message: Memory, responses: Memory[], state?: State, - callback?: HandlerCallback, + callback?: HandlerCallback ): Promise; evaluate(message: Memory, state?: State): Promise; ensureParticipantExists(userId: UUID, roomId: UUID): Promise; @@ -520,14 +520,14 @@ export interface IAgentRuntime { userId: UUID, userName: string | null, name: string | null, - source: string | null, + source: string | null ): Promise; registerAction(action: Action): void; ensureParticipantInRoom(userId: UUID, roomId: UUID): Promise; ensureRoomExists(roomId: UUID): Promise; composeState( message: Memory, - additionalKeys?: { [key: string]: unknown }, + additionalKeys?: { [key: string]: unknown } ): Promise; updateRecentMessageState(state: State): Promise; } @@ -535,7 +535,7 @@ export interface IAgentRuntime { export interface IImageRecognitionService { initialize(modelId?: string | null, device?: string | null): Promise; describeImage( - imageUrl: string, + imageUrl: string ): Promise<{ title: string; description: string }>; } @@ -559,7 +559,7 @@ export interface ILlamaService { stop: string[], frequency_penalty: number, presence_penalty: number, - max_tokens: number, + max_tokens: number ): Promise; queueTextCompletion( context: string, @@ -567,7 +567,7 @@ export interface ILlamaService { stop: string[], frequency_penalty: number, presence_penalty: number, - max_tokens: number, + max_tokens: number ): Promise; getEmbeddingResponse(input: string): Promise; } @@ -576,7 +576,7 @@ export interface IBrowserService { initialize(): Promise; closeBrowser(): Promise; getPageContent( - url: string, + url: string ): Promise<{ title: string; description: string; bodyContent: string }>; } diff --git a/src/index.ts b/src/index.ts index 5d3a65ba..25cdac6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,10 @@ import mute_room from "./actions/mute_room.ts"; import unfollow_room from "./actions/unfollow_room.ts"; import unmute_room from "./actions/unmute_room.ts"; import { SqliteDatabaseAdapter } from "./adapters/sqlite.ts"; +import { + PostgresDatabaseAdapter, + createLoggingDatabaseAdapter +} from "./adapters/postgres.ts"; import { DiscordClient } from "./clients/discord/index.ts"; import DirectClient from "./clients/direct/index.ts"; import { TelegramClient } from "./clients/telegram/src/index.ts"; // Added Telegram import @@ -32,7 +36,7 @@ interface Arguments { let argv: Arguments = { character: "./src/agent/default_character.json", - characters: "", + characters: "" }; try { @@ -40,16 +44,16 @@ try { argv = yargs(process.argv.slice(2)) .option("character", { type: "string", - description: "Path to the character JSON file", + description: "Path to the character JSON file" }) .option("characters", { type: "string", - description: "Comma separated list of paths to character JSON files", + description: "Comma separated list of paths to character JSON files" }) .option("telegram", { type: "boolean", description: "Enable Telegram client", - default: false, + default: false }) .parseSync() as Arguments; } catch (error) { @@ -85,15 +89,27 @@ if (characterPaths?.length > 0) { async function startAgent(character: Character) { console.log("Starting agent for character " + character.name); - const token = character.settings?.secrets?.OPENAI_API_KEY || - (settings.OPENAI_API_KEY as string) + const token = + character.settings?.secrets?.OPENAI_API_KEY || + (settings.OPENAI_API_KEY as string); console.log("token", token); - const db = new SqliteDatabaseAdapter(new Database("./db.sqlite")) + + let db; + if (process.env.POSTGRES_URL) { + // const db = new SqliteDatabaseAdapter(new Database("./db.sqlite")); + db = new PostgresDatabaseAdapter({ + connectionString: process.env.POSTGRES_URL + }); + } else { + db = new SqliteDatabaseAdapter(new Database("./db.sqlite")); + // Debug adapter + // const loggingDb = createLoggingDatabaseAdapter(db); + } + const runtime = new AgentRuntime({ databaseAdapter: db, - token: - token, + token: token, serverUrl: "https://api.openai.com/v1", model: "gpt-4o", evaluators: [], @@ -105,8 +121,8 @@ async function startAgent(character: Character) { follow_room, unfollow_room, unmute_room, - mute_room, - ], + mute_room + ] }); const directRuntime = new AgentRuntime({ @@ -119,9 +135,7 @@ async function startAgent(character: Character) { evaluators: [], character, providers: [timeProvider, boredomProvider], - actions: [ - ...defaultActions, - ], + actions: [...defaultActions] }); function startDiscord(runtime: IAgentRuntime) { @@ -131,50 +145,55 @@ async function startAgent(character: Character) { async function startTelegram(runtime: IAgentRuntime, character: Character) { console.log("🔍 Attempting to start Telegram bot..."); - + const botToken = character.settings?.secrets?.TELEGRAM_BOT_TOKEN ?? settings.TELEGRAM_BOT_TOKEN; - + if (!botToken) { console.error( `❌ Telegram bot token is not set for character ${character.name}.` ); return null; } - + console.log("✅ Bot token found, initializing Telegram client..."); - + try { console.log("Creating new TelegramClient instance..."); const telegramClient = new TelegramClient(runtime, botToken); - + console.log("Calling start() on TelegramClient..."); await telegramClient.start(); - - console.log(`✅ Telegram client successfully started for character ${character.name}`); + + console.log( + `✅ Telegram client successfully started for character ${character.name}` + ); return telegramClient; } catch (error) { - console.error(`❌ Error creating/starting Telegram client for ${character.name}:`, error); + console.error( + `❌ Error creating/starting Telegram client for ${character.name}:`, + error + ); return null; } } async function startTwitter(runtime) { - console.log("Starting search client"); - const twitterSearchClient = new TwitterSearchClient(runtime); - await wait(); - console.log("Starting interaction client"); - const twitterInteractionClient = new TwitterInteractionClient(runtime); - await wait(); - console.log("Starting generation client"); - const twitterGenerationClient = new TwitterGenerationClient(runtime); - - return { - twitterInteractionClient, - twitterSearchClient, - twitterGenerationClient, - }; + console.log("Starting search client"); + const twitterSearchClient = new TwitterSearchClient(runtime); + await wait(); + console.log("Starting interaction client"); + const twitterInteractionClient = new TwitterInteractionClient(runtime); + await wait(); + console.log("Starting generation client"); + const twitterGenerationClient = new TwitterGenerationClient(runtime); + + return { + twitterInteractionClient, + twitterSearchClient, + twitterGenerationClient + }; } if (!character.clients) { @@ -190,7 +209,8 @@ async function startAgent(character: Character) { // Add Telegram client initialization if ( - (argv.telegram || character.clients.map((str) => str.toLowerCase()).includes("telegram")) + argv.telegram || + character.clients.map((str) => str.toLowerCase()).includes("telegram") ) { console.log("🔄 Telegram client enabled, starting initialization..."); const telegramClient = await startTelegram(runtime, character); @@ -203,14 +223,16 @@ async function startAgent(character: Character) { } if (character.clients.map((str) => str.toLowerCase()).includes("twitter")) { - const { - twitterInteractionClient, - twitterSearchClient, - twitterGenerationClient, - } = await startTwitter(runtime); - clients.push( - twitterInteractionClient, twitterSearchClient, twitterGenerationClient, - ); + const { + twitterInteractionClient, + twitterSearchClient, + twitterGenerationClient + } = await startTwitter(runtime); + clients.push( + twitterInteractionClient, + twitterSearchClient, + twitterGenerationClient + ); } directClient.registerAgent(directRuntime); @@ -230,7 +252,7 @@ const startAgents = async () => { startAgents(); -import readline from 'readline'; +import readline from "readline"; const rl = readline.createInterface({ input: process.stdin, @@ -238,23 +260,23 @@ const rl = readline.createInterface({ }); function chat() { - rl.question('You: ', async (input) => { - if (input.toLowerCase() === 'exit') { + rl.question("You: ", async (input) => { + if (input.toLowerCase() === "exit") { rl.close(); return; } const agentId = characters[0].name.toLowerCase(); // Assuming we're using the first character const response = await fetch(`http://localhost:3000/${agentId}/message`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json" }, body: JSON.stringify({ text: input, - userId: 'user', - userName: 'User', - }), + userId: "user", + userName: "User" + }) }); const data = await response.json(); @@ -264,4 +286,4 @@ function chat() { } console.log("Chat started. Type 'exit' to quit."); -chat(); \ No newline at end of file +chat(); diff --git a/supabase/README.md b/supabase/README.md new file mode 100644 index 00000000..82ed22ff --- /dev/null +++ b/supabase/README.md @@ -0,0 +1,5 @@ +# Postgres Schema + +Install CLI https://www.timescale.com/blog/how-to-install-psql-on-mac-ubuntu-debian-windows/ + +`psql -f ./postgres-schema.sql` diff --git a/supabase/postgres-schema.sql b/supabase/postgres-schema.sql new file mode 100644 index 00000000..aede30a2 --- /dev/null +++ b/supabase/postgres-schema.sql @@ -0,0 +1,101 @@ +-- Enable pgvector extension + +-- -- Drop existing tables and extensions +-- DROP EXTENSION IF EXISTS vector CASCADE; +-- DROP TABLE IF EXISTS relationships CASCADE; +-- DROP TABLE IF EXISTS participants CASCADE; +-- DROP TABLE IF EXISTS logs CASCADE; +-- DROP TABLE IF EXISTS goals CASCADE; +-- DROP TABLE IF EXISTS memories CASCADE; +-- DROP TABLE IF EXISTS rooms CASCADE; +-- DROP TABLE IF EXISTS accounts CASCADE; + + +CREATE EXTENSION IF NOT EXISTS vector; + +BEGIN; + +CREATE TABLE accounts ( + "id" UUID PRIMARY KEY, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "name" TEXT, + "username" TEXT, + "email" TEXT NOT NULL, + "avatarUrl" TEXT, + "details" JSONB DEFAULT '{}'::jsonb +); + +CREATE TABLE rooms ( + "id" UUID PRIMARY KEY, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE memories ( + "id" UUID PRIMARY KEY, + "type" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "content" JSONB NOT NULL, + "embedding" vector(1536), + "userId" UUID REFERENCES accounts("id"), + "roomId" UUID REFERENCES rooms("id"), + "unique" BOOLEAN DEFAULT true NOT NULL, + CONSTRAINT fk_room FOREIGN KEY ("roomId") REFERENCES rooms("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE +); + +CREATE TABLE goals ( + "id" UUID PRIMARY KEY, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "userId" UUID REFERENCES accounts("id"), + "name" TEXT, + "status" TEXT, + "description" TEXT, + "roomId" UUID REFERENCES rooms("id"), + "objectives" JSONB DEFAULT '[]'::jsonb NOT NULL, + CONSTRAINT fk_room FOREIGN KEY ("roomId") REFERENCES rooms("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE +); + +CREATE TABLE logs ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "userId" UUID NOT NULL REFERENCES accounts("id"), + "body" JSONB NOT NULL, + "type" TEXT NOT NULL, + "roomId" UUID NOT NULL REFERENCES rooms("id"), + CONSTRAINT fk_room FOREIGN KEY ("roomId") REFERENCES rooms("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE +); + +CREATE TABLE participants ( + "id" UUID PRIMARY KEY, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "userId" UUID REFERENCES accounts("id"), + "roomId" UUID REFERENCES rooms("id"), + "userState" TEXT, + "last_message_read" TEXT, + UNIQUE("userId", "roomId"), + CONSTRAINT fk_room FOREIGN KEY ("roomId") REFERENCES rooms("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE +); + +CREATE TABLE relationships ( + "id" UUID PRIMARY KEY, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "userA" UUID NOT NULL REFERENCES accounts("id"), + "userB" UUID NOT NULL REFERENCES accounts("id"), + "status" TEXT, + "userId" UUID NOT NULL REFERENCES accounts("id"), + CONSTRAINT fk_user_a FOREIGN KEY ("userA") REFERENCES accounts("id") ON DELETE CASCADE, + CONSTRAINT fk_user_b FOREIGN KEY ("userB") REFERENCES accounts("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE +); + +-- Indexes +CREATE INDEX idx_memories_embedding ON memories USING hnsw ("embedding" vector_cosine_ops); +CREATE INDEX idx_memories_type_room ON memories("type", "roomId"); +CREATE INDEX idx_participants_user ON participants("userId"); +CREATE INDEX idx_participants_room ON participants("roomId"); +CREATE INDEX idx_relationships_users ON relationships("userA", "userB"); + +COMMIT; \ No newline at end of file