tangled
alpha
login
or
join now
indexx.dev
/
aero
9
fork
atom
A simple Bluesky bot to make sense of the noise, with responses powered by Gemini, similar to Grok.
9
fork
atom
overview
issues
3
pulls
pipelines
update inference structure (closes #3)
indexx.dev
4 months ago
2fbf412d
dda25521
+61
-69
3 changed files
expand all
collapse all
unified
split
docker-compose.yml
src
handlers
messages.ts
utils
conversation.ts
+1
docker-compose.yml
···
12
12
- "HANDLE=${HANDLE:?}"
13
13
- "APP_PASSWORD=${APP_PASSWORD:?}"
14
14
- "GEMINI_API_KEY=${GEMINI_API_KEY:?}"
15
15
+
- "USE_JETSTREAM=${USE_JETSTREAM:-false}"
15
16
volumes:
16
17
- aero_db:/sqlite.db
+39
-43
src/handlers/messages.ts
···
18
18
19
19
type SupportedFunctionCall = typeof c.SUPPORTED_FUNCTION_CALLS[number];
20
20
21
21
-
async function generateAIResponse(parsedConversation: string) {
21
21
+
async function generateAIResponse(parsedContext: string, messages: {
22
22
+
role: string;
23
23
+
parts: {
24
24
+
text: string;
25
25
+
}[];
26
26
+
}[]) {
22
27
const config = {
23
28
model: env.GEMINI_MODEL,
24
29
config: {
···
37
42
],
38
43
},
39
44
{
40
40
-
role: "user" as const,
45
45
+
role: "model" as const,
41
46
parts: [
42
47
{
43
43
-
text:
44
44
-
`Below is the yaml for the current conversation. The last message is the one to respond to. The post is the current one you are meant to be analyzing.
45
45
-
46
46
-
${parsedConversation}`,
48
48
+
text: parsedContext,
47
49
},
48
50
],
49
51
},
52
52
+
...messages,
50
53
];
51
54
52
55
let inference = await c.ai.models.generateContent({
···
99
102
return inference;
100
103
}
101
104
102
102
-
async function sendResponse(
103
103
-
conversation: Conversation,
104
104
-
text: string,
105
105
-
): Promise<void> {
106
106
-
if (exceedsGraphemes(text)) {
107
107
-
multipartResponse(conversation, text);
108
108
-
} else {
109
109
-
conversation.sendMessage({
110
110
-
text,
111
111
-
});
112
112
-
}
113
113
-
}
114
114
-
115
105
export async function handler(message: ChatMessage): Promise<void> {
116
106
const conversation = await message.getConversation();
117
107
// ? Conversation should always be able to be found, but just in case:
···
132
122
return;
133
123
}
134
124
135
135
-
const today = new Date();
136
136
-
today.setHours(0, 0, 0, 0);
137
137
-
const tomorrow = new Date(today);
138
138
-
tomorrow.setDate(tomorrow.getDate() + 1);
125
125
+
if (message.senderDid != env.ADMIN_DID) {
126
126
+
const todayStart = new Date();
127
127
+
todayStart.setHours(0, 0, 0, 0);
139
128
140
140
-
const dailyCount = await db
141
141
-
.select({ count: count(messages.id) })
142
142
-
.from(messages)
143
143
-
.where(
144
144
-
and(
145
145
-
eq(messages.did, message.senderDid),
146
146
-
gte(messages.created_at, today),
147
147
-
lt(messages.created_at, tomorrow),
148
148
-
),
149
149
-
);
129
129
+
const dailyCount = await db
130
130
+
.select({ count: count(messages.id) })
131
131
+
.from(messages)
132
132
+
.where(
133
133
+
and(
134
134
+
eq(messages.did, message.senderDid),
135
135
+
gte(messages.created_at, todayStart),
136
136
+
),
137
137
+
);
150
138
151
151
-
if (dailyCount[0]!.count >= env.DAILY_QUERY_LIMIT) {
152
152
-
conversation.sendMessage({
153
153
-
text: c.QUOTA_EXCEEDED_MESSAGE,
154
154
-
});
155
155
-
return;
139
139
+
if (dailyCount[0]!.count >= env.DAILY_QUERY_LIMIT) {
140
140
+
conversation.sendMessage({
141
141
+
text: c.QUOTA_EXCEEDED_MESSAGE,
142
142
+
});
143
143
+
return;
144
144
+
}
156
145
}
157
146
158
147
logger.success("Found conversation");
···
160
149
text: "...",
161
150
});
162
151
163
163
-
const parsedConversation = await parseConversation(conversation);
164
164
-
165
165
-
logger.info("Parsed conversation: ", parsedConversation);
152
152
+
const parsedConversation = await parseConversation(conversation, message);
166
153
167
154
try {
168
168
-
const inference = await generateAIResponse(parsedConversation);
155
155
+
const inference = await generateAIResponse(
156
156
+
parsedConversation.context,
157
157
+
parsedConversation.messages,
158
158
+
);
169
159
if (!inference) {
170
160
throw new Error("Failed to generate text. Returned undefined.");
171
161
}
···
176
166
logger.success("Generated text:", inference.text);
177
167
saveMessage(conversation, env.DID, inference.text!);
178
168
179
179
-
await sendResponse(conversation, responseText);
169
169
+
if (exceedsGraphemes(responseText)) {
170
170
+
multipartResponse(conversation, responseText);
171
171
+
} else {
172
172
+
conversation.sendMessage({
173
173
+
text: responseText,
174
174
+
});
175
175
+
}
180
176
}
181
177
} catch (error) {
182
178
logger.error("Error in post handler:", error);
+21
-26
src/utils/conversation.ts
···
14
14
/*
15
15
Utilities
16
16
*/
17
17
-
const resolveDid = (convo: Conversation, did: string) =>
18
18
-
convo.members.find((actor) => actor.did == did)!;
19
19
-
20
17
const getUserDid = (convo: Conversation) =>
21
18
convo.members.find((actor) => actor.did != env.DID)!;
22
19
···
29
26
/*
30
27
Conversations
31
28
*/
32
32
-
async function initConvo(convo: Conversation) {
29
29
+
async function initConvo(convo: Conversation, initialMessage: ChatMessage) {
33
30
const user = getUserDid(convo);
34
34
-
35
35
-
const initialMessage = (await convo.getMessages()).messages[0] as
36
36
-
| ChatMessage
37
37
-
| undefined;
38
38
-
if (!initialMessage) {
39
39
-
throw new Error("Failed to get initial message of conversation");
40
40
-
}
41
31
42
32
const postUri = await parseMessagePostUri(initialMessage);
43
33
if (!postUri) {
···
87
77
return convo;
88
78
}
89
79
90
90
-
export async function parseConversation(convo: Conversation) {
80
80
+
export async function parseConversation(
81
81
+
convo: Conversation,
82
82
+
latestMessage: ChatMessage,
83
83
+
) {
91
84
let row = await getConvo(convo.id);
92
85
if (!row) {
93
93
-
row = await initConvo(convo);
86
86
+
row = await initConvo(convo, latestMessage);
94
87
} else {
95
95
-
const latestMessage = (await convo.getMessages())
96
96
-
.messages[0] as ChatMessage;
97
97
-
98
88
const postUri = await parseMessagePostUri(latestMessage);
99
89
if (postUri) {
100
90
const [updatedRow] = await db
···
128
118
129
119
let parseResult = null;
130
120
try {
131
131
-
parseResult = yaml.dump({
132
132
-
post: await parsePost(post, true),
121
121
+
parseResult = {
122
122
+
context: yaml.dump({
123
123
+
post: await parsePost(post, true),
124
124
+
}),
133
125
messages: convoMessages.map((message) => {
134
134
-
const profile = resolveDid(convo, message.did);
126
126
+
const role = message.did == env.DID ? "model" : "user";
135
127
136
128
return {
137
137
-
user: profile.displayName
138
138
-
? `${profile.displayName} (${profile.handle})`
139
139
-
: `Handle: ${profile.handle}`,
140
140
-
text: message.text,
129
129
+
role,
130
130
+
parts: [
131
131
+
{
132
132
+
text: message.text,
133
133
+
},
134
134
+
],
141
135
};
142
136
}),
143
143
-
});
137
137
+
};
144
138
} catch (e) {
145
139
convo.sendMessage({
146
140
text:
···
169
163
.where(
170
164
and(
171
165
eq(messages.conversationId, convo.id),
172
172
-
eq(messages.postUri, convo!.postUri),
166
166
+
eq(messages.postUri, convo.postUri),
167
167
+
eq(messages.revision, convo.revision),
173
168
),
174
169
)
175
170
.limit(15);
···
192
187
.values({
193
188
conversationId: _convo.id,
194
189
postUri: _convo.postUri,
195
195
-
revision: _convo.postUri,
190
190
+
revision: _convo.revision,
196
191
did,
197
192
text,
198
193
});