diff --git a/app/integrations/llm/prompt.py b/app/integrations/llm/prompt.py index d4c3b88..9c00c23 100644 --- a/app/integrations/llm/prompt.py +++ b/app/integrations/llm/prompt.py @@ -1,7 +1,7 @@ import os from pydantic import BaseModel from common.utils import get_env -from integrations.llm.schemas.report import ReportInput, ReportOutput, YouTubeDiagnosisInput, YouTubeDiagnosisOutput +from integrations.llm.schemas.report import ReportInput, ReportOutput, YouTubeDiagnosisInput, YouTubeDiagnosisOutput, BrandConsistencyInput, BrandConsistencyOutput from integrations.llm.schemas.plan import PlanInput, PlanOutput from integrations.llm.schemas.market import ( MarketCompetitorsInput, MarketCompetitorsOutput, @@ -87,3 +87,10 @@ youtube_diagnosis_prompt = Prompt( input_class=YouTubeDiagnosisInput, output_class=YouTubeDiagnosisOutput, ) + +brand_consistency_prompt = Prompt( + file_name="brand_consistency_prompt.txt", + prompt_model="REPORT_MODEL", + input_class=BrandConsistencyInput, + output_class=BrandConsistencyOutput, +) diff --git a/app/integrations/llm/schemas/report.py b/app/integrations/llm/schemas/report.py index 9f24be3..5e6ee46 100644 --- a/app/integrations/llm/schemas/report.py +++ b/app/integrations/llm/schemas/report.py @@ -355,3 +355,18 @@ class YouTubeDiagnosisInput(BaseModel): class YouTubeDiagnosisOutput(BaseModel): diagnosis: list[DiagnosisItem] + + +# --- BrandConsistency --- + +class BrandConsistencyInput(BaseModel): + clinic_name: str | None = None + mainpage: str | None = None + instagram: str | None = None + facebook: str | None = None + youtube: str | None = None + gangnam_unni: str | None = None + + +class BrandConsistencyOutput(BaseModel): + brand_inconsistencies: list[BrandInconsistency] diff --git a/app/integrations/llm/temp-prompt/brand_consistency_prompt.txt b/app/integrations/llm/temp-prompt/brand_consistency_prompt.txt new file mode 100644 index 0000000..799dde4 --- /dev/null +++ b/app/integrations/llm/temp-prompt/brand_consistency_prompt.txt @@ -0,0 +1,18 @@ +다음은 성형외과/피부과 {clinic_name} 의 채널별 브랜드 데이터입니다. + +공식 홈페이지: {mainpage} +인스타그램: {instagram} +페이스북: {facebook} +유튜브: {youtube} +강남언니: {gangnam_unni} + +위 채널들 간의 브랜드 불일치 항목을 분석해줘. +비교 대상 필드 예시: 병원명(한글/영문), 전화번호, 주소, 로고, 슬로건, 소개 문구 등. + +각 항목은 다음 JSON 형식의 배열로 출력해줘: +- field: 불일치 필드명 +- values: 채널별 실제 값 목록 (channel, value, is_correct) +- impact: 불일치가 브랜드에 미치는 영향 +- recommendation: 개선 권고사항 + +출처 번호([1], [2] 등)는 포함하지 마. diff --git a/app/services/analysis.py b/app/services/analysis.py index 7fef7ba..1229047 100644 --- a/app/services/analysis.py +++ b/app/services/analysis.py @@ -8,10 +8,9 @@ from common.db.run import select_run, update_run_report, update_run_plan from common.db.source import select_run_raw_data, select_run_mainpage_url from common.db.market import select_market from integrations.llm.llm_service import LLMService -from integrations.llm.prompt import report_prompt, plan_prompt, youtube_diagnosis_prompt -from integrations.llm.schemas.report import ReportOutput, ClinicSnapshot, YouTubeAudit +from integrations.llm.prompt import report_prompt, plan_prompt, youtube_diagnosis_prompt, brand_consistency_prompt +from integrations.llm.schemas.report import ReportOutput, ClinicSnapshot, YouTubeAudit, BrandConsistencyOutput from integrations.llm.schemas.plan import PlanOutput -from models.status import AnalysisStatus logger = logging.getLogger(__name__) @@ -233,15 +232,23 @@ async def _build_overrides(analysis_run_id: str) -> dict: if instagram.get("username"): ig_patch["profile_link"] = f"https://www.instagram.com/{instagram['username']}/" # ── facebook ────────────────────────────────────────────────────────────── - fb_patch: dict = {} - if facebook.get("pageUrl"): fb_patch["url"] = facebook["pageUrl"] - if facebook.get("pageUrl"): fb_patch["link"] = facebook["pageUrl"] - if facebook.get("pageName"): fb_patch["page_name"] = facebook["pageName"] - if facebook.get("followers"): fb_patch["followers"] = facebook["followers"] - if facebook.get("intro"): fb_patch["bio"] = facebook["intro"] - if facebook.get("categories"): fb_patch["category"] = ", ".join(facebook["categories"]) - if facebook.get("website"): fb_patch["linked_domain"] = facebook["website"] + fb_pages: dict = {} + if facebook.get("pageUrl"): fb_pages["url"] = facebook["pageUrl"] + if facebook.get("pageUrl"): fb_pages["link"] = facebook["pageUrl"] + if facebook.get("pageName"): fb_pages["page_name"] = facebook["pageName"] + if facebook.get("followers"): fb_pages["followers"] = facebook["followers"] + if facebook.get("intro"): fb_pages["bio"] = facebook["intro"] + if facebook.get("categories"): fb_pages["category"] = ", ".join(facebook["categories"]) + if facebook.get("website"): fb_pages["linked_domain"] = facebook["website"] + brand = await generate_brand_consistency(analysis_run_id) + brand_patch = brand.model_dump()["brand_inconsistencies"] + + fb_patch: dict = {} + if fb_pages: + fb_patch["pages"] = [fb_pages] + if brand_patch: + fb_patch["brand_inconsistencies"] = brand_patch overrides: dict = {} if snapshot: @@ -249,7 +256,7 @@ async def _build_overrides(analysis_run_id: str) -> dict: if ig_patch: overrides["instagram_audit"] = {"accounts": [ig_patch]} if fb_patch: - overrides["facebook_audit"] = {"pages": [fb_patch]} + overrides["facebook_audit"] = fb_patch if yt_patch: overrides["youtube_audit"] = yt_patch return overrides @@ -268,8 +275,10 @@ def _deep_merge(base: dict, overrides: dict) -> dict: return base def _patch_report(result: ReportOutput, overrides: dict) -> ReportOutput: - merged = _deep_merge(result.model_dump(), overrides) - return ReportOutput(**merged) + base = result.model_dump() + for key in overrides: + base.pop(key, None) + return ReportOutput(**_deep_merge(base, overrides)) _MOCK_DOMAINS = {"viewclinic.com"} @@ -294,6 +303,24 @@ def _load_mock_plan() -> PlanOutput: return PlanOutput(**json.load(f)) +async def generate_brand_consistency(analysis_run_id: str) -> BrandConsistencyOutput: + raw = await select_run_raw_data(analysis_run_id) + + def _json(v) -> str | None: + return json.dumps(v, ensure_ascii=False) if v else None + + mainpage = raw.get("mainpage") or {} + input_data = { + "clinic_name": mainpage.get("clinicName"), + "mainpage": _json(mainpage), + "instagram": _json(raw.get("instagram")), + "facebook": _json(raw.get("facebook")), + "youtube": _json(raw.get("youtube")), + "gangnam_unni": _json(raw.get("gangnam_unni")), + } + return await LLMService(provider="perplexity").generate(brand_consistency_prompt, input_data) + + async def run_report_task(analysis_run_id: str) -> None: logger.info("[report] start run=%s", analysis_run_id) if await _is_mock(analysis_run_id): @@ -302,6 +329,7 @@ async def run_report_task(analysis_run_id: str) -> None: result.youtube_audit.linked_urls = [] else: result = await generate_report(analysis_run_id) + result = _patch_report(result, await _build_overrides(analysis_run_id)) await update_run_report(analysis_run_id, result.model_dump()) logger.info("[report] done run=%s", analysis_run_id)