Compare commits

...

4 Commits

7 changed files with 223 additions and 15 deletions

View File

@ -1,7 +1,14 @@
import os import os
from pydantic import BaseModel from pydantic import BaseModel
from common.utils import get_env from common.utils import get_env
from integrations.llm.schemas.report import ReportInput, ReportOutput, YouTubeDiagnosisInput, YouTubeDiagnosisOutput from integrations.llm.schemas.report import (
ReportInput, ReportOutput,
CriticalIssuesInput, CriticalIssuesOutput,
YouTubeDiagnosisInput, YouTubeDiagnosisOutput,
BrandConsistencyInput, BrandConsistencyOutput,
TransformationInput, TransformationProposal,
RoadmapInput, RoadmapOutput,
)
from integrations.llm.schemas.plan import PlanInput, PlanOutput from integrations.llm.schemas.plan import PlanInput, PlanOutput
from integrations.llm.schemas.market import ( from integrations.llm.schemas.market import (
MarketCompetitorsInput, MarketCompetitorsOutput, MarketCompetitorsInput, MarketCompetitorsOutput,
@ -87,3 +94,31 @@ youtube_diagnosis_prompt = Prompt(
input_class=YouTubeDiagnosisInput, input_class=YouTubeDiagnosisInput,
output_class=YouTubeDiagnosisOutput, output_class=YouTubeDiagnosisOutput,
) )
brand_consistency_prompt = Prompt(
file_name="brand_consistency_prompt.txt",
prompt_model="REPORT_MODEL",
input_class=BrandConsistencyInput,
output_class=BrandConsistencyOutput,
)
critical_issues_prompt = Prompt(
file_name="critical_issues_prompt.txt",
prompt_model="REPORT_MODEL",
input_class=CriticalIssuesInput,
output_class=CriticalIssuesOutput,
)
transformation_prompt = Prompt(
file_name="transformation_prompt.txt",
prompt_model="REPORT_MODEL",
input_class=TransformationInput,
output_class=TransformationProposal,
)
roadmap_prompt = Prompt(
file_name="roadmap_prompt.txt",
prompt_model="REPORT_MODEL",
input_class=RoadmapInput,
output_class=RoadmapOutput,
)

View File

@ -355,3 +355,47 @@ class YouTubeDiagnosisInput(BaseModel):
class YouTubeDiagnosisOutput(BaseModel): class YouTubeDiagnosisOutput(BaseModel):
diagnosis: list[DiagnosisItem] diagnosis: list[DiagnosisItem]
# --- Diagnosis ---
class CriticalIssuesInput(BaseModel):
clinic_name: str | None = None
data: str | None = None
class CriticalIssuesOutput(BaseModel):
diagnosis: list[DiagnosisItem]
# --- Roadmap ---
class RoadmapInput(BaseModel):
clinic_name: str | None = None
data: str | None = None
class RoadmapOutput(BaseModel):
roadmap: list[RoadmapMonth]
# --- Transformation ---
class TransformationInput(BaseModel):
clinic_name: str | None = None
data: str | None = None
# --- 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]

View File

@ -0,0 +1,18 @@
다음은 성형외과/피부과 {clinic_name} 의 채널별 브랜드 데이터입니다.
공식 홈페이지: {mainpage}
인스타그램: {instagram}
페이스북: {facebook}
유튜브: {youtube}
강남언니: {gangnam_unni}
위 채널들 간의 브랜드 불일치 항목을 분석해줘.
비교 대상 필드 예시: 병원명(한글/영문), 전화번호, 주소, 로고, 슬로건, 소개 문구 등.
각 항목은 다음 JSON 형식의 배열로 출력해줘:
- field: 불일치 필드명
- values: 채널별 실제 값 목록 (channel, value, is_correct)
- impact: 불일치가 브랜드에 미치는 영향
- recommendation: 개선 권고사항
출처 번호([1], [2] 등)는 포함하지 마.

View File

@ -0,0 +1,13 @@
다음은 성형외과/피부과 {clinic_name} 의 전 채널 수집 데이터입니다.
{data}
위 데이터를 바탕으로 이 병원의 마케팅 전반에 걸친 핵심 문제점과 개선사항을 진단해줘.
각 항목은 category(진단 카테고리), detail(상세 설명), severity(critical/warning/info) 형식의 JSON 배열로 출력해줘.
현재 주요 진단 카테고리는 3개야.
브랜드 아이덴티티 파편화
콘텐츠 전략 부재
플랫폼 간 유입 단절
출처 번호([1], [2] 등)는 포함하지 마.

View File

@ -0,0 +1,14 @@
다음은 성형외과/피부과 {clinic_name} 의 전 채널 수집 데이터입니다.
{data}
위 데이터를 바탕으로 이 병원의 3개월 마케팅 실행 로드맵을 수립해줘.
month 1, 2, 3 각각 하나씩, 총 3개 항목을 포함한 roadmap JSON 배열로 출력해줘.
각 항목은 아래 형식을 따라줘:
- month: 월 번호 (1, 2, 3)
- title: 해당 월의 핵심 테마 (예: "브랜드 정비")
- subtitle: 한 줄 부제 (예: "기반 구축 — 로고·계정 통일")
- tasks: 실행 과제 목록, 각 과제는 task(string)와 completed(false)로 구성
출처 번호([1], [2] 등)는 포함하지 마.

View File

@ -0,0 +1,14 @@
다음은 성형외과/피부과 {clinic_name} 의 전 채널 수집 데이터입니다.
{data}
위 데이터를 바탕으로 이 병원의 마케팅 전환 전략을 수립해줘.
아래 5개 항목을 포함한 JSON을 출력해줘.
1. brand_identity: 브랜드 아이덴티티 개선 항목 (area, as_is, to_be)
2. content_strategy: 콘텐츠 전략 개선 항목 (area, as_is, to_be)
3. platform_strategies: 플랫폼별 전략 (platform, icon, current_metric, target_metric, strategies[{strategy, detail}])
4. website_improvements: 웹사이트 개선 항목 (area, as_is, to_be)
5. new_channel_proposals: 신규 채널 제안 (channel, priority, rationale)
출처 번호([1], [2] 등)는 포함하지 마.

View File

@ -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.source import select_run_raw_data, select_run_mainpage_url
from common.db.market import select_market from common.db.market import select_market
from integrations.llm.llm_service import LLMService from integrations.llm.llm_service import LLMService
from integrations.llm.prompt import report_prompt, plan_prompt, youtube_diagnosis_prompt from integrations.llm.prompt import report_prompt, plan_prompt, youtube_diagnosis_prompt, brand_consistency_prompt, critical_issues_prompt, transformation_prompt, roadmap_prompt
from integrations.llm.schemas.report import ReportOutput, ClinicSnapshot, YouTubeAudit from integrations.llm.schemas.report import ReportOutput, ClinicSnapshot, YouTubeAudit, BrandConsistencyOutput, CriticalIssuesOutput, DiagnosisItem, TransformationProposal, RoadmapOutput, RoadmapMonth
from integrations.llm.schemas.plan import PlanOutput from integrations.llm.schemas.plan import PlanOutput
from models.status import AnalysisStatus
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -209,6 +208,39 @@ async def _build_youtube_audit(youtube: dict) -> dict:
return YouTubeAudit.model_validate(yt_patch).model_dump() return YouTubeAudit.model_validate(yt_patch).model_dump()
async def _build_roadmap(analysis_run_id: str, raw: dict) -> list[dict]:
result: RoadmapOutput = await LLMService(provider="perplexity").generate(
roadmap_prompt,
{
"clinic_name": (raw.get("mainpage") or {}).get("clinicName"),
"data": json.dumps(raw, ensure_ascii=False),
},
)
return [RoadmapMonth.model_validate(item).model_dump() for item in result.roadmap]
async def _build_transformation(analysis_run_id: str, raw: dict) -> dict:
result: TransformationProposal = await LLMService(provider="perplexity").generate(
transformation_prompt,
{
"clinic_name": (raw.get("mainpage") or {}).get("clinicName"),
"data": json.dumps(raw, ensure_ascii=False),
},
)
return result.model_dump()
async def _build_critical_issues(analysis_run_id: str, raw: dict) -> list[dict]:
result: CriticalIssuesOutput = await LLMService(provider="perplexity").generate(
critical_issues_prompt,
{
"clinic_name": (raw.get("mainpage") or {}).get("clinicName"),
"data": json.dumps(raw, ensure_ascii=False),
},
)
return [DiagnosisItem.model_validate(item).model_dump() for item in result.diagnosis]
async def _build_overrides(analysis_run_id: str) -> dict: async def _build_overrides(analysis_run_id: str) -> dict:
raw = await select_run_raw_data(analysis_run_id) raw = await select_run_raw_data(analysis_run_id)
if not raw: if not raw:
@ -233,15 +265,26 @@ async def _build_overrides(analysis_run_id: str) -> dict:
if instagram.get("username"): ig_patch["profile_link"] = f"https://www.instagram.com/{instagram['username']}/" if instagram.get("username"): ig_patch["profile_link"] = f"https://www.instagram.com/{instagram['username']}/"
# ── facebook ────────────────────────────────────────────────────────────── # ── facebook ──────────────────────────────────────────────────────────────
fb_patch: dict = {} fb_pages: dict = {}
if facebook.get("pageUrl"): fb_patch["url"] = facebook["pageUrl"] if facebook.get("pageUrl"): fb_pages["url"] = facebook["pageUrl"]
if facebook.get("pageUrl"): fb_patch["link"] = facebook["pageUrl"] if facebook.get("pageUrl"): fb_pages["link"] = facebook["pageUrl"]
if facebook.get("pageName"): fb_patch["page_name"] = facebook["pageName"] if facebook.get("pageName"): fb_pages["page_name"] = facebook["pageName"]
if facebook.get("followers"): fb_patch["followers"] = facebook["followers"] if facebook.get("followers"): fb_pages["followers"] = facebook["followers"]
if facebook.get("intro"): fb_patch["bio"] = facebook["intro"] if facebook.get("intro"): fb_pages["bio"] = facebook["intro"]
if facebook.get("categories"): fb_patch["category"] = ", ".join(facebook["categories"]) if facebook.get("categories"): fb_pages["category"] = ", ".join(facebook["categories"])
if facebook.get("website"): fb_patch["linked_domain"] = facebook["website"] 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"]
critical_issues = await _build_critical_issues(analysis_run_id, raw)
transformation = await _build_transformation(analysis_run_id, raw)
roadmap = await _build_roadmap(analysis_run_id, raw)
fb_patch: dict = {}
if fb_pages:
fb_patch["pages"] = [fb_pages]
if brand_patch:
fb_patch["brand_inconsistencies"] = brand_patch
overrides: dict = {} overrides: dict = {}
if snapshot: if snapshot:
@ -249,9 +292,15 @@ async def _build_overrides(analysis_run_id: str) -> dict:
if ig_patch: if ig_patch:
overrides["instagram_audit"] = {"accounts": [ig_patch]} overrides["instagram_audit"] = {"accounts": [ig_patch]}
if fb_patch: if fb_patch:
overrides["facebook_audit"] = {"pages": [fb_patch]} overrides["facebook_audit"] = fb_patch
if yt_patch: if yt_patch:
overrides["youtube_audit"] = yt_patch overrides["youtube_audit"] = yt_patch
if critical_issues:
overrides["problem_diagnosis"] = critical_issues
if transformation:
overrides["transformation"] = transformation
if roadmap:
overrides["roadmap"] = roadmap
return overrides return overrides
@ -268,8 +317,10 @@ def _deep_merge(base: dict, overrides: dict) -> dict:
return base return base
def _patch_report(result: ReportOutput, overrides: dict) -> ReportOutput: def _patch_report(result: ReportOutput, overrides: dict) -> ReportOutput:
merged = _deep_merge(result.model_dump(), overrides) base = result.model_dump()
return ReportOutput(**merged) for key in overrides:
base.pop(key, None)
return ReportOutput(**_deep_merge(base, overrides))
_MOCK_DOMAINS = {"viewclinic.com"} _MOCK_DOMAINS = {"viewclinic.com"}
@ -294,6 +345,24 @@ def _load_mock_plan() -> PlanOutput:
return PlanOutput(**json.load(f)) 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: async def run_report_task(analysis_run_id: str) -> None:
logger.info("[report] start run=%s", analysis_run_id) logger.info("[report] start run=%s", analysis_run_id)
if await _is_mock(analysis_run_id): if await _is_mock(analysis_run_id):
@ -302,6 +371,7 @@ async def run_report_task(analysis_run_id: str) -> None:
result.youtube_audit.linked_urls = [] result.youtube_audit.linked_urls = []
else: else:
result = await generate_report(analysis_run_id) result = await generate_report(analysis_run_id)
result = _patch_report(result, await _build_overrides(analysis_run_id)) result = _patch_report(result, await _build_overrides(analysis_run_id))
await update_run_report(analysis_run_id, result.model_dump()) await update_run_report(analysis_run_id, result.model_dump())
logger.info("[report] done run=%s", analysis_run_id) logger.info("[report] done run=%s", analysis_run_id)