Telegram Bot + Mini App for city walking quests. - React 19 + TypeScript + Vite 6 frontend - Express 5 + PostgreSQL backend - grammY Telegram bot with DeepSeek AI - GitLab CI/CD: lint, build, deploy to production Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
180 lines
5.4 KiB
TypeScript
180 lines
5.4 KiB
TypeScript
import OpenAI from 'openai'
|
|
import type { QuestPlan, GeneratedPoint, PointContent } from '../types.js'
|
|
import { getSystemPrompt } from '../prompts/system.js'
|
|
import { getStepGenerationPrompt } from '../prompts/step-generation.js'
|
|
import { getPointContentPrompt } from '../prompts/point-content.js'
|
|
import { getReactionPrompt } from '../prompts/reaction.js'
|
|
|
|
const client = new OpenAI({
|
|
baseURL: process.env.LLM_BASE_URL || 'https://api.deepseek.com',
|
|
apiKey: process.env.LLM_API_KEY || '',
|
|
})
|
|
|
|
const MODEL = process.env.LLM_MODEL || 'deepseek-chat'
|
|
|
|
class AIService {
|
|
async generateQuestPlan(params: {
|
|
city: string
|
|
days: number
|
|
companions: string
|
|
pace: string
|
|
userComment?: string
|
|
lang: string
|
|
}): Promise<QuestPlan> {
|
|
const systemPrompt = getSystemPrompt(params.lang)
|
|
|
|
const userPrompt = `Create a quest plan for a traveler:
|
|
- City: ${params.city}
|
|
- Duration: ${params.days} day(s)
|
|
- Companions: ${params.companions}
|
|
- Pace: ${params.pace}
|
|
${params.userComment ? `- Special wishes: ${params.userComment}` : ''}
|
|
|
|
Return a JSON object with:
|
|
{
|
|
"title": "Creative quest title (in ${params.lang === 'ru' ? 'Russian' : 'English'})",
|
|
"description": "1-2 sentence quest description that sets the mood (in ${params.lang === 'ru' ? 'Russian' : 'English'})",
|
|
"dayThemes": ["Theme for day 1", "Theme for day 2", ...]
|
|
}
|
|
|
|
IMPORTANT: Return ONLY valid JSON, no markdown, no extra text.`
|
|
|
|
const response = await client.chat.completions.create({
|
|
model: MODEL,
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: userPrompt },
|
|
],
|
|
temperature: 0.8,
|
|
max_tokens: 1000,
|
|
})
|
|
|
|
const text = response.choices[0]?.message?.content?.trim() || '{}'
|
|
|
|
try {
|
|
// Try to extract JSON from potential markdown code blocks
|
|
const jsonMatch = text.match(/\{[\s\S]*\}/)
|
|
const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : text)
|
|
return {
|
|
title: parsed.title || `Adventure in ${params.city}`,
|
|
description: parsed.description || 'Your personal city quest awaits!',
|
|
dayThemes: parsed.dayThemes || Array(params.days).fill('Exploration'),
|
|
}
|
|
} catch {
|
|
console.error('Failed to parse quest plan:', text)
|
|
return {
|
|
title: params.lang === 'ru'
|
|
? `Приключение в городе ${params.city}`
|
|
: `Adventure in ${params.city}`,
|
|
description: params.lang === 'ru'
|
|
? 'Твой персональный городской квест ждёт!'
|
|
: 'Your personal city quest awaits!',
|
|
dayThemes: Array(params.days).fill(
|
|
params.lang === 'ru' ? 'Исследование' : 'Exploration'
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
async generatePoint(params: {
|
|
city: string
|
|
dayNumber: number
|
|
totalDays: number
|
|
dayTheme: string
|
|
pace: string
|
|
companions: string
|
|
userComment?: string
|
|
previousPoints: string[]
|
|
lang: string
|
|
}): Promise<GeneratedPoint> {
|
|
const systemPrompt = getSystemPrompt(params.lang)
|
|
const userPrompt = getStepGenerationPrompt(params)
|
|
|
|
const response = await client.chat.completions.create({
|
|
model: MODEL,
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: userPrompt },
|
|
],
|
|
temperature: 0.8,
|
|
max_tokens: 500,
|
|
})
|
|
|
|
const text = response.choices[0]?.message?.content?.trim() || '{}'
|
|
|
|
try {
|
|
const jsonMatch = text.match(/\{[\s\S]*\}/)
|
|
const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : text)
|
|
return {
|
|
title: parsed.title || 'Unknown Location',
|
|
teaserText: parsed.teaserText || 'A mysterious place awaits...',
|
|
locationLat: parsed.locationLat || 0,
|
|
locationLon: parsed.locationLon || 0,
|
|
isLastPointOfDay: parsed.isLastPointOfDay || false,
|
|
}
|
|
} catch {
|
|
console.error('Failed to parse generated point:', text)
|
|
return {
|
|
title: 'Mystery Spot',
|
|
teaserText: params.lang === 'ru'
|
|
? 'Загадочное место ждёт тебя...'
|
|
: 'A mysterious place awaits...',
|
|
locationLat: 0,
|
|
locationLon: 0,
|
|
isLastPointOfDay: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
async generatePointContent(params: {
|
|
city: string
|
|
pointTitle: string
|
|
dayNumber: number
|
|
companions: string
|
|
pace: string
|
|
lang: string
|
|
}): Promise<PointContent> {
|
|
const systemPrompt = getSystemPrompt(params.lang)
|
|
const userPrompt = getPointContentPrompt(params)
|
|
|
|
const response = await client.chat.completions.create({
|
|
model: MODEL,
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: userPrompt },
|
|
],
|
|
temperature: 0.7,
|
|
max_tokens: 2000,
|
|
})
|
|
|
|
const text = response.choices[0]?.message?.content?.trim() || ''
|
|
return { contentText: text }
|
|
}
|
|
|
|
async generateReaction(params: {
|
|
eventType: string
|
|
pointTitle?: string
|
|
city: string
|
|
lang: string
|
|
companions: string
|
|
pace: string
|
|
}): Promise<string> {
|
|
const systemPrompt = getSystemPrompt(params.lang)
|
|
const userPrompt = getReactionPrompt(params)
|
|
|
|
const response = await client.chat.completions.create({
|
|
model: MODEL,
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: userPrompt },
|
|
],
|
|
temperature: 0.8,
|
|
max_tokens: 300,
|
|
})
|
|
|
|
return response.choices[0]?.message?.content?.trim() || ''
|
|
}
|
|
}
|
|
|
|
export const aiService = new AIService()
|