IDEA-DESIGN / app.py
ginipick's picture
Update app.py
fe49aa3 verified
raw
history blame
14.1 kB
"""
Ginigen Blog / Streamlit App
────────────────────────────────────────────────────────────────────
- 2025-04-23 : Brave Search API 버전
- SerpHouse μ „λ©΄ 제거, Brave Search API 적용
- API Key : ν™˜κ²½λ³€μˆ˜ SERPHOUSE_API_KEY (μ΄λ¦„λ§Œ κ·ΈλŒ€λ‘œ μ‚¬μš©)
────────────────────────────────────────────────────────────────────
"""
import os
import streamlit as st
import json
import anthropic
import requests
import logging
from gradio_client import Client
import markdown
import re
from datetime import datetime
# BeautifulSoupλŠ” 더 이상 μ‚¬μš©ν•˜μ§€ μ•Šμ§€λ§Œ, ν•„μš” μ‹œ μœ μ§€
# from bs4 import BeautifulSoup
# ───────────────────────────── 1) λ‘œκΉ… ─────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
# ───────────────────────────── 2) μ „μ—­ μƒμˆ˜ / API ν‚€ ───────────────────────────
ANTHROPIC_KEY = os.getenv("API_KEY", "")
BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # Brave Search API ν‚€
BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
IMAGE_API_URL = "http://211.233.58.201:7896"
MAX_TOKENS = 7_999
# ───────────────────────────── 3) ν΄λΌμ΄μ–ΈνŠΈ ──────────────────────────────────
client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
# ───────────────────────────── 4) μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ ─────────────────────────────
def get_system_prompt() -> str:
return """
당신은 μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λͺ¨λ“  λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± μš”μ²­μ— λŒ€ν•΄ λ‹€μŒμ˜ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό μ² μ €νžˆ λ”°λ₯΄λ˜, μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 글이 λ˜λ„λ‘ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€:
λ…μž μ—°κ²° 단계
1.1. κ³΅κ°λŒ€ ν˜•μ„±μ„ μœ„ν•œ μΉœκ·Όν•œ 인사
1.2. λ…μžμ˜ μ‹€μ œ 고민을 λ°˜μ˜ν•œ λ„μž… 질문
1.3. μ£Όμ œμ— λŒ€ν•œ 즉각적 관심 μœ λ„
문제 μ •μ˜ 단계
2.1. λ…μžμ˜ 페인포인트 ꡬ체화
2.2. 문제의 μ‹œκΈ‰μ„±κ³Ό 영ν–₯도 뢄석
2.3. ν•΄κ²° ν•„μš”μ„±μ— λŒ€ν•œ κ³΅κ°λŒ€ ν˜•μ„±
μ „λ¬Έμ„± μž…μ¦ 단계
3.1. 객관적 데이터 기반 뢄석
3.2. μ „λ¬Έκ°€ 견해와 연ꡬ κ²°κ³Ό 인용
3.3. μ‹€μ œ 사둀λ₯Ό ν†΅ν•œ 문제 ꡬ체화
μ†”λ£¨μ…˜ 제곡 단계
4.1. 단계별 μ‹€μ²œ κ°€μ΄λ“œλΌμΈ μ œμ‹œ
4.2. μ¦‰μ‹œ 적용 κ°€λŠ₯ν•œ ꡬ체적 팁
4.3. μ˜ˆμƒ μž₯μ• λ¬Όκ³Ό 극볡 λ°©μ•ˆ 포함
신뒰도 κ°•ν™” 단계
5.1. μ‹€μ œ 성곡 사둀 μ œμ‹œ
5.2. ꡬ체적 μ‚¬μš©μž ν›„κΈ° 인용
5.3. 객관적 λ°μ΄ν„°λ‘œ 효과 μž…μ¦
행동 μœ λ„ 단계
6.1. λͺ…ν™•ν•œ 첫 μ‹€μ²œ 단계 μ œμ‹œ
6.2. μ‹œκΈ‰μ„±μ„ κ°•μ‘°ν•œ 행동 촉ꡬ
6.3. μ‹€μ²œ 동기 λΆ€μ—¬ μš”μ†Œ 포함
μ§„μ •μ„± κ°•ν™” 단계
7.1. μ†”λ£¨μ…˜μ˜ ν•œκ³„ 투λͺ…ν•˜κ²Œ 곡개
7.2. κ°œμΈλ³„ 차이 쑴재 인정
7.3. ν•„μš” 쑰건과 μ£Όμ˜μ‚¬ν•­ λͺ…μ‹œ
관계 지속 단계
8.1. μ§„μ •μ„± μžˆλŠ” 감사 인사
8.2. λ‹€μŒ 컨텐츠 예고둜 κΈ°λŒ€κ° μ‘°μ„±
8.3. μ†Œν†΅ 채널 μ•ˆλ‚΄
μž‘μ„± μ‹œ μ€€μˆ˜μ‚¬ν•­
9.1. κΈ€μž 수: 1500-2000자 λ‚΄μ™Έ
9.2. 문단 길이: 3-4λ¬Έμž₯ 이내
9.3. μ‹œκ°μ  ꡬ뢄: μ†Œμ œλͺ©, ꡬ뢄선, 번호 λͺ©λ‘ ν™œμš©
9.4. ν†€μ•€λ§€λ„ˆ: μΉœκ·Όν•˜κ³  전문적인 λŒ€ν™”μ²΄
9.5. 데이터: λͺ¨λ“  μ •λ³΄μ˜ 좜처 λͺ…μ‹œ
9.6. 가독성: λͺ…ν™•ν•œ 단락 ꡬ뢄과 강쑰점 μ‚¬μš©
"""
# ───────────────────────────── 5) Brave Search ν•¨μˆ˜ ───────────────────────────
def brave_search(query: str, count: int = 5):
"""
Brave Web Search API 호좜 β†’ list[dict] λ°˜ν™˜
λ°˜ν™˜ ν•­λͺ©: title, link, snippet, displayed_link, index
"""
if not BRAVE_KEY:
raise RuntimeError("ν™˜κ²½λ³€μˆ˜ SERPHOUSE_API_KEY(=Brave API key)κ°€ μ„€μ •λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": BRAVE_KEY
}
params = {"q": query, "count": str(count)}
resp = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
resp.raise_for_status()
data = resp.json()
web_results = (
data.get("web", {}).get("results") or
data.get("results", [])
)
articles = []
for idx, r in enumerate(web_results[:count], 1):
url = r.get("url", r.get("link", ""))
host = re.sub(r"https?://(www\\.)?", "", url).split("/")[0]
articles.append({
"index": idx,
"title": r.get("title", "제λͺ© μ—†μŒ"),
"link": url,
"snippet": r.get("description", r.get("text", "λ‚΄μš© μ—†μŒ")),
"displayed_link": host
})
return articles
# ───────────────────────────── 6) 검색 β†’ λ§ˆν¬λ‹€μš΄ ─────────────────────────────
def generate_mock_search_results(query: str) -> str:
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
mock = [{
"title": f"{query} κ΄€λ ¨ 가상 κ²°κ³Ό",
"link": "https://example.com",
"snippet": "API 호좜 μ‹€νŒ¨λ‘œ μƒμ„±λœ μ˜ˆμ‹œ κ²°κ³Όμž…λ‹ˆλ‹€.",
"displayed_link": "example.com"
}]
body = "\n".join(
f"### Result {i+1}: {m['title']}\n\n{m['snippet']}\n\n"
f"**좜처**: [{m['displayed_link']}]({m['link']})\n\n---\n"
for i, m in enumerate(mock)
)
return f"# 가상 검색 κ²°κ³Ό (생성: {ts})\n\n{body}"
def do_web_search(query: str) -> str:
"""
Brave Search μ „μš© 검색 ν•¨μˆ˜.
μ‹€νŒ¨ν•˜κ±°λ‚˜ μΏΌν„° 초과 μ‹œ mock κ²°κ³Ό λ°˜ν™˜.
"""
try:
articles = brave_search(query, count=5)
except Exception as e:
logging.error(f"Brave 검색 μ‹€νŒ¨: {e}")
return generate_mock_search_results(query)
if not articles:
return generate_mock_search_results(query)
md_lines = []
for a in articles:
md_lines.append(
f"### Result {a['index']}: {a['title']}\n\n"
f"{a['snippet']}\n\n"
f"**좜처**: [{a['displayed_link']}]({a['link']})\n\n---\n"
)
header = (
"# μ›Ή 검색 κ²°κ³Ό\n"
"μ•„λž˜ 정보λ₯Ό 닡변에 ν™œμš©ν•˜μ„Έμš”: 좜처 인용·링크 ν¬ν•¨Β·λ‹€μˆ˜ 좜처 μ’…ν•©\n\n"
)
return header + "".join(md_lines)
# ───────────────────────────── 7) 이미지·MD λ³€ν™˜ λ“± μœ ν‹Έ ───────────────────────
def test_image_api_connection():
try:
Client(IMAGE_API_URL)
return "이미지 API μ—°κ²° 성곡"
except Exception as e:
logging.error(e)
return f"이미지 API μ—°κ²° μ‹€νŒ¨: {e}"
def generate_image(prompt, width=768, height=768, guidance=3.5,
inference_steps=30, seed=3):
if not prompt:
return None, "ν”„λ‘¬ν”„νŠΈ λΆ€μ‘±"
try:
c = Client(IMAGE_API_URL)
res = c.predict(
prompt=prompt, width=width, height=height,
guidance=guidance, inference_steps=inference_steps,
seed=seed, do_img2img=False, init_image=None,
image2image_strength=0.8, resize_img=True,
api_name="/generate_image"
)
return res[0], f"Seed: {res[1]}"
except Exception as e:
logging.error(e)
return None, str(e)
def extract_image_prompt(blog_content, blog_topic):
system = f"λ‹€μŒ 글을 λ°”νƒ•μœΌλ‘œ μ μ ˆν•œ 이미지 ν”„λ‘¬ν”„νŠΈλ₯Ό μ˜μ–΄λ‘œ ν•œ μ€„λ§Œ 써쀘:\n{blog_topic}"
try:
res = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=80,
system=system,
messages=[{"role": "user", "content": blog_content}]
)
return res.content[0].text.strip()
except Exception:
return f"A professional photo related to {blog_topic}, high quality"
def convert_md_to_html(md_text, title="Ginigen Blog"):
body = markdown.markdown(md_text)
return f"""<!DOCTYPE html><html><head>
<title>{title}</title><meta charset="utf-8"></head><body>{body}</body></html>"""
def extract_keywords(text: str, k: int = 5) -> str:
txt = re.sub(r"[^κ°€-힣a-zA-Z0-9\\s]", "", text)
return " ".join(txt.split()[:k])
# ───────────────────────────── 8) Streamlit UI ────────────────────────────────
def chatbot_interface():
st.title("Ginigen Blog")
# μ„Έμ…˜ μƒνƒœ μ΄ˆκΈ°ν™”
defaults = {
"ai_model": "claude-3-7-sonnet-20250219",
"messages": [],
"auto_save": True,
"generate_image": False,
"use_web_search": False,
"image_api_status": test_image_api_connection()
}
for k, v in defaults.items():
if k not in st.session_state:
st.session_state[k] = v
sb = st.sidebar
sb.title("λŒ€ν™” 기둝 관리")
sb.toggle("μžλ™ μ €μž₯", key="auto_save")
sb.toggle("λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± ν›„ 이미지 μžλ™ 생성", key="generate_image")
sb.toggle("주제 μ›Ή 검색 및 뢄석", key="use_web_search")
sb.text(st.session_state.image_api_status)
# κΈ°μ‘΄ λ©”μ‹œμ§€ λ Œλ”λ§
for m in st.session_state.messages:
with st.chat_message(m["role"]):
st.markdown(m["content"])
if "image" in m:
st.image(m["image"], caption=m.get("image_caption", ""))
# μ‚¬μš©μž μž…λ ₯
if prompt := st.chat_input("무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
placeholder = st.empty()
full_resp = ""
sys_prompt = get_system_prompt()
# (선택) Brave 검색
if st.session_state.use_web_search:
with st.spinner("μ›Ή 검색 쀑…"):
q = extract_keywords(prompt)
sb.info(f"검색어: {q}")
search_md = do_web_search(q)
if "가상 검색 κ²°κ³Ό" in search_md:
sb.warning("μ‹€μ œ 검색 κ²°κ³Όλ₯Ό κ°€μ Έμ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.")
sys_prompt += f"\n\n검색 κ²°κ³Ό:\n{search_md}\n"
# Claude 슀트리밍
with client.messages.stream(
model=st.session_state.ai_model,
max_tokens=MAX_TOKENS,
system=sys_prompt,
messages=[{"role": m["role"], "content": m["content"]}
for m in st.session_state.messages]
) as stream:
for t in stream.text_stream:
full_resp += t or ""
placeholder.markdown(full_resp + "β–Œ")
placeholder.markdown(full_resp)
# (선택) 이미지 생성
if st.session_state.generate_image:
with st.spinner("이미지 생성 쀑…"):
img_prompt = extract_image_prompt(full_resp, prompt)
img, caption = generate_image(img_prompt)
if img:
st.image(img, caption=caption)
st.session_state.messages.append(
{"role": "assistant", "content": full_resp,
"image": img, "image_caption": caption}
)
else:
st.error(f"이미지 생성 μ‹€νŒ¨: {caption}")
st.session_state.messages.append(
{"role": "assistant", "content": full_resp}
)
else:
st.session_state.messages.append(
{"role": "assistant", "content": full_resp}
)
# λ‹€μš΄λ‘œλ“œ λ²„νŠΌ
st.subheader("이 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ:")
c1, c2 = st.columns(2)
c1.download_button("λ§ˆν¬λ‹€μš΄", full_resp,
file_name=f"{prompt[:30]}.md", mime="text/markdown")
html = convert_md_to_html(full_resp, prompt[:30])
c2.download_button("HTML", html,
file_name=f"{prompt[:30]}.html", mime="text/html")
# μžλ™ μ €μž₯
if st.session_state.auto_save and st.session_state.messages:
try:
fname = f"chat_history_{datetime.now():%Y%m%d_%H%M%S}.json"
with open(fname, "w", encoding="utf-8") as f:
json.dump(st.session_state.messages, f, ensure_ascii=False, indent=2)
except Exception as e:
sb.error(f"μžλ™ μ €μž₯ 였λ₯˜: {e}")
# ───────────────────────────── 9) main ────────────────────────────────────────
def main():
chatbot_interface()
if __name__ == "__main__":
# requirements.txt 생성
with open("requirements.txt", "w") as f:
f.write("\n".join([
"streamlit>=1.31.0",
"anthropic>=0.18.1",
"gradio-client>=1.8.0",
"requests>=2.32.3",
"markdown>=3.5.1",
"pillow>=10.1.0"
]))
main()