+3
-1
.env.example
+3
-1
.env.example
···
1
1
# Comma-separated list of users who can use the bot (delete var if you want everyone to be able to use it)
2
2
AUTHORIZED_USERS=""
3
3
4
-
SERVICE="https://pds.indexx.dev" # PDS service URL (optional)
4
+
# PDS service URL (optional)
5
+
SERVICE="https://pds.indexx.dev"
6
+
5
7
DB_PATH="data/sqlite.db"
6
8
GEMINI_MODEL="gemini-2.0-flash-lite"
7
9
+7
-7
bun.lock
+7
-7
bun.lock
···
5
5
"name": "bsky-echo",
6
6
"dependencies": {
7
7
"@atproto/syntax": "^0.4.0",
8
-
"@google/genai": "^1.10.0",
8
+
"@google/genai": "^1.11.0",
9
9
"@skyware/bot": "^0.3.12",
10
10
"@types/js-yaml": "^4.0.9",
11
11
"consola": "^3.4.2",
12
-
"drizzle-orm": "^0.44.3",
12
+
"drizzle-orm": "^0.44.4",
13
13
"js-yaml": "^4.1.0",
14
-
"zod": "^4.0.5",
14
+
"zod": "^4.0.14",
15
15
},
16
16
"devDependencies": {
17
17
"@types/bun": "^1.2.19",
18
18
"drizzle-kit": "^0.31.4",
19
19
},
20
20
"peerDependencies": {
21
-
"typescript": "^5",
21
+
"typescript": "^5.8.3",
22
22
},
23
23
},
24
24
},
···
107
107
108
108
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="],
109
109
110
-
"@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=="],
110
+
"@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=="],
111
111
112
112
"@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=="],
113
113
···
145
145
146
146
"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=="],
147
147
148
-
"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=="],
148
+
"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=="],
149
149
150
150
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
151
151
···
217
217
218
218
"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=="],
219
219
220
-
"zod": ["zod@4.0.5", "", {}, "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA=="],
220
+
"zod": ["zod@4.0.14", "", {}, "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw=="],
221
221
222
222
"@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=="],
223
223
+3
drizzle/0003_flowery_korvac.sql
+3
drizzle/0003_flowery_korvac.sql
+255
drizzle/meta/0003_snapshot.json
+255
drizzle/meta/0003_snapshot.json
···
1
+
{
2
+
"version": "6",
3
+
"dialect": "sqlite",
4
+
"id": "30d38111-8e11-4d7d-99e8-cbafd962ca62",
5
+
"prevId": "11e8b31f-8e38-4013-8d50-bec6177b015a",
6
+
"tables": {
7
+
"interactions": {
8
+
"name": "interactions",
9
+
"columns": {
10
+
"id": {
11
+
"name": "id",
12
+
"type": "integer",
13
+
"primaryKey": true,
14
+
"notNull": true,
15
+
"autoincrement": true
16
+
},
17
+
"uri": {
18
+
"name": "uri",
19
+
"type": "text",
20
+
"primaryKey": false,
21
+
"notNull": false,
22
+
"autoincrement": false
23
+
},
24
+
"did": {
25
+
"name": "did",
26
+
"type": "text",
27
+
"primaryKey": false,
28
+
"notNull": false,
29
+
"autoincrement": false
30
+
},
31
+
"post": {
32
+
"name": "post",
33
+
"type": "text",
34
+
"primaryKey": false,
35
+
"notNull": false,
36
+
"autoincrement": false
37
+
},
38
+
"response": {
39
+
"name": "response",
40
+
"type": "text",
41
+
"primaryKey": false,
42
+
"notNull": false,
43
+
"autoincrement": false
44
+
},
45
+
"muted": {
46
+
"name": "muted",
47
+
"type": "integer",
48
+
"primaryKey": false,
49
+
"notNull": false,
50
+
"autoincrement": false
51
+
},
52
+
"created_at": {
53
+
"name": "created_at",
54
+
"type": "integer",
55
+
"primaryKey": false,
56
+
"notNull": false,
57
+
"autoincrement": false,
58
+
"default": "CURRENT_TIMESTAMP"
59
+
}
60
+
},
61
+
"indexes": {
62
+
"interactions_uri_unique": {
63
+
"name": "interactions_uri_unique",
64
+
"columns": [
65
+
"uri"
66
+
],
67
+
"isUnique": true
68
+
}
69
+
},
70
+
"foreignKeys": {},
71
+
"compositePrimaryKeys": {},
72
+
"uniqueConstraints": {},
73
+
"checkConstraints": {}
74
+
},
75
+
"memory_block_entries": {
76
+
"name": "memory_block_entries",
77
+
"columns": {
78
+
"id": {
79
+
"name": "id",
80
+
"type": "integer",
81
+
"primaryKey": true,
82
+
"notNull": true,
83
+
"autoincrement": true
84
+
},
85
+
"block_id": {
86
+
"name": "block_id",
87
+
"type": "integer",
88
+
"primaryKey": false,
89
+
"notNull": true,
90
+
"autoincrement": false
91
+
},
92
+
"label": {
93
+
"name": "label",
94
+
"type": "text",
95
+
"primaryKey": false,
96
+
"notNull": true,
97
+
"autoincrement": false
98
+
},
99
+
"value": {
100
+
"name": "value",
101
+
"type": "text",
102
+
"primaryKey": false,
103
+
"notNull": true,
104
+
"autoincrement": false
105
+
},
106
+
"added_by": {
107
+
"name": "added_by",
108
+
"type": "text",
109
+
"primaryKey": false,
110
+
"notNull": false,
111
+
"autoincrement": false
112
+
},
113
+
"created_at": {
114
+
"name": "created_at",
115
+
"type": "integer",
116
+
"primaryKey": false,
117
+
"notNull": false,
118
+
"autoincrement": false,
119
+
"default": "CURRENT_TIMESTAMP"
120
+
}
121
+
},
122
+
"indexes": {},
123
+
"foreignKeys": {
124
+
"memory_block_entries_block_id_memory_blocks_id_fk": {
125
+
"name": "memory_block_entries_block_id_memory_blocks_id_fk",
126
+
"tableFrom": "memory_block_entries",
127
+
"tableTo": "memory_blocks",
128
+
"columnsFrom": [
129
+
"block_id"
130
+
],
131
+
"columnsTo": [
132
+
"id"
133
+
],
134
+
"onDelete": "no action",
135
+
"onUpdate": "no action"
136
+
}
137
+
},
138
+
"compositePrimaryKeys": {},
139
+
"uniqueConstraints": {},
140
+
"checkConstraints": {}
141
+
},
142
+
"memory_blocks": {
143
+
"name": "memory_blocks",
144
+
"columns": {
145
+
"id": {
146
+
"name": "id",
147
+
"type": "integer",
148
+
"primaryKey": true,
149
+
"notNull": true,
150
+
"autoincrement": true
151
+
},
152
+
"did": {
153
+
"name": "did",
154
+
"type": "text",
155
+
"primaryKey": false,
156
+
"notNull": true,
157
+
"autoincrement": false
158
+
},
159
+
"name": {
160
+
"name": "name",
161
+
"type": "text",
162
+
"primaryKey": false,
163
+
"notNull": true,
164
+
"autoincrement": false,
165
+
"default": "'memory'"
166
+
},
167
+
"description": {
168
+
"name": "description",
169
+
"type": "text",
170
+
"primaryKey": false,
171
+
"notNull": true,
172
+
"autoincrement": false,
173
+
"default": "'User memory'"
174
+
},
175
+
"mutable": {
176
+
"name": "mutable",
177
+
"type": "integer",
178
+
"primaryKey": false,
179
+
"notNull": true,
180
+
"autoincrement": false,
181
+
"default": false
182
+
}
183
+
},
184
+
"indexes": {},
185
+
"foreignKeys": {},
186
+
"compositePrimaryKeys": {},
187
+
"uniqueConstraints": {},
188
+
"checkConstraints": {}
189
+
},
190
+
"muted_threads": {
191
+
"name": "muted_threads",
192
+
"columns": {
193
+
"id": {
194
+
"name": "id",
195
+
"type": "integer",
196
+
"primaryKey": true,
197
+
"notNull": true,
198
+
"autoincrement": true
199
+
},
200
+
"uri": {
201
+
"name": "uri",
202
+
"type": "text",
203
+
"primaryKey": false,
204
+
"notNull": false,
205
+
"autoincrement": false
206
+
},
207
+
"rkey": {
208
+
"name": "rkey",
209
+
"type": "text",
210
+
"primaryKey": false,
211
+
"notNull": false,
212
+
"autoincrement": false
213
+
},
214
+
"muted_at": {
215
+
"name": "muted_at",
216
+
"type": "integer",
217
+
"primaryKey": false,
218
+
"notNull": false,
219
+
"autoincrement": false,
220
+
"default": "CURRENT_TIMESTAMP"
221
+
}
222
+
},
223
+
"indexes": {
224
+
"muted_threads_uri_unique": {
225
+
"name": "muted_threads_uri_unique",
226
+
"columns": [
227
+
"uri"
228
+
],
229
+
"isUnique": true
230
+
},
231
+
"muted_threads_rkey_unique": {
232
+
"name": "muted_threads_rkey_unique",
233
+
"columns": [
234
+
"rkey"
235
+
],
236
+
"isUnique": true
237
+
}
238
+
},
239
+
"foreignKeys": {},
240
+
"compositePrimaryKeys": {},
241
+
"uniqueConstraints": {},
242
+
"checkConstraints": {}
243
+
}
244
+
},
245
+
"views": {},
246
+
"enums": {},
247
+
"_meta": {
248
+
"schemas": {},
249
+
"tables": {},
250
+
"columns": {}
251
+
},
252
+
"internal": {
253
+
"indexes": {}
254
+
}
255
+
}
+7
drizzle/meta/_journal.json
+7
drizzle/meta/_journal.json
+4
-4
package.json
+4
-4
package.json
···
14
14
"drizzle-kit": "^0.31.4"
15
15
},
16
16
"peerDependencies": {
17
-
"typescript": "^5"
17
+
"typescript": "^5.8.3"
18
18
},
19
19
"dependencies": {
20
20
"@atproto/syntax": "^0.4.0",
21
-
"@google/genai": "^1.10.0",
21
+
"@google/genai": "^1.11.0",
22
22
"@skyware/bot": "^0.3.12",
23
23
"@types/js-yaml": "^4.0.9",
24
24
"consola": "^3.4.2",
25
-
"drizzle-orm": "^0.44.3",
25
+
"drizzle-orm": "^0.44.4",
26
26
"js-yaml": "^4.1.0",
27
-
"zod": "^4.0.5"
27
+
"zod": "^4.0.14"
28
28
},
29
29
"repository": {
30
30
"url": "https://github.com/indexxing/echo"
sqlite.db
sqlite.db
This is a binary file and will not be displayed.
+2
-1
src/db/index.ts
+2
-1
src/db/index.ts
···
1
1
import { drizzle } from "drizzle-orm/bun-sqlite";
2
2
import { Database } from "bun:sqlite";
3
+
import * as schema from "./schema";
3
4
import { env } from "../env";
4
5
5
6
const sqlite = new Database(env.DB_PATH);
6
-
export default drizzle(sqlite);
7
+
export default drizzle(sqlite, { schema });
+3
src/db/schema.ts
+3
src/db/schema.ts
+47
-7
src/handlers/posts.ts
+47
-7
src/handlers/posts.ts
···
1
-
import { isAuthorizedUser, logInteraction } from "../utils/interactions";
1
+
import {
2
+
isAuthorizedUser,
3
+
logInteraction,
4
+
getRecentInteractions,
5
+
} from "../utils/interactions";
2
6
import * as threadUtils from "../utils/thread";
3
7
import modelPrompt from "../model/prompt.txt";
4
8
import { GoogleGenAI } from "@google/genai";
5
-
import { interactions } from "../db/schema";
6
9
import { type Post } from "@skyware/bot";
7
10
import * as c from "../constants";
8
11
import * as tools from "../tools";
9
12
import consola from "consola";
10
13
import { env } from "../env";
14
+
import { MemoryHandler } from "../utils/memory";
15
+
import * as yaml from "js-yaml";
11
16
12
17
const logger = consola.withTag("Post Handler");
13
18
14
19
type SupportedFunctionCall = typeof c.SUPPORTED_FUNCTION_CALLS[number];
15
20
16
-
async function generateAIResponse(parsedThread: string) {
21
+
async function generateAIResponse(memory: string, parsedThread: string) {
17
22
const genai = new GoogleGenAI({
18
23
apiKey: env.GEMINI_API_KEY,
19
24
});
···
38
43
text: modelPrompt
39
44
.replace("{{ administrator }}", env.ADMIN_HANDLE)
40
45
.replace("{{ handle }}", env.HANDLE),
46
+
},
47
+
{
48
+
text: memory,
41
49
},
42
50
],
43
51
},
···
126
134
return;
127
135
}
128
136
129
-
await logInteraction(post);
130
-
131
-
if (await threadUtils.isThreadMuted(post)) {
137
+
const isMuted = await threadUtils.isThreadMuted(post);
138
+
if (isMuted) {
132
139
logger.warn("Thread is muted.");
140
+
await logInteraction(post, {
141
+
responseText: null,
142
+
wasMuted: true,
143
+
});
133
144
return;
134
145
}
135
146
···
137
148
const parsedThread = threadUtils.parseThread(thread);
138
149
logger.success("Generated thread context:", parsedThread);
139
150
140
-
const inference = await generateAIResponse(parsedThread);
151
+
const botMemory = new MemoryHandler(
152
+
env.DID,
153
+
await MemoryHandler.getBlocks(env.DID),
154
+
);
155
+
const userMemory = new MemoryHandler(
156
+
post.author.did,
157
+
await MemoryHandler.getBlocks(post.author.did),
158
+
);
159
+
160
+
const recentInteractions = await getRecentInteractions(
161
+
post.author.did,
162
+
thread,
163
+
);
164
+
165
+
const memory = yaml.dump({
166
+
users_with_memory_blocks: {
167
+
[env.HANDLE]: botMemory.parseBlocks(),
168
+
[post.author.handle]: userMemory.parseBlocks(),
169
+
},
170
+
recent_interactions: recentInteractions,
171
+
});
172
+
173
+
logger.log("Parsed memory blocks: ", memory);
174
+
175
+
const inference = await generateAIResponse(memory, parsedThread);
141
176
logger.success("Generated text:", inference.text);
142
177
143
178
const responseText = inference.text;
144
179
if (responseText) {
145
180
await sendResponse(post, responseText);
146
181
}
182
+
183
+
await logInteraction(post, {
184
+
responseText: responseText ?? null,
185
+
wasMuted: false,
186
+
});
147
187
} catch (error) {
148
188
logger.error("Error in post handler:", error);
149
189
+37
-6
src/utils/interactions.ts
+37
-6
src/utils/interactions.ts
···
1
1
import { interactions } from "../db/schema";
2
2
import type { Post } from "@skyware/bot";
3
+
import { desc, notInArray } from "drizzle-orm";
3
4
import { env } from "../env";
4
5
import db from "../db";
5
6
···
9
10
: env.AUTHORIZED_USERS.includes(did as any);
10
11
}
11
12
12
-
export async function logInteraction(post: Post): Promise<void> {
13
-
await db.insert(interactions).values([{
14
-
uri: post.uri,
15
-
did: post.author.did,
16
-
}]);
13
+
export async function logInteraction(
14
+
post: Post,
15
+
options: {
16
+
responseText: string | null;
17
+
wasMuted: boolean;
18
+
},
19
+
): Promise<void> {
20
+
await db.insert(interactions).values([
21
+
{
22
+
uri: post.uri,
23
+
did: post.author.did,
24
+
post: post.text,
25
+
response: options.responseText,
26
+
muted: options.wasMuted,
27
+
},
28
+
]);
29
+
30
+
console.log(`Logged interaction, initiated by @${post.author.handle}`);
31
+
}
32
+
33
+
export async function getRecentInteractions(did: string, thread: Post[]) {
34
+
const threadUris = thread.map((p) => p.uri);
35
+
36
+
const recentInteractions = await db.query.interactions.findMany({
37
+
where: (interactions, { eq, and, notInArray }) =>
38
+
and(
39
+
eq(interactions.did, did),
40
+
notInArray(interactions.uri, threadUris),
41
+
),
42
+
orderBy: (interactions, { desc }) => [desc(interactions.created_at)],
43
+
limit: 5,
44
+
});
17
45
18
-
console.log(`Logged interaction, initiated by @${post.author.handle}`);
46
+
return recentInteractions.map((i) => ({
47
+
post: i.post,
48
+
response: i.response,
49
+
}));
19
50
}
+130
src/utils/memory.ts
+130
src/utils/memory.ts
···
1
+
import { and, desc, eq } from "drizzle-orm";
2
+
import db from "../db";
3
+
import { memory_block_entries, memory_blocks } from "../db/schema";
4
+
import * as yaml from "js-yaml";
5
+
6
+
type MemoryBlock = {
7
+
id: number;
8
+
name: string;
9
+
description: string;
10
+
mutable: boolean;
11
+
entries: Entry[];
12
+
};
13
+
14
+
type Entry = {
15
+
id: number;
16
+
block_id: number;
17
+
label: string;
18
+
value: string;
19
+
added_by: string | null;
20
+
created_at: Date | null;
21
+
};
22
+
23
+
export class MemoryHandler {
24
+
did: string;
25
+
blocks: MemoryBlockHandler[];
26
+
27
+
constructor(did: string, blocks: MemoryBlockHandler[]) {
28
+
this.did = did;
29
+
this.blocks = blocks;
30
+
}
31
+
32
+
static async getBlocks(did: string) {
33
+
const blocks = await db
34
+
.select({
35
+
id: memory_blocks.id,
36
+
name: memory_blocks.name,
37
+
description: memory_blocks.description,
38
+
mutable: memory_blocks.mutable,
39
+
})
40
+
.from(memory_blocks)
41
+
.where(eq(memory_blocks.did, did));
42
+
43
+
const hydratedBlocks = [];
44
+
45
+
for (const block of blocks) {
46
+
const entries = await db
47
+
.select()
48
+
.from(memory_block_entries)
49
+
.where(eq(memory_block_entries.block_id, block.id))
50
+
.orderBy(desc(memory_block_entries.id))
51
+
.limit(15);
52
+
53
+
hydratedBlocks.push({
54
+
...block,
55
+
entries,
56
+
});
57
+
}
58
+
59
+
if (hydratedBlocks.length == 0) {
60
+
const [newBlock] = await db
61
+
.insert(memory_blocks)
62
+
.values([
63
+
{
64
+
did,
65
+
name: "memory",
66
+
description: "User memory",
67
+
mutable: false,
68
+
},
69
+
])
70
+
.returning();
71
+
72
+
hydratedBlocks.push({
73
+
...newBlock,
74
+
entries: [],
75
+
});
76
+
}
77
+
78
+
return hydratedBlocks.map(
79
+
(block) =>
80
+
new MemoryBlockHandler(
81
+
block as MemoryBlock,
82
+
),
83
+
);
84
+
}
85
+
86
+
public parseBlocks() {
87
+
return this.blocks.map((handler) => ({
88
+
name: handler.block.name,
89
+
description: handler.block.description,
90
+
entries: handler.block.entries.map((entry) => ({
91
+
label: entry.label,
92
+
value: entry.value,
93
+
added_by: entry.added_by || "nobody",
94
+
})),
95
+
}));
96
+
}
97
+
}
98
+
99
+
export class MemoryBlockHandler {
100
+
block: MemoryBlock;
101
+
102
+
constructor(block: MemoryBlock) {
103
+
this.block = block;
104
+
}
105
+
106
+
public async createEntry(label: string, value: string) {
107
+
const [entry] = await db
108
+
.insert(memory_block_entries)
109
+
.values([
110
+
{
111
+
block_id: this.block.id,
112
+
label,
113
+
value,
114
+
},
115
+
])
116
+
.returning();
117
+
118
+
if (!entry) {
119
+
return {
120
+
added_to_memory: false,
121
+
};
122
+
}
123
+
124
+
this.block.entries.push(entry);
125
+
126
+
return {
127
+
added_to_memory: true,
128
+
};
129
+
}
130
+
}