From 16d51a1d2940fe479586f7442162c52cbca1b940 Mon Sep 17 00:00:00 2001 From: Index Date: Sat, 2 Aug 2025 15:16:43 -0500 Subject: [PATCH] feat: basic parsing --- .env.example | 4 +- bun.lock | 6 +- package.json | 8 +-- src/handlers/posts.ts | 28 ++++++++- src/utils/memory.ts | 130 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 src/utils/memory.ts 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..6580c1d 100644 --- a/bun.lock +++ b/bun.lock @@ -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/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/src/handlers/posts.ts b/src/handlers/posts.ts index e48d7b3..933cba2 100644 --- a/src/handlers/posts.ts +++ b/src/handlers/posts.ts @@ -2,18 +2,19 @@ import { isAuthorizedUser, logInteraction } 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, }); @@ -40,6 +41,9 @@ async function generateAIResponse(parsedThread: string) { env.ADMIN_HANDLE, ), }, + { + text: memory, + }, ], }, { @@ -138,7 +142,25 @@ 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 memory = yaml.dump({ + users_with_memory_blocks: { + [env.HANDLE]: botMemory.parseBlocks(), + [post.author.handle]: userMemory.parseBlocks(), + }, + }); + + logger.log("Parsed memory blocks: ", memory); + + const inference = await generateAIResponse(memory, parsedThread); logger.success("Generated text:", inference.text); const responseText = inference.text; 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, + }; + } +}