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 { AuthUser } from '../auth/auth.types';
|
||||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
import { RequireBusinessPermission } from '../auth/decorators/require-business-permission.decorator';
|
import { RequireBusinessPermission } from '../auth/decorators/require-business-permission.decorator';
|
||||||
import { BusinessPermissionGuard } from '../auth/guards/business-permission.guard';
|
import { BusinessPermissionGuard } from '../auth/guards/business-permission.guard';
|
||||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||||
|
import { BusinessProfileAiService } from './business-profile-ai.service';
|
||||||
import { BusinessProfileService } from './business-profile.service';
|
import { BusinessProfileService } from './business-profile.service';
|
||||||
|
import { FillBusinessProfileFieldByAiDto } from './dto/business-profile-ai.dto';
|
||||||
import { UpdateBusinessProfileDto } from './dto/update-business-profile.dto';
|
import { UpdateBusinessProfileDto } from './dto/update-business-profile.dto';
|
||||||
|
|
||||||
@Controller('businesses/:businessId/profile')
|
@Controller('businesses/:businessId/profile')
|
||||||
@UseGuards(JwtAuthGuard, BusinessPermissionGuard)
|
@UseGuards(JwtAuthGuard, BusinessPermissionGuard)
|
||||||
export class BusinessProfileController {
|
export class BusinessProfileController {
|
||||||
constructor(private readonly service: BusinessProfileService) {}
|
constructor(
|
||||||
|
private readonly service: BusinessProfileService,
|
||||||
|
private readonly aiService: BusinessProfileAiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@RequireBusinessPermission('business.read')
|
@RequireBusinessPermission('business.read')
|
||||||
@@ -27,4 +32,14 @@ export class BusinessProfileController {
|
|||||||
) {
|
) {
|
||||||
return this.service.update(businessId, dto, user);
|
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 { Module } from '@nestjs/common';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
import { BusinessProfileAiService } from './business-profile-ai.service';
|
||||||
import { BusinessProfileController } from './business-profile.controller';
|
import { BusinessProfileController } from './business-profile.controller';
|
||||||
import { BusinessProfileService } from './business-profile.service';
|
import { BusinessProfileService } from './business-profile.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule],
|
imports: [AuthModule],
|
||||||
controllers: [BusinessProfileController],
|
controllers: [BusinessProfileController],
|
||||||
providers: [BusinessProfileService],
|
providers: [BusinessProfileService, BusinessProfileAiService],
|
||||||
})
|
})
|
||||||
export class BusinessProfileModule {}
|
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