File size: 3,337 Bytes
21bdc64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# Claude Haiku๋กœ ํด๋Ÿฌ์Šคํ„ฐ์˜ ์˜๋„ยทํ…Œ๋งˆ๋ช…์„ ๋ถ„๋ฅ˜ํ•˜๊ณ  ๋งˆ์ผ€ํŒ… ์ œ์•ˆ๊นŒ์ง€ ํ•œ ๋ฒˆ์— ์ƒ์„ฑํ•˜๋Š” ๋ชจ๋“ˆ
import os
import json

import anthropic

MODEL = "claude-haiku-4-5"
USD_TO_KRW = 1400  # ๋น„์šฉ ํ™˜์‚ฐ์šฉ ๋Œ€๋žต๊ฐ’

INTENT_LABELS = {
    "info": "์ •๋ณด ํƒ์ƒ‰ํ˜•",
    "transactional": "๊ตฌ๋งค/๊ฑฐ๋ž˜ํ˜•",
    "navigational": "๋ธŒ๋žœ๋“œ/๋‚ด๋น„๊ฒŒ์ด์…˜ํ˜•",
    "mixed": "ํ˜ผํ•ฉํ˜•",
}

SYSTEM = """๋„ˆ๋Š” ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ ํด๋Ÿฌ์Šคํ„ฐ๋ฅผ ๋ถ„์„ํ•˜๋Š” ๋งˆ์ผ€ํŒ… ๋ถ„์„๊ฐ€๋‹ค. ๊ฐ ํด๋Ÿฌ์Šคํ„ฐ์— ๋Œ€ํ•ด ๋‘ ๊ฐ€์ง€๋ฅผ ํŒ๋‹จํ•œ๋‹ค.
1. intent โ€” ๊ฒ€์ƒ‰ ์˜๋„๋ฅผ ์ •ํ™•ํžˆ ํ•˜๋‚˜๋กœ ๋ถ„๋ฅ˜
   - info: ๋ฐฉ๋ฒ•ยท์ถ”์ฒœยท๋น„๊ตยทํ›„๊ธฐยท๊ฐ€์ด๋“œ ๋“ฑ ์ •๋ณด ํƒ์ƒ‰
   - transactional: ๊ตฌ๋งคยท๊ฐ€๊ฒฉยท์ตœ์ €๊ฐ€ยท์ฃผ๋ฌธ ๋“ฑ ๊ฑฐ๋ž˜
   - navigational: ํŠน์ • ๋ธŒ๋žœ๋“œยท์‚ฌ์ดํŠธ๋กœ ์ด๋™
   - mixed: ์œ„๊ฐ€ ์„ž์—ฌ ํ•˜๋‚˜๋กœ ๋‹จ์ •ํ•˜๊ธฐ ์–ด๋ ค์›€
2. theme โ€” ํด๋Ÿฌ์Šคํ„ฐ๋ฅผ ๋Œ€ํ‘œํ•˜๋Š” 2~6์–ด์ ˆ์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ตญ์–ด ํ…Œ๋งˆ ๊ทธ๋ฃน๋ช… (์˜ˆ: "์บ ํ•‘ ๊ฐ€๊ตฌ", "์ดˆ๋ณด ์ž…๋ฌธ ๊ฐ€์ด๋“œ", "๋™๊ณ„ ๋‚œ๋ฐฉ")
๊ทธ๋ฆฌ๊ณ  ์ „์ฒด ํด๋Ÿฌ์Šคํ„ฐ ํŒจํ„ด์„ ๋ณด๊ณ  ์ค‘์†Œ๊ธฐ์—…(SMB)์ด ๋ฐ”๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๋งˆ์ผ€ํŒ… ์•ก์…˜ ์ œ์•ˆ 1๊ฐœ๋ฅผ 2~3๋ฌธ์žฅ์œผ๋กœ ๊ตฌ์ฒด์ ์œผ๋กœ ์ž‘์„ฑํ•œ๋‹ค.
๋ฐ˜๋“œ์‹œ JSON๋งŒ ์ถœ๋ ฅํ•œ๋‹ค. ํ˜•์‹:
{"clusters":[{"cluster_id":0,"intent":"transactional","theme":"์บ ํ•‘ ๊ฐ€๊ตฌ"}],"marketing_suggestion":"..."}"""


def _client() -> anthropic.Anthropic:
    return anthropic.Anthropic()  # ANTHROPIC_API_KEY๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๋กœ๋“œ


def classify_clusters(clusters: list[dict]) -> dict:
    # ํด๋Ÿฌ์Šคํ„ฐ์— intent/intent_label/theme๋ฅผ ์ฑ„์šฐ๊ณ , ๋„๋„› ๋น„์ค‘ยท๋งˆ์ผ€ํŒ… ์ œ์•ˆยทํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰์„ ๋ฐ˜ํ™˜
    if not clusters:
        return {"intent_breakdown": {}, "marketing_suggestion": None, "usage": None}

    listing = "\n".join(
        f"{c['cluster_id']}: {c['cluster_label']} โ€” {', '.join(c['top_keywords'])}"
        for c in clusters
    )
    res = _client().messages.create(
        model=MODEL,
        max_tokens=1500,
        system=SYSTEM,
        messages=[{"role": "user", "content": f"๋‹ค์Œ ํด๋Ÿฌ์Šคํ„ฐ๋ฅผ ๋ถ„์„ํ•ด๋ผ.\n{listing}"}],
    )
    text = next(b.text for b in res.content if b.type == "text").strip()
    if text.startswith("```"):
        text = text.split("```")[1].lstrip("json").strip()
    parsed = json.loads(text)
    by_id = {item["cluster_id"]: item for item in parsed.get("clusters", [])}

    for c in clusters:
        item = by_id.get(c["cluster_id"], {})
        intent = item.get("intent", "mixed")
        if intent not in INTENT_LABELS:
            intent = "mixed"
        c["intent"] = intent
        c["intent_label"] = INTENT_LABELS[intent]
        c["theme"] = item.get("theme") or c["cluster_label"]  # ํ…Œ๋งˆ๋ช…, ์—†์œผ๋ฉด ๋Œ€ํ‘œ ํ‚ค์›Œ๋“œ

    breakdown: dict[str, int] = {}
    for c in clusters:
        breakdown[c["intent"]] = breakdown.get(c["intent"], 0) + c["total_search_volume"]

    usd = res.usage.input_tokens / 1e6 * 1 + res.usage.output_tokens / 1e6 * 5
    return {
        "intent_breakdown": breakdown,
        "marketing_suggestion": parsed.get("marketing_suggestion"),
        "usage": {
            "input_tokens": res.usage.input_tokens,
            "output_tokens": res.usage.output_tokens,
            "cost_usd": round(usd, 5),
            "cost_krw": round(usd * USD_TO_KRW, 1),
        },
    }