Skip to content

Commit 7a811cd

Browse files
author
Sorra
committed
SB-0MNGPWAET002LCGD: Post OpenBrain summaries to Discord threads
1 parent f248601 commit 7a811cd

9 files changed

Lines changed: 690 additions & 31 deletions

File tree

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,15 @@ DISCORD_CHANNEL_ID=your_channel_id_here
88
# Optional: Log level (debug, info, warn, error)
99
LOG_LEVEL=info
1010

11+
# Optional: post a generated summary to Discord after successful `ob add`
12+
SEND_SUMMARY_ON_INSERT=true
13+
14+
# Optional: fallback target channel ID when no thread is available
15+
# DEFAULT_DISCORD_CHANNEL_ID=123456789012345678
16+
17+
# Optional: OpenBrain item URL template used in summary messages
18+
# Supports placeholders: {id} and {url}
19+
# OPENBRAIN_ITEM_URL_TEMPLATE=https://openbrain.local/items/{id}
20+
1121
# Optional: Comma-separated list of Discord user IDs allowed to submit file:// URLs
1222
ALLOWED_FILE_URL_USERS=

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ See [Local Setup](#local-setup) below for detailed instructions.
7676
- `YOUTUBE_CAPTION_LANGUAGE` - Preferred caption language (default: `en`)
7777
- `ENABLE_YOUTUBE_CAPTIONS` - Enable/disable captions (default: `true`)
7878

79+
Optional (for summary posting in Discord threads):
80+
- `SEND_SUMMARY_ON_INSERT` - Post generated summary after successful `ob add` (default: `true`)
81+
- `DEFAULT_DISCORD_CHANNEL_ID` - Fallback channel if thread posting is not available
82+
- `OPENBRAIN_ITEM_URL_TEMPLATE` - Template for OpenBrain item link in summary messages, supports `{id}` and `{url}` placeholders
83+
7984
4. **Set up the database:**
8085

8186
```bash
@@ -230,19 +235,28 @@ $ sb add --ndjson https://example.com/article
230235
**Webhook Events:**
231236
When using `--format webhook`, each progress event is POSTed as JSON to the provided URL. The webhook receives the same JSON structure as NDJSON output.
232237

233-
### Context Flags for Bot Integration
238+
### Context Tags for Bot Integration
234239

235-
When the Discord bot invokes the CLI, it passes context via flags:
240+
When the Discord bot invokes OpenBrain CLI commands, it passes Discord context as metadata tags:
236241

237242
```bash
238-
sb add \
239-
--channel-id "123456789" \
240-
--message-id "987654321" \
241-
--author-id "111222333" \
243+
ob add \
244+
--tag "discord_channel_id:123456789" \
245+
--tag "discord_message_id:987654321" \
246+
--tag "discord_author_id:111222333" \
242247
https://example.com/article
243248
```
244249

245-
These flags associate the operation with Discord entities but are optional for standalone CLI usage.
250+
These tags associate operations with Discord entities for traceability while remaining valid OpenBrain CLI arguments.
251+
252+
### Automatic Summary Posting
253+
254+
After a successful URL add, the bot can call `ob summary <url>` and post the generated summary into the processing thread (or a configured fallback channel).
255+
256+
- Retries summary generation up to 3 times with exponential backoff
257+
- Includes metadata in the message: OpenBrain item link, source URL, item id, author, timestamp
258+
- Uses item-id deduplication in-process to avoid posting duplicate summaries
259+
- Posts a manual-review notice if summary generation fails after retries
246260

247261
### Global Options
248262

src/bot/cli-runner.ts

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export interface AddProgressEvent {
5252
title?: string;
5353
/** Optional timestamp */
5454
timestamp?: string;
55+
/** Optional OpenBrain item id */
56+
id?: number | string;
5557
}
5658

5759
/**
@@ -90,6 +92,10 @@ export interface AddResult {
9092
error?: string;
9193
/** Title of the page if successful */
9294
title?: string;
95+
/** OpenBrain item id if provided by CLI events */
96+
id?: number;
97+
/** Timestamp if provided by CLI events */
98+
timestamp?: string;
9399
}
94100

95101
/**
@@ -106,6 +112,20 @@ export interface QueueResult {
106112
id?: number;
107113
}
108114

115+
/**
116+
* Result of summary command
117+
*/
118+
export interface SummaryResult {
119+
/** Whether the operation succeeded */
120+
success: boolean;
121+
/** URL that was summarized */
122+
url: string;
123+
/** Generated summary text */
124+
summary?: string;
125+
/** Error message if failed */
126+
error?: string;
127+
}
128+
109129
/**
110130
* Statistics result from CLI
111131
*/
@@ -293,18 +313,23 @@ function runCliSubprocess(
293313
timeoutMs = DEFAULT_TIMEOUT_MS,
294314
} = options;
295315

316+
const supportsDiscordContextTags = command === "add" || command === "queue";
317+
296318
// Build command arguments
297319
const cmdArgs = [command];
298320

299-
// Add context flags if provided
300-
if (channelId) {
301-
cmdArgs.push("--channel-id", channelId);
302-
}
303-
if (messageId) {
304-
cmdArgs.push("--message-id", messageId);
305-
}
306-
if (authorId) {
307-
cmdArgs.push("--author-id", authorId);
321+
// Add context tags for commands that support metadata tags.
322+
// OpenBrain CLI does not support --channel-id/--message-id/--author-id flags.
323+
if (supportsDiscordContextTags) {
324+
if (channelId) {
325+
cmdArgs.push("--tag", `discord_channel_id:${channelId}`);
326+
}
327+
if (messageId) {
328+
cmdArgs.push("--tag", `discord_message_id:${messageId}`);
329+
}
330+
if (authorId) {
331+
cmdArgs.push("--tag", `discord_author_id:${authorId}`);
332+
}
308333
}
309334

310335
// Add remaining args
@@ -443,19 +468,31 @@ export async function* runAddCommand(
443468
return result;
444469
}
445470

471+
const rawId = (lastEvent as { id?: unknown } | null)?.id;
472+
const eventId =
473+
typeof rawId === "number"
474+
? rawId
475+
: typeof rawId === "string" && /^\d+$/.test(rawId)
476+
? parseInt(rawId, 10)
477+
: undefined;
478+
446479
// Determine result from last event
447480
if (lastEvent?.phase === "completed") {
448481
const result: AddResult = {
449482
success: true,
450483
url,
451484
title: lastEvent.title,
485+
id: eventId,
486+
timestamp: lastEvent.timestamp,
452487
};
453488
return result;
454489
} else if (lastEvent?.phase === "failed") {
455490
const result: AddResult = {
456491
success: false,
457492
url,
458493
error: lastEvent.message || "Unknown error during ingestion",
494+
id: eventId,
495+
timestamp: lastEvent.timestamp,
459496
};
460497
return result;
461498
}
@@ -539,6 +576,54 @@ export async function runQueueCommand(
539576
};
540577
}
541578

579+
/**
580+
* Run the `ob summary` command
581+
*
582+
* @param url - URL to summarize
583+
* @param options - Context and runner options
584+
* @returns Promise that resolves with summary result
585+
*/
586+
export async function runSummaryCommand(
587+
url: string,
588+
options: RunnerOptions = {}
589+
): Promise<SummaryResult> {
590+
const { stdoutIterator, exitPromise } = runCliSubprocess(
591+
"summary",
592+
[url],
593+
options
594+
);
595+
596+
const stdoutLines: string[] = [];
597+
for await (const line of stdoutIterator) {
598+
stdoutLines.push(line);
599+
}
600+
601+
const { exitCode, stderr } = await exitPromise;
602+
603+
if (exitCode !== 0) {
604+
return {
605+
success: false,
606+
url,
607+
error: stderr.trim() || `CLI exited with code ${exitCode}`,
608+
};
609+
}
610+
611+
const summary = stdoutLines.join("\n").trim();
612+
if (!summary) {
613+
return {
614+
success: false,
615+
url,
616+
error: "No summary output received",
617+
};
618+
}
619+
620+
return {
621+
success: true,
622+
url,
623+
summary,
624+
};
625+
}
626+
542627
/**
543628
* Run the `sb stats` command
544629
*

src/config/bot.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,28 @@ import { z } from "zod";
33

44
dotenv.config();
55

6+
const envBoolean = z.preprocess((value) => {
7+
if (value === undefined || value === null || value === "") {
8+
return undefined;
9+
}
10+
11+
if (typeof value === "boolean") {
12+
return value;
13+
}
14+
15+
if (typeof value === "string") {
16+
const normalized = value.trim().toLowerCase();
17+
if (["1", "true", "yes", "on"].includes(normalized)) {
18+
return true;
19+
}
20+
if (["0", "false", "no", "off"].includes(normalized)) {
21+
return false;
22+
}
23+
}
24+
25+
return value;
26+
}, z.boolean());
27+
628
// Bot configuration schema - only includes what the bot actually needs
729
// CLI-related config has been moved to the openBrain repository
830
export const botConfigSchema = z.object({
@@ -12,6 +34,11 @@ export const botConfigSchema = z.object({
1234

1335
// Optional configuration
1436
LOG_LEVEL: z.string().optional().default("info"),
37+
38+
// Summary posting behavior
39+
SEND_SUMMARY_ON_INSERT: envBoolean.optional().default(true),
40+
DEFAULT_DISCORD_CHANNEL_ID: z.string().optional(),
41+
OPENBRAIN_ITEM_URL_TEMPLATE: z.string().optional(),
1542

1643
// File URL configuration
1744
ALLOWED_FILE_URL_USERS: z.string().optional().transform(v => v ? v.split(',').map(id => id.trim()) : []),

0 commit comments

Comments
 (0)