Files
backend/src/business-profile/business-profile-ai.service.ts

100 lines
3.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { BadRequestException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AuthUser } from '../auth/auth.types';
import { normalizeAiDescriptionHtml } from '../common/ai-html.util';
import {
requestAiJsonCompletion,
resolveAiProvider,
} from '../common/ai-provider.util';
import {
BusinessProfileAiFillField,
FillBusinessProfileFieldByAiDto,
} from './dto/business-profile-ai.dto';
@Injectable()
export class BusinessProfileAiService {
constructor(private readonly config: ConfigService) {}
async fillField(
_businessIdRaw: string,
dto: FillBusinessProfileFieldByAiDto,
_actor: AuthUser,
) {
const prompt = dto.prompt?.trim();
if (!prompt || prompt.length < 10) {
throw new BadRequestException('Prompt is required');
}
const nameEn = dto.nameEn?.trim() || '';
const nameFa = dto.nameFa?.trim() || '';
if (!nameEn && !nameFa) {
throw new BadRequestException(
'Enter a business name before using AI fill',
);
}
const provider = resolveAiProvider(this.config);
const isAbout = dto.field === BusinessProfileAiFillField.about;
const systemPrompt = isAbout
? `You write short business abstracts for an Iranian B2B marketplace listing.
Return ONLY valid JSON: { "text": "..." }
Follow the user's instructions for language and tone.
Keep the abstract to 1–4 sentences. No HTML. No markdown.
Prefer a clear, professional, informative style about what the business does.
Never write sales CTAs, discounts, or vague praise.`
: `You write business profile descriptions as HTML for an advanced rich-text editor on an Iranian marketplace.
Return ONLY valid JSON: { "html": "..." }
The "html" value must be a raw HTML fragment (real tags, not escaped entities, not markdown, not code fences).
Allowed tags only: p, ul, ol, li, strong, em, br.
Do not wrap in html/body/div. Do not include scripts, styles, classes, or attributes.
Follow the user's instructions for language and tone.
Prefer a professional, informative structure about the company, offerings, and strengths.
Never write sales CTAs, discounts, or vague marketing slogans.
Aim for 2–5 short paragraphs and optionally a bullet list.`;
const userPrompt = JSON.stringify({
instructions: prompt,
business: {
nameEn: nameEn || null,
nameFa: nameFa || null,
categories: dto.categories?.trim() || null,
},
});
let content: string;
try {
content = await requestAiJsonCompletion(
provider,
systemPrompt,
userPrompt,
0.5,
);
} catch (err) {
const message = err instanceof Error ? err.message : 'AI request failed';
throw new BadRequestException(message);
}
let parsed: { text?: unknown; html?: unknown };
try {
parsed = JSON.parse(content) as { text?: unknown; html?: unknown };
} catch {
throw new BadRequestException('AI returned invalid JSON');
}
if (isAbout) {
const text = String(parsed.text ?? '').trim();
if (!text) {
throw new BadRequestException('AI returned an empty abstract');
}
return { field: BusinessProfileAiFillField.about, text };
}
const html = normalizeAiDescriptionHtml(String(parsed.html ?? ''));
if (!html) {
throw new BadRequestException('AI returned an empty description');
}
return { field: BusinessProfileAiFillField.vision, html };
}
}