Skip to content

Commit ee9a841

Browse files
Added AI summaries to update notfications
1 parent cbc0ed4 commit ee9a841

18 files changed

Lines changed: 953 additions & 128 deletions

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
"dev:cli": "pnpm --filter patch-pulse dev",
3333
"pp": "node --import tsx packages/cli/src/index.ts",
3434
"dev:vscode-extension": "pnpm --filter patch-pulse-vscode-extension watch",
35-
"format": "pnpm exec oxfmt --write . '!packages/docs/.astro/**'",
36-
"format:check": "pnpm exec oxfmt --check . '!packages/docs/.astro/**'",
35+
"format": "pnpm exec oxfmt --write . '!packages/docs/.astro/**' '!packages/*/convex/_generated/**'",
36+
"format:check": "pnpm exec oxfmt --check . '!packages/docs/.astro/**' '!packages/*/convex/_generated/**'",
3737
"ci:check": "pnpm lint && pnpm format:check && pnpm knip && pnpm typecheck && pnpm test:shared && pnpm test:cli && pnpm test:notifier && pnpm build:cli && pnpm build:vscode-extension && pnpm build:docs",
3838
"knip": "pnpm exec knip",
3939
"lint": "pnpm exec oxlint .",

packages/notifier-bot/.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ SLACK_CLIENT_ID=
33
SLACK_CLIENT_SECRET=
44
SLACK_SIGNING_SECRET=
55
SLACK_REDIRECT_URI=
6+
OPENAI_API_KEY=
7+
OPENAI_SUMMARY_NANO_MODEL=
8+
OPENAI_SUMMARY_MINI_MODEL=
9+
GITHUB_TOKEN=
610

711
# Deployment used by `npx convex dev`
812
CONVEX_DEPLOYMENT=
913

1014
CONVEX_URL=
1115

12-
CONVEX_SITE_URL=
16+
CONVEX_SITE_URL=

packages/notifier-bot/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ It currently supports:
1111
- Slack workspace subscriptions
1212
- npm package polling
1313
- Multi-channel Slack tracking per workspace
14+
- Delayed GitHub metadata enrichment and AI release summaries
1415

1516
More detailed docs live here:
1617

@@ -29,6 +30,7 @@ An hourly cron checks tracked packages for updates. When a newer version is foun
2930
2. GitHub repo metadata is stored on the package when it can be derived from npm metadata.
3031
3. Matching subscribers are grouped by Slack target channel.
3132
4. Slack notifications are sent to the relevant channel or the workspace default channel.
33+
5. If upstream metadata is incomplete, Patch Pulse retries later, updates the original post with release links, and adds an AI summary in the thread when the evidence is strong enough.
3234

3335
## Slack Summary
3436

@@ -61,3 +63,4 @@ Useful commands from this package directory:
6163
- `/npmlist` does not perform live npm lookups. It uses stored package metadata so the response stays fast.
6264
- GitHub links in `/npmlist` appear after polling has enriched a package with repo metadata.
6365
- Update notifications can include richer release links because polling already fetches npm manifests during the update check.
66+
- Patch Pulse uses status reactions on the original Slack post: `` queued/pending, `📝` summary added, `⚠️` no trustworthy release details found after retries, `` processing failed.

packages/notifier-bot/convex/_generated/api.d.ts

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,48 @@
88
* @module
99
*/
1010

11-
import type * as crons from '../crons.js';
12-
import type * as http from '../http.js';
13-
import type * as packages from '../packages.js';
14-
import type * as polling from '../polling.js';
15-
import type * as releaseChecks from '../releaseChecks.js';
16-
import type * as slack_api from '../slack/api.js';
17-
import type * as slack_bannerAsset from '../slack/bannerAsset.js';
18-
import type * as slack_commands from '../slack/commands.js';
19-
import type * as slack_events from '../slack/events.js';
20-
import type * as slack_format from '../slack/format.js';
21-
import type * as slack_interactions from '../slack/interactions.js';
22-
import type * as slack_links from '../slack/links.js';
23-
import type * as slack_oauth from '../slack/oauth.js';
24-
import type * as slack_verify from '../slack/verify.js';
25-
import type * as subscribers from '../subscribers.js';
26-
import type * as subscriptions from '../subscriptions.js';
11+
import type * as aiSummary from "../aiSummary.js";
12+
import type * as crons from "../crons.js";
13+
import type * as http from "../http.js";
14+
import type * as packages from "../packages.js";
15+
import type * as polling from "../polling.js";
16+
import type * as releaseChecks from "../releaseChecks.js";
17+
import type * as releaseEvidence from "../releaseEvidence.js";
18+
import type * as slack_api from "../slack/api.js";
19+
import type * as slack_bannerAsset from "../slack/bannerAsset.js";
20+
import type * as slack_commands from "../slack/commands.js";
21+
import type * as slack_events from "../slack/events.js";
22+
import type * as slack_format from "../slack/format.js";
23+
import type * as slack_interactions from "../slack/interactions.js";
24+
import type * as slack_links from "../slack/links.js";
25+
import type * as slack_oauth from "../slack/oauth.js";
26+
import type * as slack_verify from "../slack/verify.js";
27+
import type * as subscribers from "../subscribers.js";
28+
import type * as subscriptions from "../subscriptions.js";
2729

2830
import type {
2931
ApiFromModules,
3032
FilterApi,
3133
FunctionReference,
32-
} from 'convex/server';
34+
} from "convex/server";
3335

3436
declare const fullApi: ApiFromModules<{
37+
aiSummary: typeof aiSummary;
3538
crons: typeof crons;
3639
http: typeof http;
3740
packages: typeof packages;
3841
polling: typeof polling;
3942
releaseChecks: typeof releaseChecks;
40-
'slack/api': typeof slack_api;
41-
'slack/bannerAsset': typeof slack_bannerAsset;
42-
'slack/commands': typeof slack_commands;
43-
'slack/events': typeof slack_events;
44-
'slack/format': typeof slack_format;
45-
'slack/interactions': typeof slack_interactions;
46-
'slack/links': typeof slack_links;
47-
'slack/oauth': typeof slack_oauth;
48-
'slack/verify': typeof slack_verify;
43+
releaseEvidence: typeof releaseEvidence;
44+
"slack/api": typeof slack_api;
45+
"slack/bannerAsset": typeof slack_bannerAsset;
46+
"slack/commands": typeof slack_commands;
47+
"slack/events": typeof slack_events;
48+
"slack/format": typeof slack_format;
49+
"slack/interactions": typeof slack_interactions;
50+
"slack/links": typeof slack_links;
51+
"slack/oauth": typeof slack_oauth;
52+
"slack/verify": typeof slack_verify;
4953
subscribers: typeof subscribers;
5054
subscriptions: typeof subscriptions;
5155
}>;
@@ -60,7 +64,7 @@ declare const fullApi: ApiFromModules<{
6064
*/
6165
export declare const api: FilterApi<
6266
typeof fullApi,
63-
FunctionReference<any, 'public'>
67+
FunctionReference<any, "public">
6468
>;
6569

6670
/**
@@ -73,7 +77,7 @@ export declare const api: FilterApi<
7377
*/
7478
export declare const internal: FilterApi<
7579
typeof fullApi,
76-
FunctionReference<any, 'internal'>
80+
FunctionReference<any, "internal">
7781
>;
7882

7983
export declare const components: {};

packages/notifier-bot/convex/_generated/api.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @module
99
*/
1010

11-
import { anyApi, componentsGeneric } from 'convex/server';
11+
import { anyApi, componentsGeneric } from "convex/server";
1212

1313
/**
1414
* A utility for referencing Convex functions in your app's API.

packages/notifier-bot/convex/_generated/dataModel.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import type {
1313
DocumentByName,
1414
TableNamesInDataModel,
1515
SystemTableNames,
16-
} from 'convex/server';
17-
import type { GenericId } from 'convex/values';
18-
import schema from '../schema.js';
16+
} from "convex/server";
17+
import type { GenericId } from "convex/values";
18+
import schema from "../schema.js";
1919

2020
/**
2121
* The names of all of your Convex tables.

packages/notifier-bot/convex/_generated/server.d.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import {
1818
GenericQueryCtx,
1919
GenericDatabaseReader,
2020
GenericDatabaseWriter,
21-
} from 'convex/server';
22-
import type { DataModel } from './dataModel.js';
21+
} from "convex/server";
22+
import type { DataModel } from "./dataModel.js";
2323

2424
/**
2525
* Define a query in this Convex app's public API.
@@ -29,7 +29,7 @@ import type { DataModel } from './dataModel.js';
2929
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
3030
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
3131
*/
32-
export declare const query: QueryBuilder<DataModel, 'public'>;
32+
export declare const query: QueryBuilder<DataModel, "public">;
3333

3434
/**
3535
* Define a query that is only accessible from other Convex functions (but not from the client).
@@ -39,7 +39,7 @@ export declare const query: QueryBuilder<DataModel, 'public'>;
3939
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
4040
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
4141
*/
42-
export declare const internalQuery: QueryBuilder<DataModel, 'internal'>;
42+
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
4343

4444
/**
4545
* Define a mutation in this Convex app's public API.
@@ -49,7 +49,7 @@ export declare const internalQuery: QueryBuilder<DataModel, 'internal'>;
4949
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
5050
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
5151
*/
52-
export declare const mutation: MutationBuilder<DataModel, 'public'>;
52+
export declare const mutation: MutationBuilder<DataModel, "public">;
5353

5454
/**
5555
* Define a mutation that is only accessible from other Convex functions (but not from the client).
@@ -59,7 +59,7 @@ export declare const mutation: MutationBuilder<DataModel, 'public'>;
5959
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
6060
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
6161
*/
62-
export declare const internalMutation: MutationBuilder<DataModel, 'internal'>;
62+
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
6363

6464
/**
6565
* Define an action in this Convex app's public API.
@@ -72,15 +72,15 @@ export declare const internalMutation: MutationBuilder<DataModel, 'internal'>;
7272
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
7373
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
7474
*/
75-
export declare const action: ActionBuilder<DataModel, 'public'>;
75+
export declare const action: ActionBuilder<DataModel, "public">;
7676

7777
/**
7878
* Define an action that is only accessible from other Convex functions (but not from the client).
7979
*
8080
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
8181
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
8282
*/
83-
export declare const internalAction: ActionBuilder<DataModel, 'internal'>;
83+
export declare const internalAction: ActionBuilder<DataModel, "internal">;
8484

8585
/**
8686
* Define an HTTP action.

packages/notifier-bot/convex/_generated/server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
internalActionGeneric,
1717
internalMutationGeneric,
1818
internalQueryGeneric,
19-
} from 'convex/server';
19+
} from "convex/server";
2020

2121
/**
2222
* Define a query in this Convex app's public API.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { type UpdateType } from '@patch-pulse/shared';
2+
import { type ReleaseEvidence } from './releaseEvidence';
3+
4+
const OPENAI_API_URL = 'https://api.openai.com/v1/responses';
5+
const DEFAULT_NANO_MODEL = 'gpt-5-nano';
6+
const DEFAULT_MINI_MODEL = 'gpt-5-mini';
7+
8+
function buildSummaryPrompt(args: {
9+
packageName: string;
10+
fromVersion: string;
11+
toVersion: string;
12+
updateType: UpdateType;
13+
evidence: ReleaseEvidence;
14+
}): string {
15+
const { packageName, fromVersion, toVersion, updateType, evidence } = args;
16+
17+
const sections = [
18+
`Package: ${packageName}`,
19+
`Update: ${fromVersion} -> ${toVersion} (${updateType})`,
20+
];
21+
22+
if (evidence.releaseTag) {
23+
sections.push(`Release tag: ${evidence.releaseTag}`);
24+
}
25+
if (evidence.releaseName) {
26+
sections.push(`Release name: ${evidence.releaseName}`);
27+
}
28+
if (evidence.releaseBody) {
29+
sections.push(`Release notes:\n${evidence.releaseBody}`);
30+
}
31+
if (evidence.commitTitles.length > 0) {
32+
sections.push(
33+
`Commit titles:\n${evidence.commitTitles.map((title) => `- ${title}`).join('\n')}`,
34+
);
35+
}
36+
if (evidence.changedFiles.length > 0) {
37+
sections.push(
38+
`Changed files:\n${evidence.changedFiles.map((file) => `- ${file}`).join('\n')}`,
39+
);
40+
}
41+
42+
return sections.join('\n\n');
43+
}
44+
45+
async function callOpenAiSummary(
46+
model: string,
47+
prompt: string,
48+
): Promise<string> {
49+
const apiKey = process.env.OPENAI_API_KEY;
50+
if (!apiKey) return '';
51+
52+
const response = await fetch(OPENAI_API_URL, {
53+
method: 'POST',
54+
headers: {
55+
'Content-Type': 'application/json',
56+
Authorization: `Bearer ${apiKey}`,
57+
},
58+
body: JSON.stringify({
59+
model,
60+
input: [
61+
{
62+
role: 'system',
63+
content: [
64+
{
65+
type: 'input_text',
66+
text:
67+
'You summarize software releases for Slack. Use only the provided evidence. ' +
68+
'Do not speculate. Return a single plain-text sentence under 240 characters. ' +
69+
'If the evidence is insufficient, return exactly INSUFFICIENT.',
70+
},
71+
],
72+
},
73+
{
74+
role: 'user',
75+
content: [{ type: 'input_text', text: prompt }],
76+
},
77+
],
78+
}),
79+
});
80+
81+
if (!response.ok) {
82+
throw new Error(`OpenAI API error ${response.status}`);
83+
}
84+
85+
const data = (await response.json()) as {
86+
output_text?: string;
87+
};
88+
89+
return data.output_text?.trim() ?? '';
90+
}
91+
92+
export async function summarizeReleaseEvidence(args: {
93+
packageName: string;
94+
fromVersion: string;
95+
toVersion: string;
96+
updateType: UpdateType;
97+
evidence: ReleaseEvidence;
98+
}): Promise<{ model: string; summary: string } | null> {
99+
const prompt = buildSummaryPrompt(args);
100+
101+
for (const model of [
102+
process.env.OPENAI_SUMMARY_NANO_MODEL ?? DEFAULT_NANO_MODEL,
103+
process.env.OPENAI_SUMMARY_MINI_MODEL ?? DEFAULT_MINI_MODEL,
104+
]) {
105+
const summary = await callOpenAiSummary(model, prompt);
106+
if (!summary || summary === 'INSUFFICIENT') continue;
107+
return { model, summary };
108+
}
109+
110+
return null;
111+
}

0 commit comments

Comments
 (0)