From c0a669dc0ac574367cc55981dd11c718e2e8eaa4 Mon Sep 17 00:00:00 2001 From: Riccardo Date: Fri, 20 Sep 2024 02:52:16 +0200 Subject: [PATCH] feat: add summary using Anthropic API --- .env.example | 3 +- .gitignore | 3 + app/api/mailing/route.ts | 4 +- components/email/Newsletter.tsx | 133 ++++++++++++--------- package.json | 1 + utils/anthropic.ts | 13 ++ utils/summarize.ts | 21 ++++ yarn.lock | 206 +++++++++++++++++++++++++++++++- 8 files changed, 325 insertions(+), 59 deletions(-) create mode 100644 utils/anthropic.ts create mode 100644 utils/summarize.ts diff --git a/.env.example b/.env.example index 768ceec..cc0f59f 100644 --- a/.env.example +++ b/.env.example @@ -36,4 +36,5 @@ VERCEL_GIT_REPO_ID="" VERCEL_GIT_REPO_OWNER="" VERCEL_GIT_REPO_SLUG="" VERCEL_URL="" -ANALYZE="false" \ No newline at end of file +ANALYZE="false" +ANTHROPIC_API_KEY="" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5247af1..088f33e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ # production /build +# analyze +/analyze + # yarn /.yarn diff --git a/app/api/mailing/route.ts b/app/api/mailing/route.ts index 0ebd155..4a75413 100644 --- a/app/api/mailing/route.ts +++ b/app/api/mailing/route.ts @@ -83,9 +83,11 @@ export async function GET(request: NextRequest) { const validRankedNews = news.sort((a, b) => b.score - a.score); + const template = await NewsletterTemplate(validRankedNews); + const sent = await sender( users.map(user => user.email), - NewsletterTemplate(validRankedNews) + template ); if (!sent) { diff --git a/components/email/Newsletter.tsx b/components/email/Newsletter.tsx index 731cad4..0042506 100644 --- a/components/email/Newsletter.tsx +++ b/components/email/Newsletter.tsx @@ -1,9 +1,12 @@ import { getSayings } from '@utils/getSayings'; +import { summirize } from '@utils/summarize'; import { textTruncate } from '@utils/textTruncate'; import { NewsType } from '@utils/validationSchemas'; import Template from './Template'; -export default function NewsletterTemplate(stories: NewsType[]) { +export default async function NewsletterTemplate(stories: NewsType[]) { + const summary = await summirize(stories); + return { subject: `What's new from the Hackernews forum?`, template: ( @@ -11,72 +14,90 @@ export default function NewsletterTemplate(stories: NewsType[]) { title={`Here is something ${getSayings[Math.floor(Math.random() * getSayings.length)]}!`} body={ -
- {stories.map(story => { - return ( -
+ <> + {summary && ( +
+

{summary}

+
+ )} +
+ {stories.map(story => { + return (
-

{story.title}

-

- by {story.by} -

-
- {story.text && (
-

500 - ? textTruncate(story.text, 500) + '...' - : story.text +

{story.title}

+

+ by {story.by} +

+
+ {story.text && ( +
-
- )} - {story.url && ( -
- Read more -
- )} -
- ); - })} -
+ > +

500 + ? textTruncate(story.text, 500) + '...' + : story.text + }} + /> +

+ )} + {story.url && ( +
+ Read more +
+ )} + + ); + })} + + } /> ) diff --git a/package.json b/package.json index c64979e..8822677 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "prisma:reset": "npx prisma db push --force-reset" }, "dependencies": { + "@anthropic-ai/sdk": "^0.27.3", "@hookform/resolvers": "^3.3.2", "@next/bundle-analyzer": "^14.2.5", "@prisma/client": "^5.6.0", diff --git a/utils/anthropic.ts b/utils/anthropic.ts new file mode 100644 index 0000000..2b9015c --- /dev/null +++ b/utils/anthropic.ts @@ -0,0 +1,13 @@ +import Anthropic from '@anthropic-ai/sdk'; + +export async function message(text: string) { + const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY + }); + + return anthropic.messages.create({ + model: 'claude-3-5-sonnet-20240620', + max_tokens: 1024, + messages: [{ role: 'user', content: text }] + }); +} diff --git a/utils/summarize.ts b/utils/summarize.ts new file mode 100644 index 0000000..6120d4e --- /dev/null +++ b/utils/summarize.ts @@ -0,0 +1,21 @@ +import { message } from './anthropic'; +import { NewsType } from './validationSchemas'; + +export async function summirize(news: NewsType[]) { + const newsInput = news + .map(n => `TITLE: ${n.title}, CONTENT: ${n.text}, LINK: ${n.url}`) + .join('\n\n'); + + const promptSetup = + "You are a tech journalist with a technology degree and background. Summarize the following list of posts from an online forum as a TL;DR (Too Long; Didn't Read) summary. Your summary should:\n\n1. Be 200-500 words long.\n\n2. Consist multiple phrases in one single paragraph, combining related news items where possible.\n\n3. Highlight the 2-3 most significant or impactful news items.\n\n4. Provide context within broader tech trends or developments.\n\n5. Use a tone that is informative and slightly enthusiastic, aimed at tech-savvy general readers.\n\n6. Group news items by themes or technology areas if applicable.\n\n7. Be formatted for HTML use, with links incorporated as follows: [linked text]\n\nFocus on conveying the key points and their potential impact on the tech landscape. Your response should consist of the summary only.\n\nThe news items are structured as follows:\n\nTITLE: \n\nCONTENT: <content>\n\nLINK: <link>\n\nPlease summarize the following news:"; + try { + const response = await message(promptSetup + newsInput); + + const summary = response.content[0] as { text: string }; + + return summary.text; + } catch (error) { + console.error(error); + return; + } +} diff --git a/yarn.lock b/yarn.lock index 1b9ebd3..82085c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,21 @@ __metadata: languageName: node linkType: hard +"@anthropic-ai/sdk@npm:^0.27.3": + version: 0.27.3 + resolution: "@anthropic-ai/sdk@npm:0.27.3" + dependencies: + "@types/node": "npm:^18.11.18" + "@types/node-fetch": "npm:^2.6.4" + abort-controller: "npm:^3.0.0" + agentkeepalive: "npm:^4.2.1" + form-data-encoder: "npm:1.7.2" + formdata-node: "npm:^4.3.2" + node-fetch: "npm:^2.6.7" + checksum: f855a10a2602f69ecfa791f2022ca5b492cc30fafd065138e3a9e12082f0fbece12008782c921746e3aa67bd72afbe69c51cfe90c66638e05e241c1c2f64d346 + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0": version: 7.24.7 resolution: "@babel/code-frame@npm:7.24.7" @@ -742,6 +757,34 @@ __metadata: languageName: node linkType: hard +"@types/node-fetch@npm:^2.6.4": + version: 2.6.11 + resolution: "@types/node-fetch@npm:2.6.11" + dependencies: + "@types/node": "npm:*" + form-data: "npm:^4.0.0" + checksum: c416df8f182ec3826278ea42557fda08f169a48a05e60722d9c8edd4e5b2076ae281c6b6601ad406035b7201f885b0257983b61c26b3f9eb0f41192a807b5de5 + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 22.5.5 + resolution: "@types/node@npm:22.5.5" + dependencies: + undici-types: "npm:~6.19.2" + checksum: 172d02c8e6d921699edcf559c28b3805616bd6481af1b3cb0299f89ad9a6f33b71050434c06ce7b503166054a26275344187c443f99f745d0b12601372452f19 + languageName: node + linkType: hard + +"@types/node@npm:^18.11.18": + version: 18.19.50 + resolution: "@types/node@npm:18.19.50" + dependencies: + undici-types: "npm:~5.26.4" + checksum: d238bb877953fcecda830df140f8722b9ba9644ae63e810fe6fa40cab8285c42f9b34c9529f2144a6f8cfeee5b0ff7fefd9425261e41830157d6710d501b829d + languageName: node + linkType: hard + "@types/node@npm:^20": version: 20.14.11 resolution: "@types/node@npm:20.14.11" @@ -957,6 +1000,15 @@ __metadata: languageName: node linkType: hard +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: "npm:^5.0.0" + checksum: ed84af329f1828327798229578b4fe03a4dd2596ba304083ebd2252666bdc1d7647d66d0b18704477e1f8aa315f055944aa6e859afebd341f12d0a53c37b4b40 + languageName: node + linkType: hard + "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -993,6 +1045,15 @@ __metadata: languageName: node linkType: hard +"agentkeepalive@npm:^4.2.1": + version: 4.5.0 + resolution: "agentkeepalive@npm:4.5.0" + dependencies: + humanize-ms: "npm:^1.2.1" + checksum: dd210ba2a2e2482028f027b1156789744aadbfd773a6c9dd8e4e8001930d5af82382abe19a69240307b1d8003222ce6b0542935038313434b900e351914fc15f + languageName: node + linkType: hard + "aggregate-error@npm:^3.0.0": version: 3.1.0 resolution: "aggregate-error@npm:3.1.0" @@ -1246,6 +1307,13 @@ __metadata: languageName: node linkType: hard +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 3ce727cbc78f69d6a4722517a58ee926c8c21083633b1d3fdf66fd688f6c127a53a592141bd4866f9b63240a86e9d8e974b13919450bd17fa33c2d22c4558ad8 + languageName: node + linkType: hard + "audit-ci@npm:^6.6.1": version: 6.6.1 resolution: "audit-ci@npm:6.6.1" @@ -1604,6 +1672,15 @@ __metadata: languageName: node linkType: hard +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 2e969e637d05d09fa50b02d74c83a1186f6914aae89e6653b62595cc75a221464f884f55f231b8f4df7a49537fba60bdc0427acd2bf324c09a1dbb84837e36e4 + languageName: node + linkType: hard + "commander@npm:^10.0.0": version: 10.0.1 resolution: "commander@npm:10.0.1" @@ -1902,6 +1979,13 @@ __metadata: languageName: node linkType: hard +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020 + languageName: node + linkType: hard + "didyoumean@npm:^1.2.2": version: 1.2.2 resolution: "didyoumean@npm:1.2.2" @@ -2553,6 +2637,13 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 49ff46c3a7facbad3decb31f597063e761785d7fdb3920d4989d7b08c97a61c2f51183e2f3a03130c9088df88d4b489b1b79ab632219901f184f85158508f4c8 + languageName: node + linkType: hard + "eventemitter3@npm:^5.0.1": version: 5.0.1 resolution: "eventemitter3@npm:5.0.1" @@ -2733,6 +2824,34 @@ __metadata: languageName: node linkType: hard +"form-data-encoder@npm:1.7.2": + version: 1.7.2 + resolution: "form-data-encoder@npm:1.7.2" + checksum: 227bf2cea083284411fd67472ccc22f5cb354ca92c00690e11ff5ed942d993c13ac99dea365046306200f8bd71e1a7858d2d99e236de694b806b1f374a4ee341 + languageName: node + linkType: hard + +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + mime-types: "npm:^2.1.12" + checksum: 7264aa760a8cf09482816d8300f1b6e2423de1b02bba612a136857413fdc96d7178298ced106817655facc6b89036c6e12ae31c9eb5bdc16aabf502ae8a5d805 + languageName: node + linkType: hard + +"formdata-node@npm:^4.3.2": + version: 4.4.1 + resolution: "formdata-node@npm:4.4.1" + dependencies: + node-domexception: "npm:1.0.0" + web-streams-polyfill: "npm:4.0.0-beta.3" + checksum: 29622f75533107c1bbcbe31fda683e6a55859af7f48ec354a9800591ce7947ed84cd3ef2b2fcb812047a884f17a1bac75ce098ffc17e23402cd373e49c1cd335 + languageName: node + linkType: hard + "fraction.js@npm:^4.3.7": version: 4.3.7 resolution: "fraction.js@npm:4.3.7" @@ -3187,6 +3306,15 @@ __metadata: languageName: node linkType: hard +"humanize-ms@npm:^1.2.1": + version: 1.2.1 + resolution: "humanize-ms@npm:1.2.1" + dependencies: + ms: "npm:^2.0.0" + checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 + languageName: node + linkType: hard + "husky@npm:^8.0.3": version: 8.0.3 resolution: "husky@npm:8.0.3" @@ -4110,6 +4238,22 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 54bb60bf39e6f8689f6622784e668a3d7f8bed6b0d886f5c3c446cb3284be28b30bf707ed05d0fe44a036f8469976b2629bbea182684977b084de9da274694d7 + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: "npm:1.52.0" + checksum: 89aa9651b67644035de2784a6e665fc685d79aba61857e02b9c8758da874a754aed4a9aced9265f5ed1171fd934331e5516b84a7f0218031b6fa0270eca1e51a + languageName: node + linkType: hard + "mimic-fn@npm:^2.1.0": version: 2.1.0 resolution: "mimic-fn@npm:2.1.0" @@ -4292,7 +4436,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.1.1": +"ms@npm:^2.0.0, ms@npm:^2.1.1": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -4395,6 +4539,7 @@ __metadata: version: 0.0.0-use.local resolution: "nextjs-hackernews@workspace:." dependencies: + "@anthropic-ai/sdk": "npm:^0.27.3" "@commitlint/cli": "npm:^18.4.3" "@commitlint/config-conventional": "npm:^18.4.3" "@hookform/resolvers": "npm:^3.3.2" @@ -4435,6 +4580,27 @@ __metadata: languageName: unknown linkType: soft +"node-domexception@npm:1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233 + languageName: node + linkType: hard + +"node-fetch@npm:^2.6.7": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 10.2.0 resolution: "node-gyp@npm:10.2.0" @@ -6130,6 +6296,13 @@ __metadata: languageName: node linkType: hard +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695 + languageName: node + linkType: hard + "trim-newlines@npm:^3.0.0": version: 3.0.1 resolution: "trim-newlines@npm:3.0.1" @@ -6300,6 +6473,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -6358,6 +6538,20 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:4.0.0-beta.3": + version: 4.0.0-beta.3 + resolution: "web-streams-polyfill@npm:4.0.0-beta.3" + checksum: dcdef67de57d83008f9dc330662b65ba4497315555dd0e4e7bcacb132ffdf8a830eaab8f74ad40a4a44f542461f51223f406e2a446ece1cc29927859b1405853 + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad + languageName: node + linkType: hard + "webpack-bundle-analyzer@npm:4.10.1": version: 4.10.1 resolution: "webpack-bundle-analyzer@npm:4.10.1" @@ -6381,6 +6575,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07 + languageName: node + linkType: hard + "which-boxed-primitive@npm:^1.0.2": version: 1.0.2 resolution: "which-boxed-primitive@npm:1.0.2"