target fix
This commit is contained in:
@@ -0,0 +1,120 @@
|
|||||||
|
package kz.konturai.parser.controller;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import kz.konturai.parser.dto.targeting.AdCreativeDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.CreativeRequest;
|
||||||
|
import kz.konturai.parser.dto.targeting.MarketingAnalysisDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.MarketingStrategyDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.StrategyRequest;
|
||||||
|
import kz.konturai.parser.dto.targeting.TargetingLaunchResultDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.TopicRequest;
|
||||||
|
import kz.konturai.parser.service.AiTargetingSystemService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless AI Targeting System API.
|
||||||
|
*
|
||||||
|
* Flow: POST /analysis → POST /strategy → POST /creative
|
||||||
|
*
|
||||||
|
* No database involved — each endpoint takes the previous step's output
|
||||||
|
* as part of the request body and enriches it with AI-generated content.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/targeting")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AiTargetingSystemController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AiTargetingSystemController.class);
|
||||||
|
|
||||||
|
private final AiTargetingSystemService aiTargetingSystemService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STEP 1 — Marketing Analysis.
|
||||||
|
*
|
||||||
|
* Accepts a business topic and returns a structured marketing analysis
|
||||||
|
* (target audience, platforms, budget, key messages, etc.).
|
||||||
|
*
|
||||||
|
* POST /api/targeting/analysis
|
||||||
|
* Body: { "topic": "детский сад в Алматы" }
|
||||||
|
*/
|
||||||
|
@PostMapping("/analysis")
|
||||||
|
public ResponseEntity<MarketingAnalysisDto> generateAnalysis(
|
||||||
|
@RequestBody @Valid TopicRequest request) {
|
||||||
|
|
||||||
|
logger.info("POST /api/targeting/analysis — topic: {}", request.getTopic());
|
||||||
|
MarketingAnalysisDto result = aiTargetingSystemService.generateAnalysis(request.getTopic());
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STEP 2 — Marketing Strategy.
|
||||||
|
*
|
||||||
|
* Accepts the full analysis DTO and returns a 3-phase 30-day campaign strategy
|
||||||
|
* with ad creatives brief and expected KPIs.
|
||||||
|
*
|
||||||
|
* POST /api/targeting/strategy
|
||||||
|
* Body: { "analysis": { ...MarketingAnalysisDto... } }
|
||||||
|
*/
|
||||||
|
@PostMapping("/strategy")
|
||||||
|
public ResponseEntity<MarketingStrategyDto> generateStrategy(
|
||||||
|
@RequestBody @Valid StrategyRequest request) {
|
||||||
|
|
||||||
|
logger.info("POST /api/targeting/strategy — topic: {}",
|
||||||
|
request.getAnalysis().getTopic());
|
||||||
|
MarketingStrategyDto result = aiTargetingSystemService.generateStrategy(request.getAnalysis());
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STEP 3 — Ad Creative.
|
||||||
|
*
|
||||||
|
* Accepts a creative brief (captionIdea + full analysis + full strategy)
|
||||||
|
* and returns a ready-to-launch ad post: headline, caption, hashtags,
|
||||||
|
* CTA button, and targeting settings.
|
||||||
|
*
|
||||||
|
* POST /api/targeting/creative
|
||||||
|
* Body: {
|
||||||
|
* "captionIdea": "string",
|
||||||
|
* "analysis": { ...MarketingAnalysisDto... },
|
||||||
|
* "strategy": { ...MarketingStrategyDto... }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@PostMapping("/creative")
|
||||||
|
public ResponseEntity<AdCreativeDto> generateCreative(
|
||||||
|
@RequestBody @Valid CreativeRequest request) {
|
||||||
|
|
||||||
|
logger.info("POST /api/targeting/creative — captionIdea: {}", request.getCaptionIdea());
|
||||||
|
AdCreativeDto result = aiTargetingSystemService.generateCreative(
|
||||||
|
request.getCaptionIdea(),
|
||||||
|
request.getAnalysis(),
|
||||||
|
request.getStrategy());
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STEP V3 — Launch Targeting from existing V3 Strategy.
|
||||||
|
*
|
||||||
|
* Берёт готовую V3-стратегию из MongoDB, генерирует настройки таргетинга
|
||||||
|
* через AI на основе V3-анализа и публикует первый пост из postCalendar в Facebook.
|
||||||
|
*
|
||||||
|
* Требования: стратегия должна иметь статус "completed" и непустой postCalendar.
|
||||||
|
*
|
||||||
|
* POST /api/targeting/launch/{strategyId}
|
||||||
|
*/
|
||||||
|
@PostMapping("/launch/{strategyId}")
|
||||||
|
public ResponseEntity<TargetingLaunchResultDto> launchFromStrategy(
|
||||||
|
@PathVariable String strategyId) {
|
||||||
|
|
||||||
|
logger.info("POST /api/targeting/launch/{} — launching targeting from V3 strategy", strategyId);
|
||||||
|
TargetingLaunchResultDto result = aiTargetingSystemService.launchTargetingFromStrategy(strategyId);
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package kz.konturai.parser.controller;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalStateException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleIllegalState(IllegalStateException e) {
|
||||||
|
logger.error("AI processing error: {}", e.getMessage());
|
||||||
|
return ResponseEntity.status(500).body(errorBody("AI_PROCESSING_ERROR", e.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleIllegalArgument(IllegalArgumentException e) {
|
||||||
|
logger.warn("Invalid argument: {}", e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(errorBody("INVALID_ARGUMENT", e.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException e) {
|
||||||
|
Map<String, String> fieldErrors = e.getBindingResult().getFieldErrors()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
FieldError::getField,
|
||||||
|
fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Некорректное значение",
|
||||||
|
(existing, duplicate) -> existing
|
||||||
|
));
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("error", "VALIDATION_ERROR");
|
||||||
|
body.put("fields", fieldErrors);
|
||||||
|
return ResponseEntity.badRequest().body(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleGeneral(Exception e) {
|
||||||
|
logger.error("Unexpected error", e);
|
||||||
|
return ResponseEntity.status(500).body(errorBody("INTERNAL_ERROR", e.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> errorBody(String code, String message) {
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("error", code);
|
||||||
|
body.put("message", message);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class AdCreativeDto {
|
||||||
|
|
||||||
|
private String headline;
|
||||||
|
private String caption;
|
||||||
|
private List<String> hashtags;
|
||||||
|
private String ctaButton;
|
||||||
|
private TargetingSettingsDto targetingSettings;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class AdCreativeNeedDto {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
private String type;
|
||||||
|
private String theme;
|
||||||
|
private String captionIdea;
|
||||||
|
private String imageDescription;
|
||||||
|
private String cta;
|
||||||
|
private String targetSegment;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class BudgetRecommendationDto {
|
||||||
|
|
||||||
|
private String minDaily;
|
||||||
|
private String optimalDaily;
|
||||||
|
private String monthly;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreativeRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "Идея подписи не может быть пустой")
|
||||||
|
private String captionIdea;
|
||||||
|
|
||||||
|
@NotNull(message = "Данные анализа обязательны")
|
||||||
|
@Valid
|
||||||
|
private MarketingAnalysisDto analysis;
|
||||||
|
|
||||||
|
@NotNull(message = "Данные стратегии обязательны")
|
||||||
|
@Valid
|
||||||
|
private MarketingStrategyDto strategy;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class ExpectedResultsDto {
|
||||||
|
|
||||||
|
private String reach;
|
||||||
|
private String clicks;
|
||||||
|
private String conversions;
|
||||||
|
private String cpl;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class MarketingAnalysisDto {
|
||||||
|
|
||||||
|
private String topic;
|
||||||
|
private String marketOverview;
|
||||||
|
private TargetAudienceDto targetAudience;
|
||||||
|
private List<PlatformDto> platforms;
|
||||||
|
private List<String> bestAdFormats;
|
||||||
|
private List<String> postingTimes;
|
||||||
|
private BudgetRecommendationDto budgetRecommendation;
|
||||||
|
private List<String> keyMessages;
|
||||||
|
private String competitorsInsight;
|
||||||
|
private String recommendedVisuals;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class MarketingStrategyDto {
|
||||||
|
|
||||||
|
private String strategyName;
|
||||||
|
private String goal;
|
||||||
|
private String duration;
|
||||||
|
private List<PhaseDto> phases;
|
||||||
|
private List<AdCreativeNeedDto> adCreativesNeeded;
|
||||||
|
private String totalBudget;
|
||||||
|
private ExpectedResultsDto expectedResults;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class PhaseDto {
|
||||||
|
|
||||||
|
private int phase;
|
||||||
|
private String name;
|
||||||
|
private String duration;
|
||||||
|
private String objective;
|
||||||
|
private List<String> tactics;
|
||||||
|
private String kpi;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class PlatformDto {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String priority;
|
||||||
|
private String reason;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class StrategyRequest {
|
||||||
|
|
||||||
|
@NotNull(message = "Данные анализа обязательны")
|
||||||
|
@Valid
|
||||||
|
private MarketingAnalysisDto analysis;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class TargetAudienceDto {
|
||||||
|
|
||||||
|
private String primary;
|
||||||
|
private String secondary;
|
||||||
|
private String ageRange;
|
||||||
|
private String gender;
|
||||||
|
private String income;
|
||||||
|
private List<String> interests;
|
||||||
|
private List<String> painPoints;
|
||||||
|
private List<String> motivations;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TargetingLaunchResultDto {
|
||||||
|
|
||||||
|
// ── Источник ──────────────────────────────────────────────────────────────
|
||||||
|
private String strategyId;
|
||||||
|
private String analysisId;
|
||||||
|
private String scoringModelName;
|
||||||
|
private String scoringModelTitle;
|
||||||
|
|
||||||
|
// ── Первый опубликованный пост ────────────────────────────────────────────
|
||||||
|
private int postIndex;
|
||||||
|
private String platform;
|
||||||
|
private String contentType; // "фото" / "видео"
|
||||||
|
private String theme;
|
||||||
|
private String postText;
|
||||||
|
private List<String> hashtags;
|
||||||
|
private String imageUrl;
|
||||||
|
private boolean imagePosted;
|
||||||
|
|
||||||
|
// ── Facebook ──────────────────────────────────────────────────────────────
|
||||||
|
private String facebookPostId;
|
||||||
|
private String facebookStatus; // "PUBLISHED" | "FAILED" | "TEXT_ONLY"
|
||||||
|
|
||||||
|
// ── Таргетинг (генерируется AI из V3 анализа) ────────────────────────────
|
||||||
|
private TargetingSettingsDto targetingSettings;
|
||||||
|
|
||||||
|
// ── Сводка ───────────────────────────────────────────────────────────────
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class TargetingSettingsDto {
|
||||||
|
|
||||||
|
private String audience;
|
||||||
|
private String age;
|
||||||
|
private List<String> interests;
|
||||||
|
private String placement;
|
||||||
|
private String budgetPerDay;
|
||||||
|
private String estimatedReach;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TopicRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "Тема не может быть пустой")
|
||||||
|
@Size(min = 3, max = 300, message = "Тема должна содержать от 3 до 300 символов")
|
||||||
|
private String topic;
|
||||||
|
}
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
package kz.konturai.parser.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
||||||
|
import kz.konturai.parser.dto.MarketingAnalysisV3Result;
|
||||||
|
import kz.konturai.parser.dto.targeting.AdCreativeDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.MarketingAnalysisDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.MarketingStrategyDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.TargetingLaunchResultDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.TargetingSettingsDto;
|
||||||
|
import kz.konturai.parser.model.MarketingAnalysisV3Document;
|
||||||
|
import kz.konturai.parser.model.MarketingStrategy;
|
||||||
|
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
|
||||||
|
import kz.konturai.parser.repository.MarketingStrategyRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Targeting System Service.
|
||||||
|
*
|
||||||
|
* Два режима работы:
|
||||||
|
* 1. STATELESS — generateAnalysis / generateStrategy / generateCreative
|
||||||
|
* Для React-приложения: принимает тему, возвращает структуры без БД.
|
||||||
|
*
|
||||||
|
* 2. V3-LINKED — launchTargetingFromStrategy(strategyId)
|
||||||
|
* Для полного цикла: берёт готовую V3-стратегию из MongoDB,
|
||||||
|
* генерирует настройки таргетинга из V3-анализа через AI
|
||||||
|
* и сразу публикует первый пост в Facebook.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AiTargetingSystemService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AiTargetingSystemService.class);
|
||||||
|
|
||||||
|
// ── Существующие сервисы ──────────────────────────────────────────────────
|
||||||
|
private final OpenAIAnalyticsService openAIAnalyticsService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final MarketingStrategyRepository strategyRepository;
|
||||||
|
private final MarketingAnalysisV3Repository analysisV3Repository;
|
||||||
|
private final FacebookPostingService facebookPostingService;
|
||||||
|
private final MinIOService minIOService;
|
||||||
|
|
||||||
|
// ── Конфиг моделей (из application.properties targeting.ai.*) ────────────
|
||||||
|
@Value("${targeting.ai.audience-model:gpt-4o}")
|
||||||
|
private String analysisModel;
|
||||||
|
|
||||||
|
@Value("${targeting.ai.creatives-model:gpt-4o}")
|
||||||
|
private String strategyModel;
|
||||||
|
|
||||||
|
@Value("${targeting.ai.creatives-model:gpt-4o}")
|
||||||
|
private String creativeModel;
|
||||||
|
|
||||||
|
@Value("${targeting.ai.max-audience-tokens:2500}")
|
||||||
|
private int analysisMaxTokens;
|
||||||
|
|
||||||
|
@Value("${targeting.ai.max-creatives-tokens:2000}")
|
||||||
|
private int strategyMaxTokens;
|
||||||
|
|
||||||
|
@Value("${targeting.ai.max-creatives-tokens:2000}")
|
||||||
|
private int creativeMaxTokens;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// V3-LINKED: Запуск таргетинга из существующей V3-стратегии
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает таргетинг на основе готовой V3-стратегии:
|
||||||
|
* 1. Загружает {@link MarketingStrategy} по strategyId из MongoDB.
|
||||||
|
* 2. Загружает связанный {@link MarketingAnalysisV3Document}.
|
||||||
|
* 3. Берёт первый пост из postCalendar.
|
||||||
|
* 4. Генерирует настройки таргетинга через AI на основе V3-анализа.
|
||||||
|
* 5. Публикует пост в Facebook (с изображением если есть).
|
||||||
|
*
|
||||||
|
* @param strategyId ID стратегии из коллекции marketing_strategy
|
||||||
|
* @return результат запуска с Facebook postId и настройками таргетинга
|
||||||
|
*/
|
||||||
|
public TargetingLaunchResultDto launchTargetingFromStrategy(String strategyId) {
|
||||||
|
logger.info("Launching targeting from V3 strategy: {}", strategyId);
|
||||||
|
|
||||||
|
// 1. Загружаем стратегию
|
||||||
|
MarketingStrategy strategy = strategyRepository.findById(strategyId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException(
|
||||||
|
"Стратегия не найдена: " + strategyId));
|
||||||
|
|
||||||
|
if (!"completed".equalsIgnoreCase(strategy.getStatus())) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Стратегия ещё не готова. Текущий статус: " + strategy.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarketingStrategy.PostCalendarItem> calendar = strategy.getPostCalendar();
|
||||||
|
if (calendar == null || calendar.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Календарь постов пуст для стратегии: " + strategyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Загружаем анализ
|
||||||
|
MarketingAnalysisV3Document analysis = analysisV3Repository
|
||||||
|
.findById(strategy.getAnalysisId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException(
|
||||||
|
"Анализ не найден: " + strategy.getAnalysisId()));
|
||||||
|
|
||||||
|
// 3. Берём первый пост из календаря
|
||||||
|
MarketingStrategy.PostCalendarItem firstPost = calendar.get(0);
|
||||||
|
logger.info("First post: platform={}, theme={}", firstPost.getPlatform(), firstPost.getTheme());
|
||||||
|
|
||||||
|
// 4. Генерируем настройки таргетинга через AI
|
||||||
|
TargetingSettingsDto targetingSettings = generateTargetingSettingsFromV3(analysis);
|
||||||
|
|
||||||
|
// 5. Публикуем в Facebook
|
||||||
|
String facebookPostId = null;
|
||||||
|
boolean imagePosted = false;
|
||||||
|
String facebookStatus;
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] imageBytes = downloadPostImage(firstPost.getImageFilename());
|
||||||
|
|
||||||
|
if (imageBytes != null && imageBytes.length > 0) {
|
||||||
|
facebookPostId = facebookPostingService.postToPageWithImage(
|
||||||
|
null,
|
||||||
|
firstPost.getPostText(),
|
||||||
|
firstPost.getHashtags(),
|
||||||
|
imageBytes);
|
||||||
|
imagePosted = true;
|
||||||
|
facebookStatus = "PUBLISHED";
|
||||||
|
logger.info("Published to Facebook WITH image. Post ID: {}", facebookPostId);
|
||||||
|
} else {
|
||||||
|
facebookPostId = facebookPostingService.postToPage(
|
||||||
|
null,
|
||||||
|
firstPost.getPostText(),
|
||||||
|
firstPost.getHashtags());
|
||||||
|
facebookStatus = "TEXT_ONLY";
|
||||||
|
logger.info("Published to Facebook TEXT ONLY. Post ID: {}", facebookPostId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Facebook posting failed for strategy {}: {}", strategyId, e.getMessage(), e);
|
||||||
|
facebookStatus = "FAILED";
|
||||||
|
}
|
||||||
|
|
||||||
|
return TargetingLaunchResultDto.builder()
|
||||||
|
.strategyId(strategyId)
|
||||||
|
.analysisId(strategy.getAnalysisId())
|
||||||
|
.scoringModelName(strategy.getScoringModelName())
|
||||||
|
.scoringModelTitle(strategy.getScoringModelTitle())
|
||||||
|
.postIndex(0)
|
||||||
|
.platform(firstPost.getPlatform())
|
||||||
|
.contentType(firstPost.getContentType())
|
||||||
|
.theme(firstPost.getTheme())
|
||||||
|
.postText(firstPost.getPostText())
|
||||||
|
.hashtags(firstPost.getHashtags())
|
||||||
|
.imageUrl(firstPost.getImageUrl())
|
||||||
|
.imagePosted(imagePosted)
|
||||||
|
.facebookPostId(facebookPostId)
|
||||||
|
.facebookStatus(facebookStatus)
|
||||||
|
.targetingSettings(targetingSettings)
|
||||||
|
.message(buildLaunchMessage(facebookStatus, facebookPostId))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерирует настройки таргетинга через AI из V3-анализа.
|
||||||
|
* Использует requestData (ниша, города, поведение) + resultData (CII, конкуренты)
|
||||||
|
* чтобы сформировать рекомендации по аудитории, бюджету и размещению.
|
||||||
|
*/
|
||||||
|
private TargetingSettingsDto generateTargetingSettingsFromV3(MarketingAnalysisV3Document analysis) {
|
||||||
|
MarketingAnalysisV3Request req = analysis.getRequestData();
|
||||||
|
MarketingAnalysisV3Result res = analysis.getResultData();
|
||||||
|
|
||||||
|
String systemPrompt = """
|
||||||
|
Ты — эксперт по платному трафику и таргетированной рекламе на рынке Казахстана.
|
||||||
|
На основе данных маркетингового анализа сформируй оптимальные настройки таргетинга.
|
||||||
|
|
||||||
|
Возвращай ТОЛЬКО валидный JSON без markdown, без пояснений, без блоков кода.
|
||||||
|
Чистый JSON-объект и ничего более. Весь текст — на русском языке.
|
||||||
|
""";
|
||||||
|
|
||||||
|
String instruction = String.format("""
|
||||||
|
Сформируй настройки таргетинга для Facebook/Instagram рекламы и верни строго следующий JSON:
|
||||||
|
{
|
||||||
|
"audience": "однострочное описание целевой аудитории",
|
||||||
|
"age": "возрастной диапазон, например 25-44",
|
||||||
|
"interests": ["интерес1", "интерес2", "интерес3", "интерес4"],
|
||||||
|
"placement": "Instagram Лента + Facebook Лента",
|
||||||
|
"budgetPerDay": "рекомендуемый дневной бюджет в KZT",
|
||||||
|
"estimatedReach": "оценочный охват в день, например 8 000 - 20 000 человек/день"
|
||||||
|
}
|
||||||
|
|
||||||
|
Данные для анализа:
|
||||||
|
Ниша: %s
|
||||||
|
Продукт: %s
|
||||||
|
Цель: %s
|
||||||
|
Города: %s
|
||||||
|
Тип клиентов: %s
|
||||||
|
Средний чек: %s
|
||||||
|
Статус SMM: %s
|
||||||
|
""",
|
||||||
|
safeGet(req != null ? req.getBusinessNiche() : null, "Не указано"),
|
||||||
|
safeGet(req != null ? req.getProductName() : null, "Не указано"),
|
||||||
|
safeGet(req != null ? req.getGoal() : null, "Не указано"),
|
||||||
|
formatList(req != null ? req.getPromotionCities() : null),
|
||||||
|
safeGet(req != null && req.getClientTarget() != null
|
||||||
|
? req.getClientTarget().name() : null, "Не указано"),
|
||||||
|
safeGet(req != null && req.getAverageCheck() != null
|
||||||
|
? req.getAverageCheck().name() : null, "Не указано"),
|
||||||
|
safeGet(req != null && req.getSmmStatus() != null
|
||||||
|
? req.getSmmStatus().name() : null, "Не указано")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Обогащаем контекст данными из resultData если доступны
|
||||||
|
String context = buildV3AnalysisContext(res);
|
||||||
|
|
||||||
|
String rawResponse = openAIAnalyticsService.generateWithInstructionWithModel(
|
||||||
|
context, instruction, "ru", creativeModel, systemPrompt, 800, 60000L);
|
||||||
|
|
||||||
|
if (rawResponse == null) {
|
||||||
|
logger.warn("AI did not return targeting settings, using defaults");
|
||||||
|
return buildDefaultTargetingSettings(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = extractJson(rawResponse);
|
||||||
|
return objectMapper.readValue(json, TargetingSettingsDto.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to parse targeting settings from AI, using defaults. Error: {}", e.getMessage());
|
||||||
|
return buildDefaultTargetingSettings(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Формирует читаемый контекст из resultData V3-анализа для AI-промпта.
|
||||||
|
*/
|
||||||
|
private String buildV3AnalysisContext(MarketingAnalysisV3Result res) {
|
||||||
|
if (res == null) return "";
|
||||||
|
|
||||||
|
StringBuilder ctx = new StringBuilder();
|
||||||
|
|
||||||
|
if (res.getExecutiveSummary() != null) {
|
||||||
|
MarketingAnalysisV3Result.ExecutiveSummary es = res.getExecutiveSummary();
|
||||||
|
ctx.append("Уровень конкуренции: ").append(safeGet(es.getCompetitionLevel(), "")).append("\n");
|
||||||
|
ctx.append("Тренд спроса: ").append(safeGet(es.getDemandTrend(), "")).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.getCompetitionIntensity() != null) {
|
||||||
|
ctx.append("Индекс конкурентности (CII): ")
|
||||||
|
.append(res.getCompetitionIntensity().getCiiIndex()).append("/100\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.getContentProfile() != null && res.getContentProfile().getTopHashtags() != null) {
|
||||||
|
ctx.append("Топ хештеги конкурентов: ")
|
||||||
|
.append(String.join(", ", res.getContentProfile().getTopHashtags())).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Дефолтные настройки таргетинга если AI недоступен.
|
||||||
|
*/
|
||||||
|
private TargetingSettingsDto buildDefaultTargetingSettings(MarketingAnalysisV3Request req) {
|
||||||
|
String city = req != null && req.getPromotionCities() != null && !req.getPromotionCities().isEmpty()
|
||||||
|
? req.getPromotionCities().get(0) : "Алматы";
|
||||||
|
String niche = req != null ? safeGet(req.getBusinessNiche(), "бизнес") : "бизнес";
|
||||||
|
|
||||||
|
return TargetingSettingsDto.builder()
|
||||||
|
.audience("Жители " + city + ", интересующиеся " + niche)
|
||||||
|
.age("25-44")
|
||||||
|
.interests(List.of(niche, "Услуги", "Казахстан", "Качество"))
|
||||||
|
.placement("Instagram Лента + Facebook Лента")
|
||||||
|
.budgetPerDay("5 000 - 15 000 KZT")
|
||||||
|
.estimatedReach("5 000 - 20 000 человек/день")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скачивает изображение из MinIO по имени файла.
|
||||||
|
* Возвращает null если файл не найден или имя пустое.
|
||||||
|
*/
|
||||||
|
private byte[] downloadPostImage(String imageFilename) {
|
||||||
|
if (imageFilename == null || imageFilename.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
InputStream stream = minIOService.downloadFile(imageFilename);
|
||||||
|
if (stream == null) return null;
|
||||||
|
byte[] bytes = stream.readAllBytes();
|
||||||
|
stream.close();
|
||||||
|
logger.info("Downloaded image from MinIO: {} ({} bytes)", imageFilename, bytes.length);
|
||||||
|
return bytes;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Could not download image '{}' from MinIO: {}", imageFilename, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildLaunchMessage(String facebookStatus, String postId) {
|
||||||
|
return switch (facebookStatus) {
|
||||||
|
case "PUBLISHED" -> "✅ Пост с изображением успешно опубликован в Facebook. ID: " + postId;
|
||||||
|
case "TEXT_ONLY" -> "✅ Текстовый пост опубликован в Facebook. ID: " + postId;
|
||||||
|
case "FAILED" -> "⚠️ Пост не удалось опубликовать в Facebook. Настройки таргетинга сохранены.";
|
||||||
|
default -> "Запуск таргетинга выполнен.";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// STATELESS: Независимый режим для React-приложения
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public MarketingAnalysisDto generateAnalysis(String topic) {
|
||||||
|
logger.info("Generating marketing analysis for topic: {}", topic);
|
||||||
|
|
||||||
|
String systemPrompt = """
|
||||||
|
Ты — профессиональный маркетинговый аналитик с 15-летним опытом работы
|
||||||
|
на рынках СНГ (Казахстан, Россия, Узбекистан). Специализируешься на digital-рекламе.
|
||||||
|
|
||||||
|
Возвращай ТОЛЬКО валидный JSON без markdown, без пояснений, без блоков кода.
|
||||||
|
Чистый JSON-объект и ничего более. Весь текст внутри JSON — на русском языке.
|
||||||
|
""";
|
||||||
|
|
||||||
|
String instruction = """
|
||||||
|
Проанализируй данный бизнес для создания рекламной кампании и верни строго следующий JSON:
|
||||||
|
{
|
||||||
|
"topic": "string",
|
||||||
|
"marketOverview": "2-3 предложения о рынке на русском",
|
||||||
|
"targetAudience": {
|
||||||
|
"primary": "описание основной аудитории",
|
||||||
|
"secondary": "описание вторичной аудитории",
|
||||||
|
"ageRange": "например 25-45",
|
||||||
|
"gender": "например Женщины 70%, Мужчины 30%",
|
||||||
|
"income": "например Средний и выше среднего",
|
||||||
|
"interests": ["интерес1", "интерес2", "интерес3", "интерес4", "интерес5"],
|
||||||
|
"painPoints": ["боль1", "боль2", "боль3"],
|
||||||
|
"motivations": ["мотивация1", "мотивация2", "мотивация3"]
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{ "name": "Instagram", "priority": "Высокий", "reason": "почему на русском" },
|
||||||
|
{ "name": "Facebook", "priority": "Средний", "reason": "почему" },
|
||||||
|
{ "name": "TikTok", "priority": "Низкий", "reason": "почему" }
|
||||||
|
],
|
||||||
|
"bestAdFormats": ["Stories", "Reels", "Публикация", "Карусель"],
|
||||||
|
"postingTimes": ["Пн-Пт 18:00-21:00", "Сб-Вс 10:00-13:00"],
|
||||||
|
"budgetRecommendation": {
|
||||||
|
"minDaily": "5 000 KZT",
|
||||||
|
"optimalDaily": "15 000 KZT",
|
||||||
|
"monthly": "300 000 - 450 000 KZT"
|
||||||
|
},
|
||||||
|
"keyMessages": ["сообщение1", "сообщение2", "сообщение3"],
|
||||||
|
"competitorsInsight": "краткий анализ конкурентов на русском",
|
||||||
|
"recommendedVisuals": "подробное описание лучших визуалов на русском"
|
||||||
|
}
|
||||||
|
|
||||||
|
Бизнес для анализа:
|
||||||
|
""";
|
||||||
|
|
||||||
|
String rawResponse = openAIAnalyticsService.generateWithInstructionWithModel(
|
||||||
|
topic, instruction, "ru", analysisModel, systemPrompt, analysisMaxTokens, 120000L);
|
||||||
|
|
||||||
|
return parseResponse(rawResponse, MarketingAnalysisDto.class, "Marketing Analysis");
|
||||||
|
}
|
||||||
|
|
||||||
|
public MarketingStrategyDto generateStrategy(MarketingAnalysisDto analysis) {
|
||||||
|
logger.info("Generating marketing strategy for topic: {}", analysis.getTopic());
|
||||||
|
|
||||||
|
String systemPrompt = """
|
||||||
|
Ты — старший маркетинговый стратег, специалист по performance-маркетингу
|
||||||
|
на рынках СНГ (Казахстан). Создаёшь эффективные рекламные стратегии.
|
||||||
|
|
||||||
|
Возвращай ТОЛЬКО валидный JSON без markdown, без пояснений, без блоков кода.
|
||||||
|
Чистый JSON-объект и ничего более. Весь текст внутри JSON — на русском языке.
|
||||||
|
""";
|
||||||
|
|
||||||
|
String instruction = """
|
||||||
|
На основе предоставленного маркетингового анализа создай полную 30-дневную
|
||||||
|
рекламную стратегию и верни строго следующий JSON:
|
||||||
|
{
|
||||||
|
"strategyName": "название стратегии на русском",
|
||||||
|
"goal": "главная цель кампании на русском",
|
||||||
|
"duration": "30 дней",
|
||||||
|
"phases": [
|
||||||
|
{
|
||||||
|
"phase": 1,
|
||||||
|
"name": "Осведомлённость",
|
||||||
|
"duration": "10 дней",
|
||||||
|
"objective": "что достигаем",
|
||||||
|
"tactics": ["тактика1", "тактика2", "тактика3"],
|
||||||
|
"kpi": "ожидаемый KPI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": 2,
|
||||||
|
"name": "Вовлечённость",
|
||||||
|
"duration": "10 дней",
|
||||||
|
"objective": "...",
|
||||||
|
"tactics": ["..."],
|
||||||
|
"kpi": "..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": 3,
|
||||||
|
"name": "Конверсия",
|
||||||
|
"duration": "10 дней",
|
||||||
|
"objective": "...",
|
||||||
|
"tactics": ["..."],
|
||||||
|
"kpi": "..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"adCreativesNeeded": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "Публикация в ленте",
|
||||||
|
"theme": "тема поста",
|
||||||
|
"captionIdea": "идея подписи",
|
||||||
|
"imageDescription": "детальное описание изображения",
|
||||||
|
"cta": "Записаться →",
|
||||||
|
"targetSegment": "сегмент аудитории"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "Сторис",
|
||||||
|
"theme": "...",
|
||||||
|
"captionIdea": "...",
|
||||||
|
"imageDescription": "...",
|
||||||
|
"cta": "...",
|
||||||
|
"targetSegment": "..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "Обложка Reels",
|
||||||
|
"theme": "...",
|
||||||
|
"captionIdea": "...",
|
||||||
|
"imageDescription": "...",
|
||||||
|
"cta": "...",
|
||||||
|
"targetSegment": "..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"totalBudget": "450 000 KZT",
|
||||||
|
"expectedResults": {
|
||||||
|
"reach": "50 000 - 100 000 человек",
|
||||||
|
"clicks": "2 000 - 5 000",
|
||||||
|
"conversions": "100 - 300 лидов",
|
||||||
|
"cpl": "1 500 - 3 000 KZT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Маркетинговый анализ:
|
||||||
|
""";
|
||||||
|
|
||||||
|
String rawResponse = openAIAnalyticsService.generateWithInstructionWithModel(
|
||||||
|
serializeToJson(analysis, "analysis"), instruction, "ru",
|
||||||
|
strategyModel, systemPrompt, strategyMaxTokens, 120000L);
|
||||||
|
|
||||||
|
return parseResponse(rawResponse, MarketingStrategyDto.class, "Marketing Strategy");
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdCreativeDto generateCreative(
|
||||||
|
String captionIdea,
|
||||||
|
MarketingAnalysisDto analysis,
|
||||||
|
MarketingStrategyDto strategy) {
|
||||||
|
|
||||||
|
logger.info("Generating ad creative for idea: {}", captionIdea);
|
||||||
|
|
||||||
|
String systemPrompt = """
|
||||||
|
Ты — профессиональный копирайтер для рекламы в социальных сетях.
|
||||||
|
Специализируешься на рынке Казахстана, пишешь эмоционально и убедительно.
|
||||||
|
Твои тексты всегда включают хук, выгоды, социальное доказательство и призыв к действию.
|
||||||
|
|
||||||
|
Возвращай ТОЛЬКО валидный JSON без markdown, без пояснений, без блоков кода.
|
||||||
|
Чистый JSON-объект и ничего более. Весь текст внутри JSON — на русском языке.
|
||||||
|
""";
|
||||||
|
|
||||||
|
String instruction = """
|
||||||
|
На основе предоставленных данных создай рекламный пост и верни строго следующий JSON:
|
||||||
|
{
|
||||||
|
"headline": "короткий цепляющий заголовок до 10 слов на русском",
|
||||||
|
"caption": "полный текст поста 3-5 предложений с эмоджи на русском",
|
||||||
|
"hashtags": ["#тег1", "#тег2", "#тег3", "#тег4", "#тег5", "#тег6"],
|
||||||
|
"ctaButton": "Записаться →",
|
||||||
|
"targetingSettings": {
|
||||||
|
"audience": "однострочное описание аудитории на русском",
|
||||||
|
"age": "возрастной диапазон из анализа",
|
||||||
|
"interests": ["интерес1", "интерес2", "интерес3", "интерес4"],
|
||||||
|
"placement": "Instagram Лента + Facebook Лента",
|
||||||
|
"budgetPerDay": "бюджет из анализа в KZT",
|
||||||
|
"estimatedReach": "например 5 000 - 15 000 человек/день"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Данные для создания поста:
|
||||||
|
""";
|
||||||
|
|
||||||
|
String contextJson = buildCreativeContext(captionIdea, analysis, strategy);
|
||||||
|
String rawResponse = openAIAnalyticsService.generateWithInstructionWithModel(
|
||||||
|
contextJson, instruction, "ru", creativeModel, systemPrompt, creativeMaxTokens, 90000L);
|
||||||
|
|
||||||
|
return parseResponse(rawResponse, AdCreativeDto.class, "Ad Creative");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// HELPERS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private String extractJson(String rawResponse) {
|
||||||
|
if (rawResponse == null || rawResponse.isBlank()) {
|
||||||
|
throw new IllegalStateException("Пустой ответ от AI");
|
||||||
|
}
|
||||||
|
String trimmed = rawResponse.trim();
|
||||||
|
if (trimmed.startsWith("```")) {
|
||||||
|
int firstNewline = trimmed.indexOf('\n');
|
||||||
|
int lastBacktick = trimmed.lastIndexOf("```");
|
||||||
|
if (firstNewline > 0 && lastBacktick > firstNewline) {
|
||||||
|
trimmed = trimmed.substring(firstNewline + 1, lastBacktick).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int start = trimmed.indexOf('{');
|
||||||
|
int end = trimmed.lastIndexOf('}');
|
||||||
|
if (start == -1 || end == -1 || end <= start) {
|
||||||
|
throw new IllegalStateException("JSON не найден в ответе AI: "
|
||||||
|
+ trimmed.substring(0, Math.min(200, trimmed.length())));
|
||||||
|
}
|
||||||
|
return trimmed.substring(start, end + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T parseResponse(String rawResponse, Class<T> type, String context) {
|
||||||
|
if (rawResponse == null) {
|
||||||
|
throw new IllegalStateException("AI не вернул ответ для: " + context);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(extractJson(rawResponse), type);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to parse {} response. Raw: {}", context, rawResponse, e);
|
||||||
|
throw new IllegalStateException("Ошибка парсинга " + context + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeToJson(Object obj, String context) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(obj);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Ошибка сериализации " + context + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildCreativeContext(String captionIdea, MarketingAnalysisDto analysis, MarketingStrategyDto strategy) {
|
||||||
|
try {
|
||||||
|
return objectMapper.createObjectNode()
|
||||||
|
.put("captionIdea", captionIdea)
|
||||||
|
.set("analysis", objectMapper.valueToTree(analysis))
|
||||||
|
.toString()
|
||||||
|
+ "\nСтратегия: " + objectMapper.writeValueAsString(strategy);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Идея: " + captionIdea + "\nАнализ: " + serializeToJson(analysis, "analysis");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeGet(String value, String fallback) {
|
||||||
|
return (value != null && !value.isBlank()) ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatList(List<String> list) {
|
||||||
|
if (list == null || list.isEmpty()) return "Не указано";
|
||||||
|
return String.join(", ", list);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user