diff --git a/.env.example b/.env.example index 9edcee6..f07277f 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,9 @@ # Comma-separated list of users who can use the bot (delete var if you want everyone to be able to use it) AUTHORIZED_USERS="" -SERVICE="https://pds.indexx.dev" # PDS service URL (optional) + # PDS service URL (optional) +SERVICE="https://pds.indexx.dev" + DB_PATH="data/sqlite.db" GEMINI_MODEL="gemini-2.0-flash-lite" diff --git a/bun.lock b/bun.lock index d4b5196..ae7e23e 100644 --- a/bun.lock +++ b/bun.lock @@ -5,20 +5,20 @@ "name": "bsky-echo", "dependencies": { "@atproto/syntax": "^0.4.0", - "@google/genai": "^1.10.0", + "@google/genai": "^1.11.0", "@skyware/bot": "^0.3.12", "@types/js-yaml": "^4.0.9", "consola": "^3.4.2", - "drizzle-orm": "^0.44.3", + "drizzle-orm": "^0.44.4", "js-yaml": "^4.1.0", - "zod": "^4.0.5", + "zod": "^4.0.14", }, "devDependencies": { "@types/bun": "^1.2.19", "drizzle-kit": "^0.31.4", }, "peerDependencies": { - "typescript": "^5", + "typescript": "^5.8.3", }, }, }, @@ -107,7 +107,7 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], - "@google/genai": ["@google/genai@1.10.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-PR4tLuiIFMrpAiiCko2Z16ydikFsPF1c5TBfI64hlZcv3xBEApSCceLuDYu1pNMq2SkNh4r66J4AG+ZexBnMLw=="], + "@google/genai": ["@google/genai@1.11.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-4XFAHCvU91ewdWOU3RUdSeXpDuZRJHNYLqT9LKw7WqPjRQcEJvVU+VOU49ocruaSp8VuLKMecl0iadlQK+Zgfw=="], "@skyware/bot": ["@skyware/bot@0.3.12", "", { "dependencies": { "@atcute/bluesky": "^1.0.7", "@atcute/bluesky-richtext-builder": "^1.0.1", "@atcute/client": "^2.0.3", "@atcute/ozone": "^1.0.5", "quick-lru": "^7.0.0", "rate-limit-threshold": "^0.1.5" }, "optionalDependencies": { "@skyware/firehose": "^0.3.2", "@skyware/jetstream": "^0.2.2" } }, "sha512-5OqTtwItYsBFMh0nwrxfsqgXrvRaJzg1P+ghMV4rlRGwHhdRgBJcnYQYgUqqREFcB247yGo73LNyqq7kHEwV7Q=="], @@ -145,7 +145,7 @@ "drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="], - "drizzle-orm": ["drizzle-orm@0.44.3", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-8nIiYQxOpgUicEL04YFojJmvC4DNO4KoyXsEIqN44+g6gNBr6hmVpWk3uyAt4CaTiRGDwoU+alfqNNeonLAFOQ=="], + "drizzle-orm": ["drizzle-orm@0.44.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-ZyzKFpTC/Ut3fIqc2c0dPZ6nhchQXriTsqTNs4ayRgl6sZcFlMs9QZKPSHXK4bdOf41GHGWf+FrpcDDYwW+W6Q=="], "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], @@ -217,7 +217,7 @@ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "zod": ["zod@4.0.5", "", {}, "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA=="], + "zod": ["zod@4.0.14", "", {}, "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], diff --git a/drizzle/0003_flowery_korvac.sql b/drizzle/0003_flowery_korvac.sql new file mode 100644 index 0000000..d9fe9ba --- /dev/null +++ b/drizzle/0003_flowery_korvac.sql @@ -0,0 +1,3 @@ +ALTER TABLE `interactions` ADD `post` text;--> statement-breakpoint +ALTER TABLE `interactions` ADD `response` text;--> statement-breakpoint +ALTER TABLE `interactions` ADD `muted` integer; \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..21cce7e --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,255 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "30d38111-8e11-4d7d-99e8-cbafd962ca62", + "prevId": "11e8b31f-8e38-4013-8d50-bec6177b015a", + "tables": { + "interactions": { + "name": "interactions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "post": { + "name": "post", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "muted": { + "name": "muted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "interactions_uri_unique": { + "name": "interactions_uri_unique", + "columns": [ + "uri" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "memory_block_entries": { + "name": "memory_block_entries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "block_id": { + "name": "block_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_block_entries_block_id_memory_blocks_id_fk": { + "name": "memory_block_entries_block_id_memory_blocks_id_fk", + "tableFrom": "memory_block_entries", + "tableTo": "memory_blocks", + "columnsFrom": [ + "block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "memory_blocks": { + "name": "memory_blocks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'memory'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'User memory'" + }, + "mutable": { + "name": "mutable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "muted_threads": { + "name": "muted_threads", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "muted_at": { + "name": "muted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "muted_threads_uri_unique": { + "name": "muted_threads_uri_unique", + "columns": [ + "uri" + ], + "isUnique": true + }, + "muted_threads_rkey_unique": { + "name": "muted_threads_rkey_unique", + "columns": [ + "rkey" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index e6a1257..bb4556e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1753682242260, "tag": "0002_green_millenium_guard", "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1754166687687, + "tag": "0003_flowery_korvac", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 5683d6d..fc47e96 100644 --- a/package.json +++ b/package.json @@ -14,17 +14,17 @@ "drizzle-kit": "^0.31.4" }, "peerDependencies": { - "typescript": "^5" + "typescript": "^5.8.3" }, "dependencies": { "@atproto/syntax": "^0.4.0", - "@google/genai": "^1.10.0", + "@google/genai": "^1.11.0", "@skyware/bot": "^0.3.12", "@types/js-yaml": "^4.0.9", "consola": "^3.4.2", - "drizzle-orm": "^0.44.3", + "drizzle-orm": "^0.44.4", "js-yaml": "^4.1.0", - "zod": "^4.0.5" + "zod": "^4.0.14" }, "repository": { "url": "https://github.com/indexxing/echo" diff --git a/sqlite.db b/sqlite.db new file mode 100644 index 0000000..de6eb41 Binary files /dev/null and b/sqlite.db differ diff --git a/src/db/index.ts b/src/db/index.ts index 0e06dfd..e044fb1 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,6 +1,7 @@ import { drizzle } from "drizzle-orm/bun-sqlite"; import { Database } from "bun:sqlite"; +import * as schema from "./schema"; import { env } from "../env"; const sqlite = new Database(env.DB_PATH); -export default drizzle(sqlite); +export default drizzle(sqlite, { schema }); diff --git a/src/db/schema.ts b/src/db/schema.ts index 4084e24..3909002 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -5,6 +5,9 @@ export const interactions = sqliteTable("interactions", { id: integer().primaryKey({ autoIncrement: true }), uri: text().unique(), did: text(), + post: text(), + response: text(), + muted: integer({ mode: "boolean" }), created_at: integer({ mode: "timestamp" }).default(sql`CURRENT_TIMESTAMP`), }); diff --git a/src/handlers/posts.ts b/src/handlers/posts.ts index 6777c9b..56cf3f2 100644 --- a/src/handlers/posts.ts +++ b/src/handlers/posts.ts @@ -1,19 +1,24 @@ -import { isAuthorizedUser, logInteraction } from "../utils/interactions"; +import { + isAuthorizedUser, + logInteraction, + getRecentInteractions, +} from "../utils/interactions"; import * as threadUtils from "../utils/thread"; import modelPrompt from "../model/prompt.txt"; import { GoogleGenAI } from "@google/genai"; -import { interactions } from "../db/schema"; import { type Post } from "@skyware/bot"; import * as c from "../constants"; import * as tools from "../tools"; import consola from "consola"; import { env } from "../env"; +import { MemoryHandler } from "../utils/memory"; +import * as yaml from "js-yaml"; const logger = consola.withTag("Post Handler"); type SupportedFunctionCall = typeof c.SUPPORTED_FUNCTION_CALLS[number]; -async function generateAIResponse(parsedThread: string) { +async function generateAIResponse(memory: string, parsedThread: string) { const genai = new GoogleGenAI({ apiKey: env.GEMINI_API_KEY, }); @@ -39,6 +44,9 @@ async function generateAIResponse(parsedThread: string) { .replace("{{ administrator }}", env.ADMIN_HANDLE) .replace("{{ handle }}", env.HANDLE), }, + { + text: memory, + }, ], }, { @@ -126,10 +134,13 @@ export async function handler(post: Post): Promise { return; } - await logInteraction(post); - - if (await threadUtils.isThreadMuted(post)) { + const isMuted = await threadUtils.isThreadMuted(post); + if (isMuted) { logger.warn("Thread is muted."); + await logInteraction(post, { + responseText: null, + wasMuted: true, + }); return; } @@ -137,13 +148,42 @@ export async function handler(post: Post): Promise { const parsedThread = threadUtils.parseThread(thread); logger.success("Generated thread context:", parsedThread); - const inference = await generateAIResponse(parsedThread); + const botMemory = new MemoryHandler( + env.DID, + await MemoryHandler.getBlocks(env.DID), + ); + const userMemory = new MemoryHandler( + post.author.did, + await MemoryHandler.getBlocks(post.author.did), + ); + + const recentInteractions = await getRecentInteractions( + post.author.did, + thread, + ); + + const memory = yaml.dump({ + users_with_memory_blocks: { + [env.HANDLE]: botMemory.parseBlocks(), + [post.author.handle]: userMemory.parseBlocks(), + }, + recent_interactions: recentInteractions, + }); + + logger.log("Parsed memory blocks: ", memory); + + const inference = await generateAIResponse(memory, parsedThread); logger.success("Generated text:", inference.text); const responseText = inference.text; if (responseText) { await sendResponse(post, responseText); } + + await logInteraction(post, { + responseText: responseText ?? null, + wasMuted: false, + }); } catch (error) { logger.error("Error in post handler:", error); diff --git a/src/utils/interactions.ts b/src/utils/interactions.ts index 16d61bb..31a78b3 100644 --- a/src/utils/interactions.ts +++ b/src/utils/interactions.ts @@ -1,5 +1,6 @@ import { interactions } from "../db/schema"; import type { Post } from "@skyware/bot"; +import { desc, notInArray } from "drizzle-orm"; import { env } from "../env"; import db from "../db"; @@ -9,11 +10,41 @@ export function isAuthorizedUser(did: string) { : env.AUTHORIZED_USERS.includes(did as any); } -export async function logInteraction(post: Post): Promise { - await db.insert(interactions).values([{ - uri: post.uri, - did: post.author.did, - }]); +export async function logInteraction( + post: Post, + options: { + responseText: string | null; + wasMuted: boolean; + }, +): Promise { + await db.insert(interactions).values([ + { + uri: post.uri, + did: post.author.did, + post: post.text, + response: options.responseText, + muted: options.wasMuted, + }, + ]); - console.log(`Logged interaction, initiated by @${post.author.handle}`); + console.log(`Logged interaction, initiated by @${post.author.handle}`); +} + +export async function getRecentInteractions(did: string, thread: Post[]) { + const threadUris = thread.map((p) => p.uri); + + const recentInteractions = await db.query.interactions.findMany({ + where: (interactions, { eq, and, notInArray }) => + and( + eq(interactions.did, did), + notInArray(interactions.uri, threadUris), + ), + orderBy: (interactions, { desc }) => [desc(interactions.created_at)], + limit: 5, + }); + + return recentInteractions.map((i) => ({ + post: i.post, + response: i.response, + })); } diff --git a/src/utils/memory.ts b/src/utils/memory.ts new file mode 100644 index 0000000..25b9214 --- /dev/null +++ b/src/utils/memory.ts @@ -0,0 +1,130 @@ +import { and, desc, eq } from "drizzle-orm"; +import db from "../db"; +import { memory_block_entries, memory_blocks } from "../db/schema"; +import * as yaml from "js-yaml"; + +type MemoryBlock = { + id: number; + name: string; + description: string; + mutable: boolean; + entries: Entry[]; +}; + +type Entry = { + id: number; + block_id: number; + label: string; + value: string; + added_by: string | null; + created_at: Date | null; +}; + +export class MemoryHandler { + did: string; + blocks: MemoryBlockHandler[]; + + constructor(did: string, blocks: MemoryBlockHandler[]) { + this.did = did; + this.blocks = blocks; + } + + static async getBlocks(did: string) { + const blocks = await db + .select({ + id: memory_blocks.id, + name: memory_blocks.name, + description: memory_blocks.description, + mutable: memory_blocks.mutable, + }) + .from(memory_blocks) + .where(eq(memory_blocks.did, did)); + + const hydratedBlocks = []; + + for (const block of blocks) { + const entries = await db + .select() + .from(memory_block_entries) + .where(eq(memory_block_entries.block_id, block.id)) + .orderBy(desc(memory_block_entries.id)) + .limit(15); + + hydratedBlocks.push({ + ...block, + entries, + }); + } + + if (hydratedBlocks.length == 0) { + const [newBlock] = await db + .insert(memory_blocks) + .values([ + { + did, + name: "memory", + description: "User memory", + mutable: false, + }, + ]) + .returning(); + + hydratedBlocks.push({ + ...newBlock, + entries: [], + }); + } + + return hydratedBlocks.map( + (block) => + new MemoryBlockHandler( + block as MemoryBlock, + ), + ); + } + + public parseBlocks() { + return this.blocks.map((handler) => ({ + name: handler.block.name, + description: handler.block.description, + entries: handler.block.entries.map((entry) => ({ + label: entry.label, + value: entry.value, + added_by: entry.added_by || "nobody", + })), + })); + } +} + +export class MemoryBlockHandler { + block: MemoryBlock; + + constructor(block: MemoryBlock) { + this.block = block; + } + + public async createEntry(label: string, value: string) { + const [entry] = await db + .insert(memory_block_entries) + .values([ + { + block_id: this.block.id, + label, + value, + }, + ]) + .returning(); + + if (!entry) { + return { + added_to_memory: false, + }; + } + + this.block.entries.push(entry); + + return { + added_to_memory: true, + }; + } +}