const DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions' export interface ItineraryCandidate { id: string name: string description: string category: string categorySlug: string neighborhood: string address: string priceRange: number rating: number | null slug: string isFeatured: boolean } interface GenerateItineraryParams { days: number style: string locale: string candidates: ItineraryCandidate[] } const LANGUAGE_NAMES: Record = { tr: 'Turkish', en: 'English', ru: 'Russian', } const STYLE_LABELS: Record = { gastronomy: 'gastronomy and food-focused', relaxation: 'relaxation and slow-paced', adventure: 'adventure and outdoor-focused', } /** * Grounded itinerary generation: the model may ONLY recommend businesses from * `candidates` — it never invents a place. This is the core anti-hallucination * rule from the product spec (docs/prd-3.md §3.3). */ export async function generateItineraryContent(params: GenerateItineraryParams): Promise { const apiKey = process.env.DEEPSEEK_API_KEY if (!apiKey) { throw new Error('DEEPSEEK_API_KEY tanımlı değil') } const languageName = LANGUAGE_NAMES[params.locale] || LANGUAGE_NAMES.tr const styleLabel = STYLE_LABELS[params.style] || params.style const candidateList = params.candidates .map((c) => { const price = '₺'.repeat(c.priceRange) const featured = c.isFeatured ? ' [featured]' : '' return `- id:${c.id}${featured} | ${c.name} | ${c.category} | ${c.neighborhood} | ${price} | rating:${c.rating ?? 'n/a'} | link:/${c.categorySlug}/${c.slug}\n ${c.description}` }) .join('\n') const systemPrompt = `You are the local guide writer for "Marmaris Local", a curated directory of Marmaris, Turkey. You write personalized multi-day itineraries. STRICT RULE: you may only recommend businesses from the CANDIDATE LIST below. Never invent, assume, or mention any place that is not in this list. If the list has fewer suitable places than needed, reuse the best candidates rather than inventing new ones. For every place you recommend, include its markdown link exactly as given in the candidate list (e.g. [Place Name](/category-slug/place-slug)). Write in ${languageName}. Output valid Markdown: use "## gün N" / "## day N" style headings per day (translate "day" to ${languageName}), short warm paragraphs, and occasional > blockquote for a "local tip". Avoid generic travel-blog clichés — be specific and grounded in the actual candidate descriptions. Places marked [featured] may be given slight preference when they fit, but relevance always comes first.` const userPrompt = `Number of days: ${params.days} Travel style: ${styleLabel} CANDIDATE LIST (choose only from these): ${candidateList} Write the full ${params.days}-day itinerary now, in Markdown, in ${languageName}.` const res = await fetch(DEEPSEEK_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ model: 'deepseek-chat', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }, ], temperature: 0.7, }), }) if (!res.ok) { const errText = await res.text() throw new Error(`DeepSeek API hatası (${res.status}): ${errText}`) } const json = await res.json() const content = json?.choices?.[0]?.message?.content if (!content) { throw new Error('DeepSeek yanıtı boş döndü') } return linkifyItineraryContent(content, params.candidates) } /** * The model reliably mentions candidate names but doesn't always wrap them in * the requested markdown link syntax. This deterministically links the first * mention of each candidate as a safety net, so every generated plan actually * drives traffic to listing pages regardless of how well the model complied. */ function linkifyItineraryContent(content: string, candidates: ItineraryCandidate[]): string { let result = content for (const c of candidates) { const href = `/${c.categorySlug}/${c.slug}` if (result.includes(`](${href})`)) continue // model already linked it correctly const escapedName = c.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const pattern = new RegExp(`\\*{0,2}${escapedName}\\*{0,2}`) if (pattern.test(result)) { result = result.replace(pattern, `**[${c.name}](${href})**`) } } return result }