Add business profile AI fill endpoint for abstract and description.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
aaf7a83742
commit
d68ef2d08a
@@ -0,0 +1,99 @@
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Body, Controller, Get, Param, Patch, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { RequireBusinessPermission } from '../auth/decorators/require-business-permission.decorator';
|
||||
import { BusinessPermissionGuard } from '../auth/guards/business-permission.guard';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { BusinessProfileAiService } from './business-profile-ai.service';
|
||||
import { BusinessProfileService } from './business-profile.service';
|
||||
import { FillBusinessProfileFieldByAiDto } from './dto/business-profile-ai.dto';
|
||||
import { UpdateBusinessProfileDto } from './dto/update-business-profile.dto';
|
||||
|
||||
@Controller('businesses/:businessId/profile')
|
||||
@UseGuards(JwtAuthGuard, BusinessPermissionGuard)
|
||||
export class BusinessProfileController {
|
||||
constructor(private readonly service: BusinessProfileService) {}
|
||||
constructor(
|
||||
private readonly service: BusinessProfileService,
|
||||
private readonly aiService: BusinessProfileAiService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@RequireBusinessPermission('business.read')
|
||||
@@ -27,4 +32,14 @@ export class BusinessProfileController {
|
||||
) {
|
||||
return this.service.update(businessId, dto, user);
|
||||
}
|
||||
|
||||
@Post('ai-fill')
|
||||
@RequireBusinessPermission('business.update')
|
||||
fillFieldByAi(
|
||||
@Param('businessId') businessId: string,
|
||||
@Body() dto: FillBusinessProfileFieldByAiDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.aiService.fillField(businessId, dto, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { BusinessProfileAiService } from './business-profile-ai.service';
|
||||
import { BusinessProfileController } from './business-profile.controller';
|
||||
import { BusinessProfileService } from './business-profile.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
controllers: [BusinessProfileController],
|
||||
providers: [BusinessProfileService],
|
||||
providers: [BusinessProfileService, BusinessProfileAiService],
|
||||
})
|
||||
export class BusinessProfileModule {}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IsEnum, IsOptional, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export enum BusinessProfileAiFillField {
|
||||
about = 'about',
|
||||
vision = 'vision',
|
||||
}
|
||||
|
||||
export class FillBusinessProfileFieldByAiDto {
|
||||
@IsEnum(BusinessProfileAiFillField)
|
||||
field!: BusinessProfileAiFillField;
|
||||
|
||||
@IsString()
|
||||
@MinLength(10)
|
||||
prompt!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nameEn?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nameFa?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
categories?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user