feat(plan5): openaiClient — génération descriptions GPT-4o (TDD)
This commit is contained in:
@@ -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('<p>Description courte</p>'))
|
||||||
|
.mockResolvedValueOnce(mockResponse('<p>Description longue</p>'))
|
||||||
|
const result = await generateDescriptions({
|
||||||
|
title: 'Perceuse', rawDescription: 'Une perceuse', comment: '', vendor: 'BOSCH', famille: 'OUTILLAGE',
|
||||||
|
})
|
||||||
|
expect(result.shortDesc).toBe('<p>Description courte</p>')
|
||||||
|
expect(result.longDesc).toBe('<p>Description longue</p>')
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lance les deux requêtes en parallèle', async () => {
|
||||||
|
mockFetch.mockResolvedValue(mockResponse('<p>ok</p>'))
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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<string> {
|
||||||
|
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 <p> ou <h1>), sans bloc \`\`\`html, sans aucun mot ou encadré autour.`
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateDescriptions(input: DescriptionInput): Promise<Descriptions> {
|
||||||
|
const [shortDesc, longDesc] = await Promise.all([
|
||||||
|
callOpenAI(buildPrompt(input, 400)),
|
||||||
|
callOpenAI(buildPrompt(input, 1000)),
|
||||||
|
])
|
||||||
|
return { shortDesc, longDesc }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user