Spaces:
Sleeping
Sleeping
Create llm_utils.py
Browse files- llm_utils.py +89 -0
llm_utils.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Literal
|
| 3 |
+
from openai import OpenAI
|
| 4 |
+
|
| 5 |
+
_OPENAI_CLIENT = None
|
| 6 |
+
|
| 7 |
+
def _client() -> OpenAI:
|
| 8 |
+
global _OPENAI_CLIENT
|
| 9 |
+
if _OPENAI_CLIENT is None:
|
| 10 |
+
api_key = os.getenv("OPENAI_API_KEY", "")
|
| 11 |
+
if not api_key:
|
| 12 |
+
raise RuntimeError("OPENAI_API_KEY is not set in Secrets.")
|
| 13 |
+
_OPENAI_CLIENT = OpenAI(api_key=api_key)
|
| 14 |
+
return _OPENAI_CLIENT
|
| 15 |
+
|
| 16 |
+
TONE_MAP = {
|
| 17 |
+
"neutral": "ニュートラルで事実中心",
|
| 18 |
+
"formal": "フォーマルで簡潔",
|
| 19 |
+
"friendly": "親しみやすく平易",
|
| 20 |
+
"investor": "投資家向け、KPI/財務メトリクスを強調",
|
| 21 |
+
"pr_bold": "PR向け、ヘッドラインを強調し勢いある表現",
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
TYPE_GUIDE = {
|
| 25 |
+
"press_release": "ニュースリリース: タイトル/リード/本文/会社概要/問い合わせ先",
|
| 26 |
+
"ir_letter": "IRレター: タイトル/ご挨拶/ハイライト/今後の見通し/お問い合わせ",
|
| 27 |
+
"investor_summary": "投資家向けサマリー: 3-7箇条書き、KPI・成長要因・リスク要因",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
SYSTEM_TMPL = """
|
| 31 |
+
あなたは日本語のPR/IRライターです。事実の正確性、簡潔さ、再利用しやすいMarkdown構成を重視します。
|
| 32 |
+
- 出力は必ずMarkdown。
|
| 33 |
+
- センセーショナルすぎる表現や根拠のない主張は避ける。
|
| 34 |
+
- プレス/IRのルールに配慮し、誤解を招かない文に整える。
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
PROMPT_TMPL = """
|
| 38 |
+
【目的】{ctype}のドラフトを{tone_ja}で作成。
|
| 39 |
+
【入力テキスト要約】以下の素材から、要点を抽出し、{ctype_guide}に沿ってMarkdownドラフトを生成。
|
| 40 |
+
---
|
| 41 |
+
{source_text}
|
| 42 |
+
---
|
| 43 |
+
出力要件:
|
| 44 |
+
- 1行目に短いタイトル(H1)
|
| 45 |
+
- 適切な小見出し(H2/H3)
|
| 46 |
+
- 箇条書きは短文で
|
| 47 |
+
- ファクトと数値はそのまま
|
| 48 |
+
- 最後に「メタデータ」セクションを追加(推奨タグ・要約140字・推奨URLスラッグ)
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
def generate_draft(source_text: str,
|
| 52 |
+
content_type: Literal["press_release","ir_letter","investor_summary"],
|
| 53 |
+
tone: str):
|
| 54 |
+
client = _client()
|
| 55 |
+
tone_ja = TONE_MAP.get(tone, TONE_MAP["neutral"])
|
| 56 |
+
guide = TYPE_GUIDE[content_type]
|
| 57 |
+
|
| 58 |
+
messages = [
|
| 59 |
+
{"role": "system", "content": SYSTEM_TMPL},
|
| 60 |
+
{"role": "user", "content": PROMPT_TMPL.format(
|
| 61 |
+
ctype=content_type, tone_ja=tone_ja, ctype_guide=guide,
|
| 62 |
+
source_text=source_text[:12000]
|
| 63 |
+
)}
|
| 64 |
+
]
|
| 65 |
+
resp = client.chat.completions.create(
|
| 66 |
+
model=os.getenv("OPENAI_MODEL","gpt-4o-mini"),
|
| 67 |
+
temperature=float(os.getenv("OPENAI_TEMPERATURE","0.2")),
|
| 68 |
+
messages=messages,
|
| 69 |
+
)
|
| 70 |
+
text = resp.choices[0].message.content
|
| 71 |
+
|
| 72 |
+
subj_resp = client.chat.completions.create(
|
| 73 |
+
model=os.getenv("OPENAI_MODEL","gpt-4o-mini"),
|
| 74 |
+
temperature=0.3,
|
| 75 |
+
messages=[
|
| 76 |
+
{"role":"system","content":"日本語のPRメール件名ライター。50文字以内、事実に忠実。"},
|
| 77 |
+
{"role":"user","content": f"次のMarkdownドラフトから、差分のある件名案を2つ出して。\n---\n{text[:6000]}"},
|
| 78 |
+
]
|
| 79 |
+
)
|
| 80 |
+
subs = [s.strip("- ・* ") for s in subj_resp.choices[0].message.content.splitlines() if s.strip()]
|
| 81 |
+
subj_a = subs[0] if subs else "お知らせ"
|
| 82 |
+
subj_b = subs[1] if len(subs) > 1 else subj_a + "【続報】"
|
| 83 |
+
|
| 84 |
+
title = None
|
| 85 |
+
for line in text.splitlines():
|
| 86 |
+
if line.strip().startswith("# "):
|
| 87 |
+
title = line.strip().lstrip("# ").strip()
|
| 88 |
+
break
|
| 89 |
+
return title or "ドラフト", text, subj_a, subj_b
|