MarketSync / tools /marketing_strategy.py
hyeonjoo's picture
Initial project commit with LFS
9b1e3db
# tools/marketing_strategy.py
import traceback
import json
from typing import List
from langchain_core.tools import tool
import config
from modules.llm_provider import get_llm
from modules.knowledge_base import load_marketing_vectorstore
from tools.profile_analyzer import get_festival_profile_by_name
logger = config.get_logger(__name__)
@tool
def search_contextual_marketing_strategy(user_query: str, store_profile: str) -> str:
"""
(RAG Tool) μ‚¬μš©μžμ˜ 질문과 κ°€κ²Œ ν”„λ‘œν•„(JSON λ¬Έμžμ—΄)을 λ°”νƒ•μœΌλ‘œ 'λ§ˆμΌ€νŒ… μ „λž΅' Vector DBμ—μ„œ
관련성이 높은 μ»¨ν…μŠ€νŠΈ(μ „λž΅)λ₯Ό κ²€μƒ‰ν•˜κ³ , LLM을 톡해 μ΅œμ’… 닡변을 μƒμ„±ν•˜μ—¬ λ°˜ν™˜ν•©λ‹ˆλ‹€.
"""
logger.info("--- [Tool] RAG λ§ˆμΌ€νŒ… μ „λž΅ 검색 호좜됨 ---")
try:
retriever = load_marketing_vectorstore()
if retriever is None:
raise RuntimeError("λ§ˆμΌ€νŒ… Retrieverκ°€ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
# 1. μ»¨ν…μŠ€νŠΈλ₯Ό κ³ λ €ν•œ 검색 쿼리 생성
try:
profile_dict = json.loads(store_profile)
profile_for_query = (
f"κ°€κ²Œ μœ„μΉ˜: {profile_dict.get('μ£Όμ†Œ', 'μ•Œ 수 μ—†μŒ')}\n"
f"κ°€κ²Œ μ—…μ’…: {profile_dict.get('μ—…μ’…', 'μ•Œ 수 μ—†μŒ')}\n"
f"핡심 고객: {profile_dict.get('μžλ™μΆ”μΆœνŠΉμ§•', {}).get('핡심고객', 'μ•Œ 수 μ—†μŒ')}"
)
except Exception:
profile_for_query = store_profile
contextual_query = f"[κ°€κ²Œ 정보:\n{profile_for_query}\n]에 λŒ€ν•œ [질문: {user_query}]"
logger.info(f"--- [Tool] RAG 검색 쿼리: {contextual_query} ---")
# 2. Vector DB 검색
docs = retriever.invoke(contextual_query)
if not docs:
logger.warning("--- [Tool] RAG 검색 κ²°κ³Ό μ—†μŒ ---")
return "μ£„μ†‘ν•©λ‹ˆλ‹€. 사μž₯λ‹˜μ˜ κ°€κ²Œ ν”„λ‘œν•„κ³Ό μ§ˆλ¬Έμ— λ§žλŠ” λ§ˆμΌ€νŒ… μ „λž΅μ„ μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€. κ°€κ²Œμ˜ νŠΉμ§•μ„ 쑰금 더 μ•Œλ €μ£Όμ‹œκ±°λ‚˜, λ‹€λ₯Έ μ§ˆλ¬Έμ„ μ‹œλ„ν•΄λ³΄μ‹œκ² μ–΄μš”?"
# 3. LLM에 전달할 μ»¨ν…μŠ€νŠΈ ν¬λ§·νŒ…
context = "\n\n---\n\n".join([doc.page_content for doc in docs])
logger.info("--- [Tool] RAG μ»¨ν…μŠ€νŠΈ 생성 μ™„λ£Œ ---")
# 4. LLM을 ν†΅ν•œ λ‹΅λ³€ μž¬κ΅¬μ„±
llm = get_llm(temperature=0.3)
# --- (μ‚¬μš©μž μš”μ²­) ν”„λ‘¬ν”„νŠΈ 원본 μœ μ§€ ---
prompt = f"""
당신은 μ†Œμƒκ³΅μΈ μ „λ¬Έ λ§ˆμΌ€νŒ… μ»¨μ„€ν„΄νŠΈμž…λ‹ˆλ‹€.
μ•„λž˜ [κ°€κ²Œ ν”„λ‘œν•„]κ³Ό [μ°Έκ³  λ§ˆμΌ€νŒ… μ „λž΅]을 λ°”νƒ•μœΌλ‘œ, μ‚¬μš©μžμ˜ [질문]에 λŒ€ν•œ λ§žμΆ€ν˜• λ§ˆμΌ€νŒ… μ „λž΅ 3κ°€μ§€λ₯Ό μ œμ•ˆν•΄μ£Όμ„Έμš”.
[κ°€κ²Œ ν”„λ‘œν•„]
{store_profile}
[질문]
{user_query}
[μ°Έκ³  λ§ˆμΌ€νŒ… μ „λž΅]
{context}
[μž‘μ„± κ°€μ΄λ“œλΌμΈ]
1. [μ°Έκ³  λ§ˆμΌ€νŒ… μ „λž΅]을 κ·ΈλŒ€λ‘œ λ³΅μ‚¬ν•˜μ§€ 말고, [κ°€κ²Œ ν”„λ‘œν•„]의 νŠΉμ§•(예: μ—…μ’…, 핡심 고객, μƒκΆŒ)κ³Ό [질문]의 μ˜λ„λ₯Ό μ‘°ν•©ν•˜μ—¬ **κ°€κ²Œμ— νŠΉν™”λœ μƒˆλ‘œμš΄ 아이디어**둜 μž¬κ΅¬μ„±ν•΄μ£Όμ„Έμš”.
2. 각 μ „λž΅μ€ ꡬ체적인 μ‹€ν–‰ λ°©μ•ˆμ„ 포함해야 ν•©λ‹ˆλ‹€.
3. μΉœμ ˆν•˜κ³  전문적인 말투λ₯Ό μ‚¬μš©ν•˜μ„Έμš”.
4. μ•„λž˜ [좜λ ₯ ν˜•μ‹]을 μ •ν™•νžˆ μ§€μΌœμ£Όμ„Έμš”.
5. **μ·¨μ†Œμ„  κΈˆμ§€**: μ ˆλŒ€λ‘œ `~~text~~`와 같은 μ·¨μ†Œμ„  λ§ˆν¬λ‹€μš΄μ„ μ‚¬μš©ν•˜μ§€ λ§ˆμ„Έμš”.
[좜λ ₯ ν˜•μ‹]
사μž₯λ‹˜ κ°€κ²Œμ˜ νŠΉμ„±μ„ κ³ λ €ν•œ 3κ°€μ§€ λ§ˆμΌ€νŒ… 아이디어λ₯Ό μ œμ•ˆν•΄ λ“œλ¦½λ‹ˆλ‹€.
**1. [μ „λž΅ 제λͺ© 1]**
* **μ „λž΅ λ‚΄μš©:** (κ°€κ²Œμ˜ μ–΄λ–€ νŠΉμ§•μ„ ν™œμš©ν•˜μ—¬ μ–΄λ–»κ²Œ μ‹€ν–‰ν•˜λŠ”μ§€ ꡬ체적으둜 μ„œμˆ )
* **κΈ°λŒ€ 효과:** (이 μ „λž΅μ„ 톡해 얻을 수 μžˆλŠ” ꡬ체적인 효과)
**2. [μ „λž΅ 제λͺ© 2]**
* **μ „λž΅ λ‚΄μš©:** (κ°€κ²Œμ˜ μ–΄λ–€ νŠΉμ§•μ„ ν™œμš©ν•˜μ—¬ μ–΄λ–»κ²Œ μ‹€ν–‰ν•˜λŠ”μ§€ ꡬ체적으둜 μ„œμˆ )
* **κΈ°λŒ€ 효과:** (이 μ „λž΅μ„ 톡해 얻을 수 μžˆλŠ” ꡬ체적인 효과)
**3. [μ „λž΅ 제λͺ© 3]**
* **μ „λž΅ λ‚΄μš©:** (κ°€κ²Œμ˜ μ–΄λ–€ νŠΉμ§•μ„ ν™œμš©ν•˜μ—¬ μ–΄λ–»κ²Œ μ‹€ν–‰ν•˜λŠ”μ§€ ꡬ체적으둜 μ„œμˆ )
* **κΈ°λŒ€ 효과:** (이 μ „λž΅μ„ 톡해 얻을 수 μžˆλŠ” ꡬ체적인 효과)
"""
try:
response = llm.invoke(prompt)
logger.info("--- [Tool] RAG + LLM λ‹΅λ³€ 생성 μ™„λ£Œ ---")
return response.content
except Exception as llm_e:
logger.critical(f"--- [Tool CRITICAL] RAG LLM 호좜 쀑 였λ₯˜: {llm_e} ---", exc_info=True)
return f"였λ₯˜: κ²€μƒ‰λœ μ „λž΅μ„ μ²˜λ¦¬ν•˜λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. (LLM 였λ₯˜: {llm_e})"
except Exception as e:
logger.critical(f"--- [Tool CRITICAL] RAG λ§ˆμΌ€νŒ… μ „λž΅ 검색 쀑 였λ₯˜: {e} ---", exc_info=True)
return f"μ£„μ†‘ν•©λ‹ˆλ‹€. λ§ˆμΌ€νŒ… μ „λž΅μ„ μƒμ„±ν•˜λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e}"
@tool
def create_festival_specific_marketing_strategy(festival_name: str, store_profile: str) -> str:
"""
(RAG x2 Tool) νŠΉμ • μΆ•μ œ 이름(예: 'κ΄€μ•…κ°•κ°μ°¬μΆ•μ œ')κ³Ό κ°€κ²Œ ν”„λ‘œν•„(JSON λ¬Έμžμ—΄)을 μž…λ ₯λ°›μ•„,
'μΆ•μ œ DB'와 'λ§ˆμΌ€νŒ… DB'λ₯Ό *λ™μ‹œμ—* RAG둜 μ°Έμ‘°ν•˜μ—¬,
ν•΄λ‹Ή μΆ•μ œ κΈ°κ°„ λ™μ•ˆ μ‹€ν–‰ν•  수 μžˆλŠ” λ§žμΆ€ν˜• λ§ˆμΌ€νŒ… μ „λž΅ *1개*λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
"""
logger.info(f"--- [Tool] '*단일* μΆ•μ œ λ§žμΆ€ν˜• μ „λž΅ 생성 (RAGx2)' 도ꡬ 호좜 (λŒ€μƒ: {festival_name}) ---")
try:
# 1. (RAG 1) μΆ•μ œ 정보 κ°€μ Έμ˜€κΈ° (κΈ°μ‘΄ 도ꡬ μž¬μ‚¬μš©)
festival_profile_str = get_festival_profile_by_name.invoke({"festival_name": festival_name})
if "였λ₯˜" in festival_profile_str or "찾을 수 μ—†μŒ" in festival_profile_str:
logger.warning(f"--- [Tool WARNING] μΆ•μ œ ν”„λ‘œν•„μ„ μ°Ύμ§€ λͺ»ν•¨: {festival_name} ---")
festival_profile_str = f"{{\"μΆ•μ œλͺ…\": \"{festival_name}\", \"정보\": \"상세 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.\"}}"
else:
logger.info(f"--- [Tool] (RAG 1) μΆ•μ œ ν”„λ‘œν•„ λ‘œλ“œ 성곡: {festival_name} ---")
# 2. (RAG 2) κ΄€λ ¨ λ§ˆμΌ€νŒ… μ „λž΅ 검색
marketing_retriever = load_marketing_vectorstore()
if marketing_retriever is None:
raise RuntimeError("λ§ˆμΌ€νŒ… Retrieverκ°€ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
combined_query = f"""
μΆ•μ œ 정보: {festival_profile_str}
κ°€κ²Œ ν”„λ‘œν•„: {store_profile}
질문: μœ„ κ°€κ²Œκ°€ μœ„ μΆ•μ œ κΈ°κ°„ λ™μ•ˆ ν•  수 μžˆλŠ” 졜고의 λ§ˆμΌ€νŒ… μ „λž΅μ€?
"""
marketing_docs = marketing_retriever.invoke(combined_query)
if not marketing_docs:
marketing_context = "μ°Έκ³ ν•  λ§Œν•œ λ§ˆμΌ€νŒ… μ „λž΅μ„ μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."
logger.warning("--- [Tool] (RAG 2) λ§ˆμΌ€νŒ… μ „λž΅ 검색 κ²°κ³Ό μ—†μŒ ---")
else:
marketing_context = "\n\n---\n\n".join([doc.page_content for doc in marketing_docs])
logger.info(f"--- [Tool] (RAG 2) λ§ˆμΌ€νŒ… μ „λž΅ μ»¨ν…μŠ€νŠΈ {len(marketing_docs)}개 확보 ---")
# 3. LLM을 ν†΅ν•œ μ΅œμ’… μ „λž΅ 생성
llm = get_llm(temperature=0.5)
# --- (μ‚¬μš©μž μš”μ²­) ν”„λ‘¬ν”„νŠΈ 원본 μœ μ§€ ---
prompt = f"""
당신은 μΆ•μ œ 연계 λ§ˆμΌ€νŒ… μ „λ¬Έ μ»¨μ„€ν„΄νŠΈμž…λ‹ˆλ‹€.
μ•„λž˜ [κ°€κ²Œ ν”„λ‘œν•„], [μΆ•μ œ ν”„λ‘œν•„], [μ°Έκ³  λ§ˆμΌ€νŒ… μ „λž΅]을 λͺ¨λ‘ κ³ λ €ν•˜μ—¬,
[κ°€κ²Œ ν”„λ‘œν•„]의 사μž₯λ‹˜μ΄ [μΆ•μ œ ν”„λ‘œν•„] κΈ°κ°„ λ™μ•ˆ μ‹€ν–‰ν•  수 μžˆλŠ”
**창의적이고 ꡬ체적인 λ§žμΆ€ν˜• λ§ˆμΌ€νŒ… μ „λž΅ 1κ°€μ§€**λ₯Ό μ œμ•ˆν•΄μ£Όμ„Έμš”.
[κ°€κ²Œ ν”„λ‘œν•„]
{store_profile}
[μΆ•μ œ ν”„λ‘œν•„]
{festival_profile_str}
[μ°Έκ³  λ§ˆμΌ€νŒ… μ „λž΅]
{marketing_context}
[μž‘μ„± κ°€μ΄λ“œλΌμΈ]
1. **맀우 μ€‘μš”:** [κ°€κ²Œ ν”„λ‘œν•„]의 νŠΉμ§•(μ—…μ’…, μœ„μΉ˜, 핡심 고객)κ³Ό [μΆ•μ œ ν”„λ‘œν•„]의 νŠΉμ§•(주제, μ£Όμš” 방문객)을
**λ°˜λ“œμ‹œ μ—°κ΄€μ§€μ–΄** ꡬ체적인 μ „λž΅μ„ λ§Œλ“œμ„Έμš”.
2. [μ°Έκ³  λ§ˆμΌ€νŒ… μ „λž΅]은 아이디어 λ°œμƒμ—λ§Œ ν™œμš©ν•˜κ³ , λ³΅μ‚¬ν•˜μ§€ λ§ˆμ„Έμš”.
3. μ „λž΅μ€ 1κ°€μ§€λ§Œ 깊이 있게 μ œμ•ˆν•©λ‹ˆλ‹€.
4. μΉœμ ˆν•˜κ³  전문적인 말투λ₯Ό μ‚¬μš©ν•˜μ„Έμš”.
5. μ•„λž˜ [좜λ ₯ ν˜•μ‹]을 μ •ν™•νžˆ μ§€μΌœμ£Όμ„Έμš”.
6. **μ·¨μ†Œμ„  κΈˆμ§€**: μ ˆλŒ€λ‘œ `~~text~~`와 같은 μ·¨μ†Œμ„  λ§ˆν¬λ‹€μš΄μ„ μ‚¬μš©ν•˜μ§€ λ§ˆμ„Έμš”.
[좜λ ₯ ν˜•μ‹]
### 🎈 {json.loads(festival_profile_str).get('μΆ•μ œλͺ…', festival_name)} λ§žμΆ€ν˜• λ§ˆμΌ€νŒ… μ „λž΅
**1. (μ „λž΅ 아이디어 제λͺ©)**
* **μ „λž΅ κ°œμš”:** (κ°€κ²Œμ˜ μ–΄λ–€ νŠΉμ§•κ³Ό μΆ•μ œμ˜ μ–΄λ–€ νŠΉμ§•μ„ μ—°κ΄€μ§€μ—ˆλŠ”μ§€ μ„€λͺ…)
* **ꡬ체적 μ‹€ν–‰ λ°©μ•ˆ:** (사μž₯λ‹˜μ΄ '무엇을', 'μ–΄λ–»κ²Œ' ν•΄μ•Ό ν•˜λŠ”μ§€ λ‹¨κ³„λ³„λ‘œ μ„€λͺ…. 예: 메뉴 개발, 홍보 문ꡬ, SNS 이벀트 λ“±)
* **νƒ€κ²Ÿ 고객:** (이 μ „λž΅μ΄ μΆ•μ œ 방문객 쀑 λˆ„κ΅¬μ—κ²Œ λ§€λ ₯적일지)
* **κΈ°λŒ€ 효과:** (μ˜ˆμƒλ˜λŠ” κ²°κ³Ό, 예: μ‹ κ·œ 고객 μœ μž…, 객단가 μƒμŠΉ λ“±)
"""
try:
response = llm.invoke(prompt)
logger.info("--- [Tool] (RAGx2) μ΅œμ’… μ „λž΅ 생성 μ™„λ£Œ ---")
return response.content
except Exception as llm_e:
logger.critical(f"--- [Tool CRITICAL] 'μΆ•μ œ λ§žμΆ€ν˜• μ „λž΅ 생성 (RAGx2)' LLM 호좜 쀑 였λ₯˜: {llm_e} ---", exc_info=True)
return f"였λ₯˜: κ²€μƒ‰λœ μ „λž΅μ„ μ²˜λ¦¬ν•˜λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. (LLM 였λ₯˜: {llm_e})"
except Exception as e:
logger.critical(f"--- [Tool CRITICAL] 'μΆ•μ œ λ§žμΆ€ν˜• μ „λž΅ 생성 (RAG)' 쀑 였λ₯˜: {e} ---", exc_info=True)
return f"μ£„μ†‘ν•©λ‹ˆλ‹€. '{festival_name}' μΆ•μ œ μ „λž΅μ„ μƒμ„±ν•˜λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e}"
@tool
def create_marketing_strategies_for_multiple_festivals(festival_names: List[str], store_profile: str) -> str:
"""
μ—¬λŸ¬ 개의 μΆ•μ œ 이름 λ¦¬μŠ€νŠΈμ™€ κ°€κ²Œ ν”„λ‘œν•„(JSON λ¬Έμžμ—΄)을 μž…λ ₯λ°›μ•„,
각 μΆ•μ œμ— νŠΉν™”λœ λ§žμΆ€ν˜• λ§ˆμΌ€νŒ… μ „λž΅μ„ *λͺ¨λ‘* μƒμ„±ν•˜κ³  ν•˜λ‚˜μ˜ λ¬Έμžμ—΄λ‘œ μ·¨ν•©ν•˜μ—¬ λ°˜ν™˜ν•©λ‹ˆλ‹€.
(예: ["μ²­μ†‘μ‚¬κ³ΌμΆ•μ œ", "λΆ€μ²œκ΅­μ œλ§Œν™”μΆ•μ œ"])
"""
logger.info(f"--- [Tool] '*λ‹€μˆ˜* μΆ•μ œ λ§žμΆ€ν˜• μ „λž΅ 생성' 도ꡬ 호좜 (λŒ€μƒ: {festival_names}) ---")
final_report = []
if not festival_names:
logger.warning("--- [Tool] μΆ•μ œ 이름 λͺ©λ‘μ΄ λΉ„μ–΄μžˆμŒ ---")
return "였λ₯˜: μΆ•μ œ 이름 λͺ©λ‘μ΄ λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€. μ „λž΅μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€."
# κ°œλ³„ μ „λž΅ 생성 도ꡬλ₯Ό μž¬μ‚¬μš©
for festival_name in festival_names:
try:
strategy = create_festival_specific_marketing_strategy.invoke({
"festival_name": festival_name,
"store_profile": store_profile
})
final_report.append(strategy)
except Exception as e:
error_message = f"--- [였λ₯˜] '{festival_name}'의 μ „λž΅ 생성 쀑 λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e} ---"
logger.critical(f"--- [Tool CRITICAL] '{festival_name}' μ „λž΅ 생성 쀑 였λ₯˜: {e} ---", exc_info=True)
final_report.append(error_message)
logger.info("--- [Tool] 'λ‹€μˆ˜ μΆ•μ œ λ§žμΆ€ν˜• μ „λž΅ 생성' μ™„λ£Œ ---")
return "\n\n---\n\n".join(final_report)