diff --git a/bun.lock b/bun.lock index 1626306..98f5284 100644 --- a/bun.lock +++ b/bun.lock @@ -52,6 +52,7 @@ "packages/server": { "name": "server", "dependencies": { + "@huggingface/inference": "^4.13.0", "@prisma/client": "^6.16.2", "dayjs": "^1.11.18", "dotenv": "^17.2.2", @@ -178,6 +179,12 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], + "@huggingface/inference": ["@huggingface/inference@4.13.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.1", "@huggingface/tasks": "^0.19.58" } }, "sha512-oqBHbYoLHpNlEUJp/MG28gCR0JOUmLi9iKKd0oJfTAgTqaDSLhoCsIdS1XfLHbT/CrzZch33cARTR3bO4q/2yQ=="], + + "@huggingface/jinja": ["@huggingface/jinja@0.5.1", "", {}, "sha512-yUZLld4lrM9iFxHCwFQ7D1HW2MWMwSbeB7WzWqFYDWK+rEb+WldkLdAJxUPOmgICMHZLzZGVcVjFh3w/YGubng=="], + + "@huggingface/tasks": ["@huggingface/tasks@0.19.60", "", {}, "sha512-oO3+s+u4bgJh3DjwIe7lE+4iyRzj//WE7V+Z0toQsj81IaGp5kBEK1uxtWz5U32PUF9by6yKAEtCCW1hprU3ug=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -1138,12 +1145,12 @@ "raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], - "server/@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="], + "server/@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "server/@types/bun/bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="], + "server/@types/bun/bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], } } diff --git a/packages/server/package.json b/packages/server/package.json index cadba02..b9a0a4b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -18,6 +18,7 @@ "typescript": "^5" }, "dependencies": { + "@huggingface/inference": "^4.13.0", "@prisma/client": "^6.16.2", "dayjs": "^1.11.18", "dotenv": "^17.2.2", diff --git a/packages/server/prompts/summarizeReviewLlama.txt b/packages/server/prompts/summarizeReviewLlama.txt new file mode 100644 index 0000000..9017428 --- /dev/null +++ b/packages/server/prompts/summarizeReviewLlama.txt @@ -0,0 +1,2 @@ +Summarize the following customer reviews into a short paragraph +highlighting key themes, both positive and negative. diff --git a/packages/server/providers/llm.provider.ts b/packages/server/providers/llm.provider.ts index 96a5f61..7c10031 100644 --- a/packages/server/providers/llm.provider.ts +++ b/packages/server/providers/llm.provider.ts @@ -1,4 +1,6 @@ import OpenAI from 'openai'; +import { InferenceClient } from '@huggingface/inference'; +import summarizeReviewPrompt from '../prompts/summarizeReviewLlama.txt'; type GenerateResponseQuery = { prompt: string; @@ -12,6 +14,14 @@ type GenerateResponseResult = { id: string; }; +type SummarizeTextQuery = { + text: string; +}; + +type SummarizeTextResult = { + summary: string; +}; + export class LlmProvider { constructor(private readonly openAiClient: OpenAI) {} async generateResponse({ @@ -31,10 +41,42 @@ export class LlmProvider { return { message: response.output_text, id: response.id }; } + + async summarize({ text }: SummarizeTextQuery): Promise { + const output = await inferenceClient.summarization({ + model: 'facebook/bart-large-cnn', + inputs: text, + provider: 'hf-inference', + }); + return { summary: output.summary_text }; + } + + async summarizeReviews({ + text: reviews, + }: SummarizeTextQuery): Promise { + const chatCompletion = await inferenceClient.chatCompletion({ + provider: 'novita', + model: 'meta-llama/Llama-3.1-8B-Instruct', + messages: [ + { + role: 'system', + content: summarizeReviewPrompt, + }, + { + role: 'user', + content: reviews, + }, + ], + }); + + return { summary: chatCompletion?.choices[0]?.message.content ?? '' }; + } } const openAiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); +const inferenceClient = new InferenceClient(process.env.HF_TOKEN); + export const llmProvider = new LlmProvider(openAiClient); diff --git a/packages/server/services/review.service.ts b/packages/server/services/review.service.ts index a596fb5..08ee521 100644 --- a/packages/server/services/review.service.ts +++ b/packages/server/services/review.service.ts @@ -34,10 +34,15 @@ export class ReviewService { } const joinedReviews = reviews.map((r) => r.content).join('\n\n'); + /* + Replace if you want to use OpenAI instead of an open-source model const prompt = template.replace('{{ reviews }}', joinedReviews); const { message: summary } = await this.llmProvider.generateResponse({ prompt, maxOutputTokens: 500, + }); */ + const { summary } = await this.llmProvider.summarizeReviews({ + text: joinedReviews, }); await this.reviewsRepository.upsertReviewSummary(productId, summary); return summary;