IDEA-DESIGN / app.py
ginipick's picture
Update app.py
c99406b verified
raw
history blame
13.9 kB
"""
Ginigen Blog / Streamlit App — Brave Search API Edition
────────────────────────────────────────────────────────────────────────
* 2025-04-23 : SerpHouse μ˜μ‘΄μ„± β‡’ Brave Search API 둜 μ „λ©΄ ꡐ체
* ν™˜κ²½λ³€μˆ˜ SERPHOUSE_API_KEY β†’ Brave API Key κ·ΈλŒ€λ‘œ μ‚¬μš©
* **원본 μ½”λ“œμ˜ κΈ°λŠ₯ 100 % μœ μ§€**
- Markdown / HTML λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ (μ‚¬μ΄λ“œλ°” + λ³Έλ¬Έ)
- λŒ€ν™” 기둝 JSON μ—…λ‘œλ“œ & λ‹€μš΄λ‘œλ“œ + λ°±κ·ΈλΌμš΄λ“œ μžλ™ μ €μž₯
- 이미지 μžλ™ 생성 μ˜΅μ…˜
- Streamlit λͺ¨λ“  UI ν† κΈ€
────────────────────────────────────────────────────────────────────────
"""
# ──────────────────────────────── Imports ────────────────────────────────
import os, json, re, logging, requests, markdown
from datetime import datetime
import streamlit as st
import anthropic
from gradio_client import Client
# from bs4 import BeautifulSoup # ν•„μš” μ‹œ 주석 ν•΄μ œ
# ──────────────────────────────── ν™˜κ²½ λ³€μˆ˜ / μƒμˆ˜ ───────────────────────────
ANTHROPIC_KEY = os.getenv("API_KEY", "")
BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # 이름 μœ μ§€
BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
IMAGE_API_URL = "http://211.233.58.201:7896"
MAX_TOKENS = 7_999
# ──────────────────────────────── λ‘œκΉ… ──────────────────────────────────────
logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s")
# ──────────────────────────────── Anthropic Client ─────────────────────────
client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
# ──────────────────────────────── λΈ”λ‘œκ·Έ μž‘μ„± μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ ────────────────
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. 가독성: λͺ…ν™•ν•œ 단락 ꡬ뢄과 강쑰점 μ‚¬μš©
"""
# ──────────────────────────────── Brave Search API ─────────────────────────
def brave_search(query: str, count: int = 5):
"""
Brave Web Search API 호좜 β†’ list[dict]
λ°˜ν™˜ ν•„λ“œ: index, title, link, snippet, displayed_link
"""
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)}
r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
r.raise_for_status()
data = r.json()
raw = data.get("web", {}).get("results") or data.get("results", [])
arts = []
for i, res in enumerate(raw[:count], 1):
url = res.get("url", res.get("link", ""))
host = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
arts.append({
"index": i,
"title": res.get("title", "제λͺ© μ—†μŒ"),
"link": url,
"snippet": res.get("description", res.get("text", "λ‚΄μš© μ—†μŒ")),
"displayed_link": host
})
return arts
def mock_results(query: str) -> str:
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return (f"# 가상 검색 κ²°κ³Ό (생성: {ts})\n\n"
f"### Result 1: {query} κ΄€λ ¨ μ˜ˆμ‹œ κ²°κ³Ό\n\n"
"API 호좜 μ‹€νŒ¨λ‘œ μƒμ„±λœ μž„μ‹œ λ°μ΄ν„°μž…λ‹ˆλ‹€.\n\n"
"**좜처**: [example.com](https://example.com)\n\n---\n")
def do_web_search(query: str) -> str:
try:
arts = brave_search(query, 5)
except Exception as e:
logging.error(f"Brave 검색 μ‹€νŒ¨: {e}")
return mock_results(query)
if not arts:
return mock_results(query)
hdr = "# μ›Ή 검색 κ²°κ³Ό\nμ•„λž˜ 정보λ₯Ό μ°Έκ³ ν•΄μ„œ λ‹΅λ³€ν•˜μ„Έμš”.\n\n"
body = "\n".join(
f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
f"**좜처**: [{a['displayed_link']}]({a['link']})\n\n---\n"
for a in arts
)
return hdr + body
# ──────────────────────────────── 이미지 Β· λ³€ν™˜ μœ ν‹Έ ────────────────────────
def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
if not prompt: return None, "ν”„λ‘¬ν”„νŠΈ λΆ€μ‘±"
try:
res = Client(IMAGE_API_URL).predict(
prompt=prompt, width=w, height=h, guidance=g,
inference_steps=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: str, topic: str):
sys = f"λ‹€μŒ κΈ€λ‘œλΆ€ν„° μ˜μ–΄ 1쀄 이미지 ν”„λ‘¬ν”„νŠΈ 생성:\n{topic}"
try:
res = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=80, system=sys,
messages=[{"role": "user", "content": blog}]
)
return res.content[0].text.strip()
except Exception:
return f"A professional photo related to {topic}, high quality"
def md_to_html(md: str, title="Ginigen Blog"):
return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
def keywords(text: str, top=5):
return " ".join(re.sub(r"[^κ°€-힣a-zA-Z0-9\\s]", "", text).split()[:top])
# ──────────────────────────────── Streamlit UI ────────────────────────────
def ginigen_app():
st.title("Ginigen Blog")
# μ„Έμ…˜ κΈ°λ³Έκ°’
defaults = dict(
ai_model="claude-3-7-sonnet-20250219",
messages=[],
auto_save=True,
generate_image=False,
use_web_search=False
)
for k, v in defaults.items():
st.session_state.setdefault(k, v)
# ── μ‚¬μ΄λ“œλ°” 컨트둀
sb = st.sidebar
sb.title("λŒ€ν™” 기둝 관리")
sb.toggle("μžλ™ μ €μž₯", key="auto_save")
sb.toggle("이미지 μžλ™ 생성", key="generate_image")
sb.toggle("μ›Ή 검색 μ‚¬μš©", key="use_web_search")
# ── 졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ (λ§ˆν¬λ‹€μš΄ / HTML)
latest_blog = next(
(m["content"] for m in reversed(st.session_state.messages)
if m["role"] == "assistant" and m["content"].strip()), None)
if latest_blog:
title = re.search(r"# (.*?)(\n|$)", latest_blog)
title = title.group(1).strip() if title else "blog"
sb.subheader("졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
c1, c2 = sb.columns(2)
c1.download_button("Markdown", latest_blog,
file_name=f"{title}.md", mime="text/markdown")
c2.download_button("HTML", md_to_html(latest_blog, title),
file_name=f"{title}.html", mime="text/html")
# ── JSON λŒ€ν™” 기둝 μ—…λ‘œλ“œ
up = sb.file_uploader("λŒ€ν™” 기둝 뢈러였기 (.json)", type=["json"])
if up:
try:
st.session_state.messages = json.load(up)
sb.success("λŒ€ν™” 기둝 뢈러였기 μ™„λ£Œ")
except Exception as e:
sb.error(f"뢈러였기 μ‹€νŒ¨: {e}")
# ── JSON λŒ€ν™” 기둝 λ‹€μš΄λ‘œλ“œ
if sb.button("λŒ€ν™” 기둝 JSON λ‹€μš΄λ‘œλ“œ"):
sb.download_button("μ €μž₯", json.dumps(st.session_state.messages,
ensure_ascii=False, indent=2),
file_name="chat_history.json",
mime="application/json")
# ── κΈ°μ‘΄ λ©”μ‹œμ§€ λ Œλ”λ§
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(); answer = ""
sys_prompt = get_system_prompt()
if st.session_state.use_web_search:
with st.spinner("μ›Ή 검색 쀑…"):
search_md = do_web_search(keywords(prompt))
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:
answer += t or ""
placeholder.markdown(answer + "β–Œ")
placeholder.markdown(answer)
# 이미지 μ˜΅μ…˜
if st.session_state.generate_image:
with st.spinner("이미지 생성 쀑…"):
ip = extract_image_prompt(answer, prompt)
img, cap = generate_image(ip)
if img:
st.image(img, caption=cap)
st.session_state.messages.append(
{"role": "assistant", "content": answer,
"image": img, "image_caption": cap})
answer_entry_saved = True
if not st.session_state.generate_image:
st.session_state.messages.append(
{"role": "assistant", "content": answer})
# λ³Έλ¬Έ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ (MD / HTML)
st.subheader("이 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
b1, b2 = st.columns(2)
b1.download_button("Markdown", answer,
file_name=f"{prompt[:30]}.md", mime="text/markdown")
b2.download_button("HTML", md_to_html(answer, prompt[:30]),
file_name=f"{prompt[:30]}.html", mime="text/html")
# ── μžλ™ λ°±μ—… μ €μž₯
if st.session_state.auto_save and st.session_state.messages:
try:
fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
with open(fn, "w", encoding="utf-8") as fp:
json.dump(st.session_state.messages, fp,
ensure_ascii=False, indent=2)
except Exception as e:
logging.error(f"μžλ™ μ €μž₯ μ‹€νŒ¨: {e}")
# ──────────────────────────────── main / requirements ──────────────────────
def main(): ginigen_app()
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()