Compare commits
No commits in common. "45a74ab9706799f837ddbc0adc8d5930fb6a7bd5" and "b6a0134ba76137ba421a8cb16f7adf1bb6589e6c" have entirely different histories.
45a74ab970
...
b6a0134ba7
|
|
@ -1,14 +1,7 @@
|
||||||
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 (
|
from integrations.llm.schemas.report import ReportInput, ReportOutput, YouTubeDiagnosisInput, YouTubeDiagnosisOutput
|
||||||
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,
|
||||||
|
|
@ -94,31 +87,3 @@ 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,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -355,47 +355,3 @@ 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]
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
다음은 성형외과/피부과 {clinic_name} 의 채널별 브랜드 데이터입니다.
|
|
||||||
|
|
||||||
공식 홈페이지: {mainpage}
|
|
||||||
인스타그램: {instagram}
|
|
||||||
페이스북: {facebook}
|
|
||||||
유튜브: {youtube}
|
|
||||||
강남언니: {gangnam_unni}
|
|
||||||
|
|
||||||
위 채널들 간의 브랜드 불일치 항목을 분석해줘.
|
|
||||||
비교 대상 필드 예시: 병원명(한글/영문), 전화번호, 주소, 로고, 슬로건, 소개 문구 등.
|
|
||||||
|
|
||||||
각 항목은 다음 JSON 형식의 배열로 출력해줘:
|
|
||||||
- field: 불일치 필드명
|
|
||||||
- values: 채널별 실제 값 목록 (channel, value, is_correct)
|
|
||||||
- impact: 불일치가 브랜드에 미치는 영향
|
|
||||||
- recommendation: 개선 권고사항
|
|
||||||
|
|
||||||
출처 번호([1], [2] 등)는 포함하지 마.
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
다음은 성형외과/피부과 {clinic_name} 의 전 채널 수집 데이터입니다.
|
|
||||||
|
|
||||||
{data}
|
|
||||||
|
|
||||||
위 데이터를 바탕으로 이 병원의 마케팅 전반에 걸친 핵심 문제점과 개선사항을 진단해줘.
|
|
||||||
각 항목은 category(진단 카테고리), detail(상세 설명), severity(critical/warning/info) 형식의 JSON 배열로 출력해줘.
|
|
||||||
|
|
||||||
현재 주요 진단 카테고리는 3개야.
|
|
||||||
브랜드 아이덴티티 파편화
|
|
||||||
콘텐츠 전략 부재
|
|
||||||
플랫폼 간 유입 단절
|
|
||||||
|
|
||||||
출처 번호([1], [2] 등)는 포함하지 마.
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
다음은 성형외과/피부과 {clinic_name} 의 전 채널 수집 데이터입니다.
|
|
||||||
|
|
||||||
{data}
|
|
||||||
|
|
||||||
위 데이터를 바탕으로 이 병원의 3개월 마케팅 실행 로드맵을 수립해줘.
|
|
||||||
month 1, 2, 3 각각 하나씩, 총 3개 항목을 포함한 roadmap JSON 배열로 출력해줘.
|
|
||||||
|
|
||||||
각 항목은 아래 형식을 따라줘:
|
|
||||||
- month: 월 번호 (1, 2, 3)
|
|
||||||
- title: 해당 월의 핵심 테마 (예: "브랜드 정비")
|
|
||||||
- subtitle: 한 줄 부제 (예: "기반 구축 — 로고·계정 통일")
|
|
||||||
- tasks: 실행 과제 목록, 각 과제는 task(string)와 completed(false)로 구성
|
|
||||||
|
|
||||||
출처 번호([1], [2] 등)는 포함하지 마.
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
다음은 성형외과/피부과 {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] 등)는 포함하지 마.
|
|
||||||
|
|
@ -8,9 +8,10 @@ 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, brand_consistency_prompt, critical_issues_prompt, transformation_prompt, roadmap_prompt
|
from integrations.llm.prompt import report_prompt, plan_prompt, youtube_diagnosis_prompt
|
||||||
from integrations.llm.schemas.report import ReportOutput, ClinicSnapshot, YouTubeAudit, BrandConsistencyOutput, CriticalIssuesOutput, DiagnosisItem, TransformationProposal, RoadmapOutput, RoadmapMonth
|
from integrations.llm.schemas.report import ReportOutput, ClinicSnapshot, YouTubeAudit
|
||||||
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__)
|
||||||
|
|
||||||
|
|
@ -208,39 +209,6 @@ 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:
|
||||||
|
|
@ -265,26 +233,15 @@ 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_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"]
|
|
||||||
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 = {}
|
fb_patch: dict = {}
|
||||||
if fb_pages:
|
if facebook.get("pageUrl"): fb_patch["url"] = facebook["pageUrl"]
|
||||||
fb_patch["pages"] = [fb_pages]
|
if facebook.get("pageUrl"): fb_patch["link"] = facebook["pageUrl"]
|
||||||
if brand_patch:
|
if facebook.get("pageName"): fb_patch["page_name"] = facebook["pageName"]
|
||||||
fb_patch["brand_inconsistencies"] = brand_patch
|
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"]
|
||||||
|
|
||||||
|
|
||||||
overrides: dict = {}
|
overrides: dict = {}
|
||||||
if snapshot:
|
if snapshot:
|
||||||
|
|
@ -292,15 +249,9 @@ 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"] = fb_patch
|
overrides["facebook_audit"] = {"pages": [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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -317,10 +268,8 @@ 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:
|
||||||
base = result.model_dump()
|
merged = _deep_merge(result.model_dump(), overrides)
|
||||||
for key in overrides:
|
return ReportOutput(**merged)
|
||||||
base.pop(key, None)
|
|
||||||
return ReportOutput(**_deep_merge(base, overrides))
|
|
||||||
|
|
||||||
|
|
||||||
_MOCK_DOMAINS = {"viewclinic.com"}
|
_MOCK_DOMAINS = {"viewclinic.com"}
|
||||||
|
|
@ -345,24 +294,6 @@ 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):
|
||||||
|
|
@ -371,7 +302,6 @@ 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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue