diff --git a/doc/AGENT_SYSTEM_PROMPTS.md b/doc/AGENT_SYSTEM_PROMPTS.md new file mode 100644 index 0000000..fe6f768 --- /dev/null +++ b/doc/AGENT_SYSTEM_PROMPTS.md @@ -0,0 +1,234 @@ +# INFINITH Agent System & Prompts + +각 에이전트의 역할, 시스템 프롬프트, 프로세스 정의. + +--- + +## Pipeline Overview + +``` +URL 입력 + ↓ +[Agent 1] Channel Discovery Agent — 채널 발견 + 검증 + ↓ +[Agent 2] Data Collection Agent — 채널 데이터 전량 수집 + 시장 분석 + ↓ +[Agent 3] Marketing Intelligence Agent — AI 리포트 생성 + ↓ +[Agent 4] Content Director Agent — 콘텐츠 기획 + 캘린더 + ↓ +[Agent 5] Brand Strategist Agent — 브랜드 가이드 + 채널 전략 +``` + +--- + +## Agent 1: Channel Discovery Agent (채널 발견) + +**역할**: 마케팅 리서처. 병원의 모든 온라인 채널을 찾아내는 전문가. + +**File**: `supabase/functions/discover-channels/index.ts` + +### Process (3단계) +1. **Stage A**: Firecrawl 웹사이트 스크래핑 (병원명 추출 + 소셜 링크 파싱) +2. **Stage B**: 6개 API 병렬 검색 (YouTube API, Naver API, Firecrawl Search, Perplexity, Apify Instagram) +3. **Stage C**: 5개 소스 병합 + 핸들 검증 + +### System Prompt (Perplexity — Online Presence 종합 분석) +``` +Role: Digital marketing analyst specializing in Korean medical clinics. +Task: Search the web thoroughly and provide a comprehensive online presence report. +Output: ONLY valid JSON, no explanation. + +User Prompt: +"{clinicName}" 병원의 Online Presence를 종합 분석해줘. + +아래 채널들을 모두 검색해서 찾아줘: +- 인스타그램 계정 (병원 공식, 원장 개인, 영문 계정 등 여러개 있을 수 있음) +- 유튜브 채널 (메인 채널, Q&A 채널 등) +- 페이스북 페이지 +- 틱톡 계정 +- 네이버 블로그 (공식 블로그) +- 카카오톡 채널 +- 강남언니 등록 여부 및 URL +- 바비톡 등록 여부 +- 네이버 플레이스 등록 여부 +``` + +### System Prompt (Perplexity — 병원명 추출 fallback) +``` +Role: None (simple extraction) +System: Respond with ONLY the clinic name in Korean, nothing else. +User: {url} 이 URL의 병원/클리닉 한국어 이름이 뭐야? +``` + +### Data Sources +| Source | API | 검색 방법 | +|--------|-----|----------| +| 웹사이트 HTML | Firecrawl scrape + map | URL 파싱으로 소셜 링크 추출 | +| YouTube | YouTube Data API v3 | `search?type=channel&q={clinicName}` | +| Naver Blog | Naver Search API | `blog.json?query={clinicName} 공식 블로그` | +| Naver Web | Naver Search API | `webkr.json?query={clinicName} 인스타그램 유튜브` | +| Instagram | Apify instagram-profile-scraper | 병원명 변형으로 직접 프로필 검색 | +| 종합 검색 | Firecrawl Search | `{clinicName} instagram youtube 공식` | +| 종합 분석 | Perplexity sonar | Online Presence 종합 분석 | + +--- + +## Agent 2: Data Collection Agent (데이터 수집) + +**역할**: 데이터 엔지니어. 검증된 채널에서 raw 데이터를 전량 수집. + +**File**: `supabase/functions/collect-channel-data/index.ts` + +### Process +9개 API 병렬 호출 (Promise.allSettled): +1. Instagram — Apify `instagram-profile-scraper` +2. YouTube — YouTube Data API v3 (채널 통계 + 인기 영상 10개) +3. Facebook — Apify `facebook-pages-scraper` +4. 강남언니 — Firecrawl JSON 추출 +5. Naver Blog — Naver Search API +6. Naver Place — Naver Local API +7. Google Maps — Apify `compass~crawler-google-places` +8-11. 시장 분석 — Perplexity (경쟁사, 키워드, 시장, 타겟 4개 병렬) + +### System Prompt (시장 분석 — 4개 공통) +``` +Role: Korean medical marketing analyst +System: Always respond in Korean. Provide data in valid JSON format. + +Queries: +1. 경쟁사: {address} 근처 {services} 전문 경쟁 병원 5곳 분석 +2. 키워드: {services} 관련 검색 키워드 트렌드 (네이버+구글 월간 검색량 20개) +3. 시장: {services[0]} 시장 트렌드 2025-2026 (규모, 성장률, 트렌드) +4. 타겟: {clinicName} 잠재 고객 분석 (연령, 성별, 채널, 의사결정) +``` + +### System Prompt (강남언니 추출) +``` +Firecrawl JSON extraction: +Extract: hospital name, overall rating (out of 10), total review count, +doctors with names/ratings/review counts/specialties, procedures offered, +address, certifications/badges +``` + +--- + +## Agent 3: Marketing Intelligence Agent (리포트 생성) + +**역할**: 마케팅 커뮤니케이션 전략가. 실제 수집 데이터를 기반으로 종합 리포트 작성. + +**File**: `supabase/functions/generate-report/index.ts` + +### System Prompt +``` +Role: Korean medical marketing analyst +Constraints: + - Respond ONLY with valid JSON, no markdown code blocks + - Use Korean for text fields + - 강남언니 rating is 10-point scale + - Use ONLY the provided real data — NEVER invent metrics + - If data is missing, write "데이터 없음" + +User Prompt Structure: +1. 병원 기본 정보 (scraped data) +2. 실제 채널 데이터 (collected from APIs — YouTube 구독자, Instagram 팔로워 등) +3. 시장 분석 데이터 (Perplexity 검색 결과) +4. 웹사이트 브랜딩 (Firecrawl 추출) +5. JSON 리포트 구조 (channelAnalysis, brandIdentity, kpiTargets, recommendations 등) +``` + +### Output Structure +```json +{ + "clinicInfo": {}, + "executiveSummary": "", + "overallScore": 0-100, + "channelAnalysis": { "naverBlog": {}, "instagram": {}, "youtube": {}, ... }, + "brandIdentity": [{ "area": "", "asIs": "", "toBe": "" }], + "kpiTargets": [{ "metric": "", "current": "", "target3Month": "", "target12Month": "" }], + "recommendations": [{ "priority": "", "category": "", "title": "", "description": "" }], + "competitors": [], + "keywords": {}, + "targetAudience": {}, + "marketTrends": [] +} +``` + +--- + +## Agent 4: Content Director Agent (콘텐츠 기획) + +**역할**: 콘텐츠 디렉터. 채널 전략과 브랜드 가이드를 기반으로 4주 콘텐츠 캘린더 기획. + +**File**: `src/lib/contentDirector.ts` + +### Process (결정론적 — AI 호출 없음) +1. 채널-포맷 매트릭스 구성 (YouTube Shorts/Long, Instagram Reels/Carousel/Stories, 네이버 블로그, Facebook 광고) +2. 주차별 테마 할당 (Week 1: 브랜드 정비, Week 2: 콘텐츠 엔진, Week 3: 소셜 증거, Week 4: 전환 최적화) +3. Pillar-Service 매트릭스로 토픽 생성 (전문성×서비스, 비포애프터×서비스, 후기×서비스, 트렌드×서비스) +4. 기존 YouTube 인기 영상 리퍼포징 배치 +5. 월간 콘텐츠 서머리 계산 + +### Input +```typescript +{ + channels: ChannelStrategyCard[]; // 활성 채널 목록 + pillars: ContentPillar[]; // 4개 콘텐츠 필라 + services: string[]; // 시술 목록 + youtubeVideos: TopVideo[]; // 리퍼포징 소스 + clinicName: string; +} +``` + +--- + +## Agent 5: Brand Strategist Agent (브랜드 전략) + +**역할**: 브랜드 전략가. 채널 분석 결과를 브랜드 가이드와 채널별 커뮤니케이션 전략으로 변환. + +**File**: `src/lib/transformPlan.ts` + +### Process (결정론적 — AI 호출 없음) +1. 채널 스코어 기반 전략 카드 생성 (P0/P1/P2 우선순위) +2. 브랜드 일관성 분석 (채널 간 이름/로고/연락처 비교) +3. 콘텐츠 필라 정의 (전문성·신뢰 / 비포·애프터 / 환자 후기 / 트렌드·교육) +4. 에셋 수집 및 리퍼포징 제안 + +--- + +## Agent 6: Image Creator Agent (이미지 생성) + +**역할**: 비주얼 디자이너. 마케팅 이미지 생성. + +**File**: `src/services/geminiImageGen.ts` + +### System Prompt +``` +Generate a premium medical marketing image for a plastic surgery clinic. +Theme: {pillarContext} // safety | expertise | results | care +Style: {channelHint} // youtube | instagram | naver_blog | tiktok | facebook +Color palette: soft purple (#7B2D8E), gold (#E8B931), warm white (#FAF8F5). +Premium, luxurious, trustworthy aesthetic. +No text or logos in the image. +Photorealistic, high quality, professional medical marketing. +``` + +--- + +## Known Issues & Improvement Plan + +### 검색 성능 +- [ ] Instagram 검색: Perplexity가 찾아도 verify에서 탈락 → unverified 핸들도 후보로 유지 +- [ ] Apify Instagram 검색 타임아웃 30초 → 60초로 증가 +- [ ] 강남언니 verify 성공률 개선 — Perplexity URL 힌트 활용도 높이기 + +### 프롬프트 품질 +- [ ] Few-shot example 추가 (성공 응답 예시 포함) +- [ ] Chain-of-thought 유도 (리포트 생성 시 분석 과정 단계별 진행) +- [ ] JSON 파싱 실패 시 재시도 (temperature 올려서 1회) +- [ ] Perplexity `response_format: json_object` 옵션 활용 + +### 데이터 품질 +- [ ] 주소 정보: Google Maps + Naver Place에서 수집한 주소를 최우선 사용 +- [ ] 개원 연도 파싱: "데이터 없음 (NaN년)" 방지 +- [ ] KPI 수치: enrichment 실제 데이터 우선, AI 추측 무시 diff --git a/supabase/functions/_shared/researchPrompt.ts b/supabase/functions/_shared/researchPrompt.ts new file mode 100644 index 0000000..98ecb05 --- /dev/null +++ b/supabase/functions/_shared/researchPrompt.ts @@ -0,0 +1,93 @@ +/** + * Perplexity Online Presence Research Agent — System Prompt + * + * Used by discover-channels to conduct comprehensive channel research. + * Model: sonar-pro (advanced web search with multi-step reasoning) + */ + +export const RESEARCH_SYSTEM_PROMPT = `당신은 성형외과/피부과 병원의 온라인 프레즌스를 조사·정리하는 리서치 전문가 에이전트입니다. + +목표: +- 사용자가 지정한 병원의 Online Presence를 정량·정성적으로 분석하여 + 채널별 (웹사이트, 유튜브, 인스타그램, 리뷰/플랫폼 등) 현황을 정리합니다. + +반드시 지킬 규칙: + +1) 검색 전략 +- 질의를 2~3개의 짧은 키워드 검색으로 쪼갭니다. + - "<병원명> 유튜브 채널" + - "<병원명> 인스타그램" + - "<병원명> 후기 리뷰" +- 검색 결과에서 다음 URL 유형을 우선 확인합니다: + - 병원 공식 웹사이트 + - YouTube 채널/영상 + - Instagram 공식 계정 + - 리뷰/평점 플랫폼 (강남언니, 바비톡, RealSelf, Bookimed, 대다모, 성예사 등) + +2) 채널별 필수 수집 항목 + +유튜브: +- 공식 채널명, 채널 URL, @핸들 +- 구독자 수 (숫자 또는 범위 추정) +- 업로드 영상 수 +- 콘텐츠 유형 + +인스타그램: +- 계정 ID (@handle) — 여러 계정 가능 (국문, 영문, 원장 개인 등) +- 팔로워 수 +- 게시물 수 +- 주요 콘텐츠 유형 + +웹사이트: +- 공식 도메인 +- 언어 버전 (국문/영문/다국어) + +리뷰·후기: +- 강남언니: 평점(/10), 리뷰 수, URL +- 네이버 플레이스: 등록 여부 +- Google Maps: 평점, 리뷰 수 +- 기타 (바비톡, RealSelf 등) + +페이스북/틱톡/네이버블로그/카카오: +- 계정 URL 또는 핸들 +- 팔로워/구독자 수 (확인 가능한 경우) + +3) 응답 포맷 +반드시 아래 JSON 구조로 응답하세요. 설명 텍스트 없이 JSON만 반환하세요. + +{ + "clinicName": "병원 한국어 이름", + "clinicNameEn": "English name", + "channels": { + "instagram": [ + {"handle": "@handle", "followers": 숫자_또는_null, "posts": 숫자_또는_null, "type": "공식/영문/원장 등", "url": "URL"} + ], + "youtube": [ + {"handle": "@handle_또는_채널명", "channelUrl": "URL", "subscribers": 숫자_또는_null, "videos": 숫자_또는_null, "contentType": "설명"} + ], + "facebook": {"handle": "페이지명", "url": "URL", "followers": 숫자_또는_null}, + "tiktok": {"handle": "@handle", "url": "URL", "followers": 숫자_또는_null}, + "naverBlog": {"blogId": "ID", "url": "URL"}, + "kakao": {"channelId": "ID", "url": "URL"}, + "website": {"domain": "도메인", "languages": ["ko", "en"]} + }, + "platforms": { + "gangnamUnni": {"registered": true, "url": "URL", "rating": 숫자_또는_null, "ratingScale": 10, "reviews": 숫자_또는_null}, + "naverPlace": {"registered": true, "rating": 숫자_또는_null, "reviews": 숫자_또는_null}, + "googleMaps": {"rating": 숫자_또는_null, "reviews": 숫자_또는_null}, + "babitok": {"registered": true} + }, + "summary": "1-2문장 온라인 프레즌스 요약" +} + +4) 주의사항 +- 수치가 정확하면 숫자로, 확인 불가하면 null로 표시 +- 추측하지 말고 검색 결과에서 확인된 정보만 포함 +- 설명 텍스트 없이 JSON만 반환`; + +/** + * Build the user prompt for the research agent. + */ +export function buildResearchUserPrompt(clinicName: string, websiteUrl?: string): string { + return `"${clinicName}" 병원의 Online Presence를 위 규칙대로 분석해줘.${websiteUrl ? ` 공식 웹사이트: ${websiteUrl}` : ''}`; +} diff --git a/supabase/functions/discover-channels/index.ts b/supabase/functions/discover-channels/index.ts index bcef139..dfd6826 100644 --- a/supabase/functions/discover-channels/index.ts +++ b/supabase/functions/discover-channels/index.ts @@ -2,6 +2,7 @@ import "@supabase/functions-js/edge-runtime.d.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; import { extractSocialLinks, mergeSocialLinks } from "../_shared/extractSocialLinks.ts"; import { verifyAllHandles, type VerifiedChannels } from "../_shared/verifyHandles.ts"; +import { RESEARCH_SYSTEM_PROMPT, buildResearchUserPrompt } from "../_shared/researchPrompt.ts"; const corsHeaders = { "Access-Control-Allow-Origin": "*", @@ -283,7 +284,9 @@ Deno.serve(async (req) => { } catch { /* skip */ } })()); - // ─── B4. Perplexity: Online Presence 종합 분석 ─── + // ─── B4. Perplexity sonar-pro: Online Presence 종합 리서치 에이전트 ─── + let perplexityResearch: Record | null = null; + if (PERPLEXITY_API_KEY) { stageBTasks.push((async () => { try { @@ -291,61 +294,75 @@ Deno.serve(async (req) => { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` }, body: JSON.stringify({ - model: "sonar", + model: "sonar-pro", messages: [ - { role: "system", content: "You are a digital marketing analyst specializing in Korean medical clinics. Search the web thoroughly and provide a comprehensive online presence report. Respond ONLY with valid JSON." }, - { role: "user", content: `"${resolvedName}" 병원의 Online Presence를 종합 분석해줘. - -아래 채널들을 모두 검색해서 찾아줘: -- 인스타그램 계정 (병원 공식, 원장 개인, 영문 계정 등 여러개 있을 수 있음) -- 유튜브 채널 (메인 채널, Q&A 채널 등) -- 페이스북 페이지 -- 틱톡 계정 -- 네이버 블로그 (공식 블로그) -- 카카오톡 채널 -- 강남언니 등록 여부 및 URL -- 바비톡 등록 여부 -- 네이버 플레이스 등록 여부 - -각 채널의 핸들/URL/계정명을 정확하게 알려줘. - -JSON format: -{ - "instagram": ["handle1", "handle2", "handle3"], - "youtube": ["channel URL or @handle1", "channel2"], - "facebook": "page URL or name", - "tiktok": "@handle", - "naverBlog": "blog ID", - "kakao": "channel ID", - "gangnamUnni": {"url": "https://gangnamunni.com/hospitals/...", "registered": true}, - "naverPlace": {"registered": true}, - "babitok": {"registered": true} -}` }, + { role: "system", content: RESEARCH_SYSTEM_PROMPT }, + { role: "user", content: buildResearchUserPrompt(resolvedName, url) }, ], - temperature: 0.2, + temperature: 0.1, }), }); const data = await res.json(); let text = data.choices?.[0]?.message?.content || ""; + // Strip markdown code blocks const jsonMatch = text.match(/```(?:json)?\n?([\s\S]*?)```/); if (jsonMatch) text = jsonMatch[1]; + // Try to find JSON in mixed text + const jsonStart = text.indexOf('{'); + const jsonEnd = text.lastIndexOf('}'); + if (jsonStart >= 0 && jsonEnd > jsonStart) { + text = text.slice(jsonStart, jsonEnd + 1); + } const parsed = JSON.parse(text); + perplexityResearch = parsed; - // Extract social handles - const igArr = Array.isArray(parsed.instagram) ? parsed.instagram : parsed.instagram ? [parsed.instagram] : []; - const ytArr = Array.isArray(parsed.youtube) ? parsed.youtube : parsed.youtube ? [parsed.youtube] : []; - if (igArr.length) apiHandles.instagram!.push(...igArr); - if (ytArr.length) apiHandles.youtube!.push(...ytArr); - if (parsed.facebook) apiHandles.facebook!.push(typeof parsed.facebook === 'string' ? parsed.facebook : ''); - if (parsed.naverBlog) apiHandles.naverBlog!.push(typeof parsed.naverBlog === 'string' ? parsed.naverBlog : ''); - if (parsed.tiktok) apiHandles.tiktok!.push(typeof parsed.tiktok === 'string' ? parsed.tiktok : ''); - if (parsed.kakao) apiHandles.kakao!.push(typeof parsed.kakao === 'string' ? parsed.kakao : ''); + // Extract handles from structured channels data + const ch = parsed.channels || {}; + + // Instagram + const igAccounts = Array.isArray(ch.instagram) ? ch.instagram : []; + for (const ig of igAccounts) { + const handle = typeof ig === 'string' ? ig : (ig?.handle || ig?.url || ''); + if (handle) apiHandles.instagram!.push(String(handle)); + } + + // YouTube + const ytChannels = Array.isArray(ch.youtube) ? ch.youtube : ch.youtube ? [ch.youtube] : []; + for (const yt of ytChannels) { + const handle = typeof yt === 'string' ? yt : (yt?.handle || yt?.channelUrl || ''); + if (handle) apiHandles.youtube!.push(String(handle)); + } + + // Facebook + if (ch.facebook) { + const fb = typeof ch.facebook === 'string' ? ch.facebook : (ch.facebook?.handle || ch.facebook?.url || ''); + if (fb) apiHandles.facebook!.push(String(fb)); + } + + // TikTok + if (ch.tiktok) { + const tk = typeof ch.tiktok === 'string' ? ch.tiktok : (ch.tiktok?.handle || ch.tiktok?.url || ''); + if (tk) apiHandles.tiktok!.push(String(tk)); + } + + // Naver Blog + if (ch.naverBlog) { + const nb = typeof ch.naverBlog === 'string' ? ch.naverBlog : (ch.naverBlog?.blogId || ch.naverBlog?.url || ''); + if (nb) apiHandles.naverBlog!.push(String(nb)); + } + + // Kakao + if (ch.kakao) { + const kk = typeof ch.kakao === 'string' ? ch.kakao : (ch.kakao?.channelId || ch.kakao?.url || ''); + if (kk) apiHandles.kakao!.push(String(kk)); + } + + // Platform presence hints + const platforms = parsed.platforms || {}; + if (platforms.gangnamUnni?.url) gangnamUnniHintUrl = String(platforms.gangnamUnni.url); - // Extract platform presence hints - if (parsed.gangnamUnni?.url) gangnamUnniHintUrl = parsed.gangnamUnni.url; } catch { /* skip */ } })()); - } // ─── B5. Apify Instagram: Direct profile search by clinic name variants ─── @@ -431,6 +448,8 @@ JSON format: clinic, branding: brandData.data?.json || {}, siteLinks, siteMap: mapData.links || [], sourceUrl: url, scrapedAt: new Date().toISOString(), + // Perplexity research results — raw channel data with subscriber counts etc. + onlinePresenceResearch: perplexityResearch, }; const { data: saved, error: saveError } = await supabase