plan 추가, analysis 오탈자 제거
parent
eec682b02c
commit
602c69543c
|
|
@ -151,3 +151,31 @@ CREATE INDEX IX_analysis_runs_1
|
|||
CREATE INDEX IX_analysis_runs_2
|
||||
ON analysis_runs(owner_user_id);
|
||||
|
||||
|
||||
-- hospital_history Table Create SQL
|
||||
CREATE TABLE hospital_history
|
||||
(
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`hospital_id` CHAR(36) NOT NULL,
|
||||
`owner_user_id` INT NOT NULL,
|
||||
`hospital_name` VARCHAR(50) NOT NULL,
|
||||
`hospital_name_en` VARCHAR(50) NULL,
|
||||
`brn` VARCHAR(50) NOT NULL,
|
||||
`road_address` VARCHAR(100) NULL,
|
||||
`site_address` VARCHAR(100) NULL,
|
||||
`url` VARCHAR(500) NULL,
|
||||
`status` VARCHAR(20) NOT NULL,
|
||||
`raw_data` JSON NULL,
|
||||
`analysis_run_id` CHAR(36) NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Index 설정 SQL - hospital_history(hospital_id)
|
||||
CREATE INDEX IX_hospital_history_1
|
||||
ON hospital_history(hospital_id);
|
||||
|
||||
-- Index 설정 SQL - hospital_history(analysis_run_id)
|
||||
CREATE INDEX IX_hospital_history_2
|
||||
ON hospital_history(analysis_run_id);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from .clinics import router as clinics_router
|
||||
from .analyses import router as analyses_router
|
||||
from .analysis import router as analysis_router
|
||||
from .reports import router as reports_router
|
||||
from .plans import router as plans_router
|
||||
|
||||
routers = [clinics_router, analyses_router, reports_router, plans_router]
|
||||
routers = [clinics_router, analysis_router, reports_router, plans_router]
|
||||
|
|
|
|||
|
|
@ -5,21 +5,21 @@ from common.deps import verify_api_key
|
|||
from common.db import fetchone, insert_instagram_row, insert_facebook_row, insert_naver_blog_row, insert_youtube_row, insert_gangnam_unni_row, insert_analysis_run
|
||||
from models.analysis import AnalysisCreate, AnalysisStartResponse, AnalysisStatusResponse
|
||||
from models.status import AnalysisStatus
|
||||
from services.collect import collect_instagram, collect_facebook, collect_naver_blog, collect_youtube, collect_gangnam_unni
|
||||
from services.collect import collect_instagram, collect_facebook, collect_naver_blog, collect_youtube, collect_gangnam_unni, collect_clinic_info
|
||||
|
||||
router = APIRouter(prefix="/api/analyses", tags=["analyses"], dependencies=[Depends(verify_api_key)])
|
||||
router = APIRouter(prefix="/api/analysis", tags=["analysis"], dependencies=[Depends(verify_api_key)])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_202_ACCEPTED, response_model=AnalysisStartResponse)
|
||||
async def start_analysis(body: AnalysisCreate, background_tasks: BackgroundTasks):
|
||||
logger.info("POST /api/analyses clinic_id=%s", body.clinic_id)
|
||||
logger.info("POST /api/analysis clinic_id=%s", body.clinic_id)
|
||||
analysis_run_id = str(uuid6.uuid7())
|
||||
hospital_id = body.clinic_id
|
||||
|
||||
# 사실 hospital과 owner_user_id 비교 후 검증이 필요한 거지만 일단 PoC 니까. 나중에 바꿉니다.
|
||||
hospital = await fetchone(
|
||||
"SELECT owner_user_id FROM hospital_baseinfo WHERE hospital_id = %s",
|
||||
"SELECT owner_user_id, url FROM hospital_baseinfo WHERE hospital_id = %s",
|
||||
(hospital_id,),
|
||||
)
|
||||
if not hospital:
|
||||
|
|
@ -34,6 +34,8 @@ async def start_analysis(body: AnalysisCreate, background_tasks: BackgroundTasks
|
|||
|
||||
analysis_run_id = await insert_analysis_run(analysis_run_id, hospital_id, owner_user_id, ig_id, fb_id, nb_id, yt_id, gu_id)
|
||||
|
||||
background_tasks.add_task(collect_clinic_info, analysis_run_id, hospital_id, hospital["url"])
|
||||
|
||||
if ig_id:
|
||||
background_tasks.add_task(collect_instagram, analysis_run_id, ig_id, body.channels.instagram)
|
||||
if fb_id:
|
||||
|
|
@ -50,13 +52,13 @@ async def start_analysis(body: AnalysisCreate, background_tasks: BackgroundTasks
|
|||
clinic_id=hospital_id,
|
||||
status=AnalysisStatus.DISCOVERING,
|
||||
estimated_seconds=90,
|
||||
poll_url=f"/api/analyses/{analysis_run_id}/status",
|
||||
poll_url=f"/api/analysis/{analysis_run_id}/status",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{run_id}/status", response_model=AnalysisStatusResponse)
|
||||
async def get_analysis_status(run_id: str):
|
||||
logger.info("GET /api/analyses/%s/status", run_id)
|
||||
logger.info("GET /api/analysis/%s/status", run_id)
|
||||
row = await fetchone("SELECT status FROM analysis_runs WHERE analysis_run_id = %s", (run_id,))
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Run not found")
|
||||
|
|
@ -1,35 +1,21 @@
|
|||
import json
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||
from common.db import fetchone
|
||||
from common.deps import verify_api_key
|
||||
from models.plan import PlanCreate, PlanResponse
|
||||
from integrations.llm.schemas.plan import PlanOutput
|
||||
|
||||
router = APIRouter(prefix="/api/plans", tags=["plans"], dependencies=[Depends(verify_api_key)])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED, response_model=PlanResponse)
|
||||
async def create_plan(body: PlanCreate):
|
||||
logger.info("POST /api/plans run_id=%s", body.analysis_run_id)
|
||||
return PlanResponse(
|
||||
id="33333333-3333-3333-3333-333333333333",
|
||||
analysis_run_id="22222222-2222-2222-2222-222222222222",
|
||||
brand_guide={},
|
||||
channel_strategies=[],
|
||||
content_strategy={},
|
||||
calendar=[],
|
||||
created_at="2026-04-20T09:10:00Z",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=PlanResponse)
|
||||
async def get_plan(id: str):
|
||||
logger.info("GET /api/plans/%s", id)
|
||||
return PlanResponse(
|
||||
id=id,
|
||||
analysis_run_id="22222222-2222-2222-2222-222222222222",
|
||||
brand_guide={},
|
||||
channel_strategies=[],
|
||||
content_strategy={},
|
||||
calendar=[],
|
||||
created_at="2026-04-20T09:10:00Z",
|
||||
)
|
||||
@router.get("/{run_id}", response_model=PlanOutput | None)
|
||||
async def get_plan(run_id: str):
|
||||
logger.info("GET /api/plans/%s", run_id)
|
||||
row = await fetchone("SELECT plan_data FROM analysis_runs WHERE analysis_run_id = %s", (run_id,))
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Run not found")
|
||||
if row["plan_data"] is None:
|
||||
return Response(status_code=204)
|
||||
data = json.loads(row["plan_data"]) if isinstance(row["plan_data"], str) else row["plan_data"]
|
||||
return PlanOutput(**data)
|
||||
|
|
|
|||
|
|
@ -154,6 +154,34 @@ async def save_gangnam_unni_raw_data(row_id: int, data: dict) -> None:
|
|||
await execute("UPDATE gangnam_unni_data SET raw_data = %s, status = 'done' WHERE id = %s", (json.dumps(data, ensure_ascii=False), row_id))
|
||||
|
||||
|
||||
async def _insert_hospital_history(hospital_id: str, analysis_run_id: str | None) -> None:
|
||||
row = await fetchone(
|
||||
"SELECT owner_user_id, hospital_name, hospital_name_en, brn, road_address, site_address, url, status, raw_data"
|
||||
" FROM hospital_baseinfo WHERE hospital_id = %s",
|
||||
(hospital_id,),
|
||||
)
|
||||
if not row:
|
||||
return
|
||||
await execute(
|
||||
"INSERT INTO hospital_history"
|
||||
" (hospital_id, owner_user_id, hospital_name, hospital_name_en, brn, road_address, site_address, url, status, raw_data, analysis_run_id)"
|
||||
" VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
||||
(
|
||||
hospital_id,
|
||||
row["owner_user_id"],
|
||||
row["hospital_name"],
|
||||
row["hospital_name_en"],
|
||||
row["brn"],
|
||||
row["road_address"],
|
||||
row["site_address"],
|
||||
row["url"],
|
||||
row["status"],
|
||||
row["raw_data"] if isinstance(row["raw_data"], str) else json.dumps(row["raw_data"], ensure_ascii=False) if row["raw_data"] else None,
|
||||
analysis_run_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def insert_hospital(
|
||||
hospital_id: str,
|
||||
name: str,
|
||||
|
|
@ -172,13 +200,14 @@ async def insert_hospital(
|
|||
json.dumps(raw_data, ensure_ascii=False) if raw_data else None,
|
||||
owner_user_id, brn),
|
||||
)
|
||||
await _insert_hospital_history(hospital_id, analysis_run_id=None)
|
||||
return await fetchone(
|
||||
"SELECT created_at FROM hospital_baseinfo WHERE hospital_id = %s",
|
||||
(hospital_id,),
|
||||
)
|
||||
|
||||
|
||||
async def save_hospital_raw_data(hospital_id: str, data: dict) -> None:
|
||||
async def save_hospital_raw_data(hospital_id: str, data: dict, analysis_run_id: str | None = None) -> None:
|
||||
await execute(
|
||||
"UPDATE hospital_baseinfo"
|
||||
" SET raw_data = %s, status = 'done',"
|
||||
|
|
@ -194,3 +223,4 @@ async def save_hospital_raw_data(hospital_id: str, data: dict) -> None:
|
|||
hospital_id,
|
||||
),
|
||||
)
|
||||
await _insert_hospital_history(hospital_id, analysis_run_id)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
from pydantic import BaseModel
|
||||
from common.utils import get_env
|
||||
from integrations.llm.schemas.report import ReportInput, ReportOutput
|
||||
from integrations.llm.schemas.plan import PlanInput, PlanOutput
|
||||
|
||||
_PROMPT_DIR = os.path.join(os.path.dirname(__file__), "temp-prompt")
|
||||
|
||||
|
|
@ -38,3 +39,10 @@ report_prompt = Prompt(
|
|||
input_class=ReportInput,
|
||||
output_class=ReportOutput,
|
||||
)
|
||||
|
||||
plan_prompt = Prompt(
|
||||
file_name="plan_prompt.txt",
|
||||
prompt_model="PLAN_MODEL",
|
||||
input_class=PlanInput,
|
||||
output_class=PlanOutput,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
from typing import Literal
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PlanInput(BaseModel):
|
||||
clinic_name: str | None = None
|
||||
clinic_name_en: str | None = None
|
||||
address: str | None = None
|
||||
phone: str | None = None
|
||||
slogan: str | None = None
|
||||
services: str | None = None
|
||||
doctors: str | None = None
|
||||
report: str | None = None
|
||||
|
||||
|
||||
# --- BrandGuide ---
|
||||
|
||||
class ColorSwatch(BaseModel):
|
||||
name: str
|
||||
hex: str
|
||||
usage: str
|
||||
|
||||
|
||||
class FontSpec(BaseModel):
|
||||
family: str
|
||||
weight: str
|
||||
usage: str
|
||||
sampleText: str
|
||||
|
||||
|
||||
class LogoUsageRule(BaseModel):
|
||||
rule: str
|
||||
description: str
|
||||
correct: bool
|
||||
|
||||
|
||||
class ToneOfVoice(BaseModel):
|
||||
personality: list[str]
|
||||
communicationStyle: str
|
||||
doExamples: list[str]
|
||||
dontExamples: list[str]
|
||||
|
||||
|
||||
class ChannelBrandingRule(BaseModel):
|
||||
channel: str
|
||||
icon: str
|
||||
profilePhoto: str
|
||||
bannerSpec: str
|
||||
bioTemplate: str
|
||||
currentStatus: Literal["correct", "incorrect", "missing"]
|
||||
|
||||
|
||||
class BrandInconsistencyValue(BaseModel):
|
||||
channel: str
|
||||
value: str
|
||||
isCorrect: bool
|
||||
|
||||
|
||||
class BrandInconsistency(BaseModel):
|
||||
field: str
|
||||
values: list[BrandInconsistencyValue]
|
||||
impact: str
|
||||
recommendation: str
|
||||
|
||||
|
||||
class BrandGuide(BaseModel):
|
||||
colors: list[ColorSwatch]
|
||||
fonts: list[FontSpec]
|
||||
logoRules: list[LogoUsageRule]
|
||||
toneOfVoice: ToneOfVoice
|
||||
channelBranding: list[ChannelBrandingRule]
|
||||
brandInconsistencies: list[BrandInconsistency]
|
||||
|
||||
|
||||
# --- ChannelStrategy ---
|
||||
|
||||
class ChannelStrategyCard(BaseModel):
|
||||
channelId: str
|
||||
channelName: str
|
||||
icon: str
|
||||
currentStatus: str
|
||||
targetGoal: str
|
||||
contentTypes: list[str]
|
||||
postingFrequency: str
|
||||
tone: str
|
||||
formatGuidelines: list[str]
|
||||
priority: Literal["P0", "P1", "P2"]
|
||||
customerJourneyStage: Literal["awareness", "interest", "consideration", "conversion", "loyalty"] | None = None
|
||||
|
||||
|
||||
# --- ContentStrategy ---
|
||||
|
||||
class ContentPillar(BaseModel):
|
||||
title: str
|
||||
description: str
|
||||
relatedUSP: str
|
||||
exampleTopics: list[str]
|
||||
color: str
|
||||
|
||||
|
||||
class ContentTypeRow(BaseModel):
|
||||
format: str
|
||||
channels: list[str]
|
||||
frequency: str
|
||||
purpose: str
|
||||
|
||||
|
||||
class WorkflowStep(BaseModel):
|
||||
step: int
|
||||
name: str
|
||||
description: str
|
||||
owner: str
|
||||
duration: str
|
||||
|
||||
|
||||
class RepurposingOutput(BaseModel):
|
||||
format: str
|
||||
channel: str
|
||||
description: str
|
||||
|
||||
|
||||
class ContentStrategyData(BaseModel):
|
||||
pillars: list[ContentPillar]
|
||||
typeMatrix: list[ContentTypeRow]
|
||||
workflow: list[WorkflowStep]
|
||||
repurposingSource: str
|
||||
repurposingOutputs: list[RepurposingOutput]
|
||||
|
||||
|
||||
# --- Calendar ---
|
||||
|
||||
class CalendarEntry(BaseModel):
|
||||
dayOfWeek: int
|
||||
channel: str
|
||||
channelIcon: str
|
||||
contentType: Literal["video", "blog", "social", "ad"]
|
||||
title: str
|
||||
id: str | None = None
|
||||
description: str | None = None
|
||||
pillar: str | None = None
|
||||
status: Literal["draft", "approved", "published"] | None = None
|
||||
isManualEdit: bool | None = None
|
||||
aiPromptSeed: str | None = None
|
||||
|
||||
|
||||
class CalendarWeek(BaseModel):
|
||||
weekNumber: int
|
||||
label: str
|
||||
entries: list[CalendarEntry]
|
||||
|
||||
|
||||
class ContentCountSummary(BaseModel):
|
||||
type: Literal["video", "blog", "social", "ad"]
|
||||
label: str
|
||||
count: int
|
||||
color: str
|
||||
|
||||
|
||||
class CalendarData(BaseModel):
|
||||
weeks: list[CalendarWeek]
|
||||
monthlySummary: list[ContentCountSummary]
|
||||
|
||||
|
||||
# --- AssetCollection ---
|
||||
|
||||
class AssetCard(BaseModel):
|
||||
id: str
|
||||
source: Literal["homepage", "naver_place", "blog", "social", "youtube"]
|
||||
sourceLabel: str
|
||||
type: Literal["photo", "video", "text"]
|
||||
title: str
|
||||
description: str
|
||||
repurposingSuggestions: list[str]
|
||||
status: Literal["collected", "pending", "needs_creation"]
|
||||
|
||||
|
||||
class YouTubeRepurposeItem(BaseModel):
|
||||
title: str
|
||||
views: int
|
||||
type: Literal["Short", "Long"]
|
||||
repurposeAs: list[str]
|
||||
|
||||
|
||||
class AssetCollectionData(BaseModel):
|
||||
assets: list[AssetCard]
|
||||
youtubeRepurpose: list[YouTubeRepurposeItem]
|
||||
|
||||
|
||||
# --- Repurposing ---
|
||||
|
||||
class RepurposingProposalItem(BaseModel):
|
||||
sourceVideo: YouTubeRepurposeItem
|
||||
outputs: list[RepurposingOutput]
|
||||
estimatedEffort: Literal["low", "medium", "high"]
|
||||
priority: Literal["high", "medium", "low"]
|
||||
|
||||
|
||||
# --- Workflow ---
|
||||
|
||||
class WorkflowVideoDraft(BaseModel):
|
||||
script: str
|
||||
shootingGuide: list[str]
|
||||
duration: str
|
||||
|
||||
|
||||
# class WorkflowImageTextDraft(BaseModel):
|
||||
# type: Literal["cardnews", "blog"]
|
||||
# headline: str
|
||||
# copy: list[str]
|
||||
# layoutHint: str | None = None
|
||||
|
||||
|
||||
# class WorkflowItem(BaseModel):
|
||||
# id: str
|
||||
# title: str
|
||||
# contentType: Literal["video", "image-text"]
|
||||
# channel: str
|
||||
# channelIcon: str
|
||||
# stage: Literal["planning", "ai-draft", "review", "approved", "scheduled"]
|
||||
# userNotes: str | None = None
|
||||
# videoDraft: WorkflowVideoDraft | None = None
|
||||
# imageTextDraft: WorkflowImageTextDraft | None = None
|
||||
# scheduledDate: str | None = None
|
||||
|
||||
|
||||
# class WorkflowData(BaseModel):
|
||||
# items: list[WorkflowItem]
|
||||
|
||||
|
||||
# --- PlanOutput ---
|
||||
|
||||
class PlanOutput(BaseModel):
|
||||
brandGuide: BrandGuide
|
||||
channelStrategies: list[ChannelStrategyCard]
|
||||
contentStrategy: ContentStrategyData
|
||||
calendar: CalendarData
|
||||
assetCollection: AssetCollectionData
|
||||
repurposingProposals: list[RepurposingProposalItem] | None = None
|
||||
#workflow: WorkflowData | None = None
|
||||
|
|
@ -1 +1,55 @@
|
|||
당신은 프리미엄 의료 마케팅 전략 플래너입니다. 아래 병원 정보와 분석 리포트를 바탕으로 실행 가능한 마케팅 플랜을 생성해주세요.
|
||||
결과물은 한국어로 생성하세요.
|
||||
|
||||
⚠️ 중요 지침:
|
||||
- 리포트에 데이터가 없는 채널은 channelStrategies에 포함하지 마세요.
|
||||
- 추측하지 마세요. 데이터에 근거한 내용만 작성하세요.
|
||||
- 선택 필드(repurposingProposals, workflow)는 YouTube 데이터가 있을 때만 생성하세요.
|
||||
|
||||
## 병원 기본 정보
|
||||
- 병원명: {clinic_name}
|
||||
- 영문명: {clinic_name_en}
|
||||
- 주소: {address}
|
||||
- 전화: {phone}
|
||||
- 슬로건: {slogan}
|
||||
- 시술: {services}
|
||||
- 의료진: {doctors}
|
||||
|
||||
## 분석 리포트
|
||||
{report}
|
||||
|
||||
## 섹션별 작성 지침
|
||||
|
||||
### Section 1: brandGuide
|
||||
- colors: 병원 아이덴티티에 맞는 컬러 팔레트 3~5개 (hex + 사용 가이드)
|
||||
- fonts: 제목/본문/캡션용 폰트 시스템 (한글/영문 포함)
|
||||
- logoRules: DO/DON'T 형식의 로고 사용 규칙 4~6개
|
||||
- toneOfVoice: 브랜드 성격 키워드, 커뮤니케이션 스타일, 권장/지양 표현 예시
|
||||
- channelBranding: 리포트에 존재하는 채널별 브랜딩 적용 규칙
|
||||
- brandInconsistencies: 채널 간 브랜딩 불일치 항목 및 개선 권고
|
||||
|
||||
### Section 2: channelStrategies
|
||||
- 리포트에 데이터가 있는 채널만 포함
|
||||
- 각 채널의 우선순위(P0/P1/P2), 목표, 콘텐츠 유형, 게시 빈도, 포맷 가이드라인 작성
|
||||
- customerJourneyStage는 해당 채널의 주요 기여 단계로 설정
|
||||
|
||||
### Section 3: contentStrategy
|
||||
- pillars: 3~5개의 콘텐츠 필러 (병원 USP 기반)
|
||||
- typeMatrix: 포맷 × 채널 매트릭스 (Shorts/Reels/블로그/카드뉴스 등)
|
||||
- workflow: 기획→촬영→편집→업로드의 제작 워크플로우 단계
|
||||
- repurposingSource/repurposingOutputs: YouTube 롱폼 기반 재가공 전략
|
||||
|
||||
### Section 4: calendar
|
||||
- 4주 캘린더로 채널별 게시 슬롯 배분
|
||||
- 각 항목은 요일(0=월~6=일), 채널, 콘텐츠 유형, 제목 포함
|
||||
- monthlySummary에 카테고리(video/blog/social/ad)별 월간 게시 수 집계
|
||||
|
||||
### Section 5: assetCollection
|
||||
- assets: 홈페이지/SNS에서 수집 가능한 에셋 카드 (사진/영상/텍스트)
|
||||
- youtubeRepurpose: 조회수 높은 YouTube 영상 재가공 후보 목록
|
||||
|
||||
### Section 6: repurposingProposals (YouTube 데이터 있을 때만)
|
||||
- YouTube 영상별 재가공 산출물, 예상 난이도, 우선순위 제시
|
||||
|
||||
### Section 7: workflow (선택)
|
||||
- 제작 진행 중인 콘텐츠 카드 초안 (스크립트 또는 카드뉴스 카피 포함)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ class AnalysisStatus(StrEnum):
|
|||
DISCOVERING = "discovering"
|
||||
COLLECTING = "collecting"
|
||||
ANALYZING = "analyzing"
|
||||
PLANNING = "planning"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import json
|
|||
import logging
|
||||
from common.db import fetchone, execute, is_done, get_analysis_raw_data, save_analysis_report
|
||||
from integrations.llm.llm_service import LLMService
|
||||
from integrations.llm.prompt import report_prompt
|
||||
from integrations.llm.prompt import report_prompt, plan_prompt
|
||||
from integrations.llm.schemas.report import ReportOutput
|
||||
from integrations.llm.schemas.plan import PlanOutput
|
||||
from models.status import AnalysisStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -39,21 +41,65 @@ async def generate_report(analysis_run_id: str) -> ReportOutput:
|
|||
return await LLMService(provider="perplexity").generate(report_prompt, input_data)
|
||||
|
||||
|
||||
async def generate_plan(analysis_run_id: str) -> PlanOutput:
|
||||
run = await fetchone(
|
||||
"SELECT hospital_id, report_data FROM analysis_runs WHERE analysis_run_id = %s",
|
||||
(analysis_run_id,),
|
||||
)
|
||||
clinic_row = await fetchone(
|
||||
"SELECT raw_data FROM hospital_baseinfo WHERE hospital_id = %s",
|
||||
(run["hospital_id"],),
|
||||
)
|
||||
raw_data = clinic_row["raw_data"] if clinic_row else None
|
||||
clinic = json.loads(raw_data) if isinstance(raw_data, str) else (raw_data or {})
|
||||
report_data = run["report_data"]
|
||||
report = json.loads(report_data) if isinstance(report_data, str) else report_data
|
||||
|
||||
input_data = {
|
||||
"clinic_name": clinic.get("clinicName"),
|
||||
"clinic_name_en": clinic.get("clinicNameEn"),
|
||||
"address": clinic.get("address"),
|
||||
"phone": clinic.get("phone"),
|
||||
"slogan": clinic.get("slogan"),
|
||||
"services": json.dumps(clinic.get("services", []), ensure_ascii=False),
|
||||
"doctors": json.dumps(clinic.get("doctors", []), ensure_ascii=False),
|
||||
"report": json.dumps(report, ensure_ascii=False) if report else None,
|
||||
}
|
||||
|
||||
return await LLMService(provider="perplexity").generate(plan_prompt, input_data)
|
||||
|
||||
|
||||
async def run_plan_task(analysis_run_id: str) -> None:
|
||||
logger.info("[plan] start run=%s", analysis_run_id)
|
||||
result = await generate_plan(analysis_run_id)
|
||||
await execute(
|
||||
"UPDATE analysis_runs SET plan_data = %s, status = %s WHERE analysis_run_id = %s",
|
||||
(json.dumps(result.model_dump(), ensure_ascii=False), AnalysisStatus.COMPLETED, analysis_run_id),
|
||||
)
|
||||
logger.info("[plan] done run=%s", analysis_run_id)
|
||||
|
||||
|
||||
async def run_report_task(analysis_run_id: str) -> None:
|
||||
logger.info("[report] start run=%s", analysis_run_id)
|
||||
result = await generate_report(analysis_run_id)
|
||||
await save_analysis_report(analysis_run_id, result.model_dump())
|
||||
await execute("UPDATE analysis_runs SET status = 'completed' WHERE analysis_run_id = %s", (analysis_run_id,))
|
||||
await execute("UPDATE analysis_runs SET status = %s WHERE analysis_run_id = %s", (AnalysisStatus.PLANNING, analysis_run_id))
|
||||
logger.info("[report] done run=%s", analysis_run_id)
|
||||
asyncio.create_task(run_plan_task(analysis_run_id))
|
||||
|
||||
|
||||
async def check_and_advance_analysis(analysis_run_id: str) -> None:
|
||||
run = await fetchone(
|
||||
"SELECT instagram_data_id, facebook_data_id, naver_blog_data_id, youtube_data_id, gangnam_unni_data_id"
|
||||
"SELECT hospital_id, instagram_data_id, facebook_data_id, naver_blog_data_id, youtube_data_id, gangnam_unni_data_id"
|
||||
" FROM analysis_runs WHERE analysis_run_id = %s",
|
||||
(analysis_run_id,),
|
||||
)
|
||||
clinic_row = await fetchone(
|
||||
"SELECT status FROM hospital_baseinfo WHERE hospital_id = %s",
|
||||
(run["hospital_id"],),
|
||||
)
|
||||
results = [
|
||||
clinic_row["status"] == "done" if clinic_row else False,
|
||||
await is_done("instagram_data", run["instagram_data_id"]),
|
||||
await is_done("facebook_data", run["facebook_data_id"]),
|
||||
await is_done("naver_blog_data", run["naver_blog_data_id"]),
|
||||
|
|
@ -61,5 +107,5 @@ async def check_and_advance_analysis(analysis_run_id: str) -> None:
|
|||
await is_done("gangnam_unni_data", run["gangnam_unni_data_id"]),
|
||||
]
|
||||
if all(results):
|
||||
await execute("UPDATE analysis_runs SET status = 'analyzing' WHERE analysis_run_id = %s", (analysis_run_id,))
|
||||
await execute("UPDATE analysis_runs SET status = %s WHERE analysis_run_id = %s", (AnalysisStatus.ANALYZING, analysis_run_id))
|
||||
asyncio.create_task(run_report_task(analysis_run_id))
|
||||
|
|
|
|||
|
|
@ -62,7 +62,10 @@ async def collect_gangnam_unni(analysis_run_id: str, row_id: int, url: str) -> N
|
|||
await check_and_advance_analysis(analysis_run_id)
|
||||
|
||||
|
||||
async def collect_clinic_info(hospital_id: str, url: str) -> None:
|
||||
async def collect_clinic_info(analysis_run_id: str, hospital_id: str, url: str) -> None:
|
||||
logger.info("[clinic] start run=%s url=%s", analysis_run_id, url)
|
||||
await execute("UPDATE hospital_baseinfo SET status = 'processing' WHERE hospital_id = %s", (hospital_id,))
|
||||
data = await FirecrawlClient(get_env("FIRECRAWL_API_KEY")).fetch_clinic_info(url)
|
||||
await save_hospital_raw_data(hospital_id, data)
|
||||
await save_hospital_raw_data(hospital_id, data, analysis_run_id=analysis_run_id)
|
||||
logger.info("[clinic] done run=%s", analysis_run_id)
|
||||
await check_and_advance_analysis(analysis_run_id)
|
||||
|
|
|
|||
Loading…
Reference in New Issue