feat: Perplexity sonar-pro research agent with structured online presence analysis

Replaced simple "find handles" prompt with comprehensive research agent:
- Model: sonar → sonar-pro (advanced multi-step web search)
- System prompt: full research methodology with 2-3 keyword searches,
  URL fetching, quantitative data extraction
- Output: structured JSON with channels (handles + follower counts +
  subscriber counts) + platforms (강남언니 rating, reviews)
- Research results saved to scrape_data.onlinePresenceResearch for
  downstream use in collect-channel-data and generate-report

Added _shared/researchPrompt.ts with prompt template + builder.
Updated agent documentation in doc/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude/bold-hawking
Haewon Kam 2026-04-04 01:31:00 +09:00
parent c74832d764
commit e64d168d34
3 changed files with 389 additions and 43 deletions

234
doc/AGENT_SYSTEM_PROMPTS.md Normal file
View File

@ -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 추측 무시

View File

@ -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}` : ''}`;
}

View File

@ -2,6 +2,7 @@ import "@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { extractSocialLinks, mergeSocialLinks } from "../_shared/extractSocialLinks.ts"; import { extractSocialLinks, mergeSocialLinks } from "../_shared/extractSocialLinks.ts";
import { verifyAllHandles, type VerifiedChannels } from "../_shared/verifyHandles.ts"; import { verifyAllHandles, type VerifiedChannels } from "../_shared/verifyHandles.ts";
import { RESEARCH_SYSTEM_PROMPT, buildResearchUserPrompt } from "../_shared/researchPrompt.ts";
const corsHeaders = { const corsHeaders = {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
@ -283,7 +284,9 @@ Deno.serve(async (req) => {
} catch { /* skip */ } } catch { /* skip */ }
})()); })());
// ─── B4. Perplexity: Online Presence 종합 분석 ─── // ─── B4. Perplexity sonar-pro: Online Presence 종합 리서치 에이전트 ───
let perplexityResearch: Record<string, unknown> | null = null;
if (PERPLEXITY_API_KEY) { if (PERPLEXITY_API_KEY) {
stageBTasks.push((async () => { stageBTasks.push((async () => {
try { try {
@ -291,61 +294,75 @@ Deno.serve(async (req) => {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` }, headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
body: JSON.stringify({ body: JSON.stringify({
model: "sonar", model: "sonar-pro",
messages: [ 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: "system", content: RESEARCH_SYSTEM_PROMPT },
{ role: "user", content: `"${resolvedName}" 병원의 Online Presence를 종합 분석해줘. { role: "user", content: buildResearchUserPrompt(resolvedName, url) },
:
- ( , , )
- ( , 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}
}` },
], ],
temperature: 0.2, temperature: 0.1,
}), }),
}); });
const data = await res.json(); const data = await res.json();
let text = data.choices?.[0]?.message?.content || ""; let text = data.choices?.[0]?.message?.content || "";
// Strip markdown code blocks
const jsonMatch = text.match(/```(?:json)?\n?([\s\S]*?)```/); const jsonMatch = text.match(/```(?:json)?\n?([\s\S]*?)```/);
if (jsonMatch) text = jsonMatch[1]; 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); const parsed = JSON.parse(text);
perplexityResearch = parsed;
// Extract social handles // Extract handles from structured channels data
const igArr = Array.isArray(parsed.instagram) ? parsed.instagram : parsed.instagram ? [parsed.instagram] : []; const ch = parsed.channels || {};
const ytArr = Array.isArray(parsed.youtube) ? parsed.youtube : parsed.youtube ? [parsed.youtube] : [];
if (igArr.length) apiHandles.instagram!.push(...igArr); // Instagram
if (ytArr.length) apiHandles.youtube!.push(...ytArr); const igAccounts = Array.isArray(ch.instagram) ? ch.instagram : [];
if (parsed.facebook) apiHandles.facebook!.push(typeof parsed.facebook === 'string' ? parsed.facebook : ''); for (const ig of igAccounts) {
if (parsed.naverBlog) apiHandles.naverBlog!.push(typeof parsed.naverBlog === 'string' ? parsed.naverBlog : ''); const handle = typeof ig === 'string' ? ig : (ig?.handle || ig?.url || '');
if (parsed.tiktok) apiHandles.tiktok!.push(typeof parsed.tiktok === 'string' ? parsed.tiktok : ''); if (handle) apiHandles.instagram!.push(String(handle));
if (parsed.kakao) apiHandles.kakao!.push(typeof parsed.kakao === 'string' ? parsed.kakao : ''); }
// 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 */ } } catch { /* skip */ }
})()); })());
} }
// ─── B5. Apify Instagram: Direct profile search by clinic name variants ─── // ─── B5. Apify Instagram: Direct profile search by clinic name variants ───
@ -431,6 +448,8 @@ JSON format:
clinic, branding: brandData.data?.json || {}, clinic, branding: brandData.data?.json || {},
siteLinks, siteMap: mapData.links || [], siteLinks, siteMap: mapData.links || [],
sourceUrl: url, scrapedAt: new Date().toISOString(), 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 const { data: saved, error: saveError } = await supabase