From ed091711c87db3f12e2e57cc2b76ae7084890657 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Tue, 9 Jun 2026 09:42:26 +0200 Subject: [PATCH] =?UTF-8?q?feat(plan5):=20openaiClient=20=E2=80=94=20g?= =?UTF-8?q?=C3=A9n=C3=A9ration=20descriptions=20GPT-4o=20(TDD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/openaiClient.test.ts | 46 ++++++++++++++++++++++++++++++++++++ src/lib/openaiClient.ts | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/lib/openaiClient.test.ts create mode 100644 src/lib/openaiClient.ts diff --git a/src/lib/openaiClient.test.ts b/src/lib/openaiClient.test.ts new file mode 100644 index 0000000..0a5c607 --- /dev/null +++ b/src/lib/openaiClient.test.ts @@ -0,0 +1,46 @@ +import { generateDescriptions } from './openaiClient' + +const mockFetch = jest.fn() +global.fetch = mockFetch + +const mockResponse = (content: string) => ({ + ok: true, + json: async () => ({ choices: [{ message: { content } }] }), +}) + +describe('generateDescriptions', () => { + beforeEach(() => { + process.env.OPENAI_API_KEY = 'sk-test' + mockFetch.mockReset() + }) + + it('retourne shortDesc et longDesc depuis OpenAI', async () => { + mockFetch + .mockResolvedValueOnce(mockResponse('

Description courte

')) + .mockResolvedValueOnce(mockResponse('

Description longue

')) + const result = await generateDescriptions({ + title: 'Perceuse', rawDescription: 'Une perceuse', comment: '', vendor: 'BOSCH', famille: 'OUTILLAGE', + }) + expect(result.shortDesc).toBe('

Description courte

') + expect(result.longDesc).toBe('

Description longue

') + expect(mockFetch).toHaveBeenCalledTimes(2) + }) + + it('lance les deux requêtes en parallèle', async () => { + mockFetch.mockResolvedValue(mockResponse('

ok

')) + await generateDescriptions({ title: 'T', rawDescription: '', comment: '', vendor: '', famille: '' }) + expect(mockFetch).toHaveBeenCalledTimes(2) + }) + + it('lance une erreur si OPENAI_API_KEY manquant', async () => { + delete process.env.OPENAI_API_KEY + await expect(generateDescriptions({ title: 'T', rawDescription: '', comment: '', vendor: '', famille: '' })) + .rejects.toThrow('OPENAI_API_KEY') + }) + + it('lance une erreur si la réponse OpenAI est non-ok', async () => { + mockFetch.mockResolvedValue({ ok: false, status: 429, text: async () => 'rate limited' }) + await expect(generateDescriptions({ title: 'T', rawDescription: '', comment: '', vendor: '', famille: '' })) + .rejects.toThrow('429') + }) +}) diff --git a/src/lib/openaiClient.ts b/src/lib/openaiClient.ts new file mode 100644 index 0000000..441b497 --- /dev/null +++ b/src/lib/openaiClient.ts @@ -0,0 +1,46 @@ +interface DescriptionInput { + title: string + rawDescription: string + comment: string + vendor: string + famille: string +} + +interface Descriptions { + shortDesc: string + longDesc: string +} + +async function callOpenAI(prompt: string): Promise { + const apiKey = process.env.OPENAI_API_KEY + if (!apiKey) throw new Error('OPENAI_API_KEY requis') + const res = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, + body: JSON.stringify({ model: 'gpt-4o', messages: [{ role: 'user', content: prompt }], temperature: 0.7 }), + }) + if (!res.ok) { + const err = await res.text() + throw new Error(`OpenAI API error: ${res.status} — ${err}`) + } + const data = await res.json() + return (data.choices?.[0]?.message?.content ?? '').trim() +} + +function buildPrompt(input: DescriptionInput, length: number): string { + return `A partir des caractéristiques suivantes, crée un texte descriptif de ${length} caractères en faisant attention au SEO pour qu'il soit en rapport avec la catégorie ${input.famille} et le produit ${input.title} : +Nom = ${input.title} +Description : ${input.rawDescription} +Commentaire : ${input.comment} +Marque : ${input.vendor} +Formate ce texte en HTML. +Ta réponse doit commencer directement par une balise HTML (comme

ou

), sans bloc \`\`\`html, sans aucun mot ou encadré autour.` +} + +export async function generateDescriptions(input: DescriptionInput): Promise { + const [shortDesc, longDesc] = await Promise.all([ + callOpenAI(buildPrompt(input, 400)), + callOpenAI(buildPrompt(input, 1000)), + ]) + return { shortDesc, longDesc } +}