guidly/bot/services/ai.service.ts
laruevin d5ed7fdcf9 Initial commit: Guidly project with CI/CD pipeline
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>
2026-02-11 11:42:42 +07:00

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()