Upload 22 files
Browse files- README.md +9 -8
- backend:.env.exampl +12 -0
- backend:app:config.py +13 -0
- backend:app:openai_utils.py +26 -0
- backend:app:prompt_templates.py +41 -0
- backend:app:schemas.py +33 -0
- backend:requirements.txt +6 -0
- frontend:index.html +12 -0
README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Sales Writer API
|
| 3 |
+
emoji: 📧
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 8000
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
backend:.env.exampl
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
|
| 2 |
+
# "gpt-4o" など。お好みで差し替え可
|
| 3 |
+
OPENAI_MODEL=gpt-4o
|
| 4 |
+
|
| 5 |
+
# Hugging Face: どちらか
|
| 6 |
+
# 1) Inference API を使う場合(簡単・有料)
|
| 7 |
+
HF_API_KEY=hf_xxxxxxxxxxxxxxxxxxxxxxxx
|
| 8 |
+
HF_SENTIMENT_MODEL=cardiffnlp/twitter-roberta-base-sentiment-latest
|
| 9 |
+
HF_TOXIC_MODEL=unitary/toxic-bert
|
| 10 |
+
HF_SUMMARY_MODEL=facebook/bart-large-cnn
|
| 11 |
+
|
| 12 |
+
# 2) ローカル推論を使う場合は HF_API_KEY 不要(このMVPはInference API前提)
|
backend:app:config.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
class Settings(BaseModel):
|
| 5 |
+
openai_api_key: str = os.getenv("OPENAI_API_KEY", "")
|
| 6 |
+
openai_model: str = os.getenv("OPENAI_MODEL", "gpt-4o")
|
| 7 |
+
|
| 8 |
+
hf_api_key: str = os.getenv("HF_API_KEY", "")
|
| 9 |
+
hf_sentiment_model: str = os.getenv("HF_SENTIMENT_MODEL", "cardiffnlp/twitter-roberta-base-sentiment-latest")
|
| 10 |
+
hf_toxic_model: str = os.getenv("HF_TOXIC_MODEL", "unitary/toxic-bert")
|
| 11 |
+
hf_summary_model: str = os.getenv("HF_SUMMARY_MODEL", "facebook/bart-large-cnn")
|
| 12 |
+
|
| 13 |
+
settings = Settings()
|
backend:app:openai_utils.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Tuple
|
| 2 |
+
from openai import OpenAI
|
| 3 |
+
from .cofig import settings
|
| 4 |
+
|
| 5 |
+
client = OpenAI(api_key = settings.openai_api_key)
|
| 6 |
+
|
| 7 |
+
def chat_complete(system_prompt:str, user_prompt:str) -> str:
|
| 8 |
+
resp = client.chat.completions.create(
|
| 9 |
+
model=settings.openai_model,
|
| 10 |
+
messages=[
|
| 11 |
+
{"role": "system", "content": user_prompt},
|
| 12 |
+
{"role": "user", "content": user_prompt}
|
| 13 |
+
],
|
| 14 |
+
temperature=0.6,
|
| 15 |
+
)
|
| 16 |
+
return resp.choices[0].message.content or ""
|
| 17 |
+
|
| 18 |
+
def split_subject_body(text:str) -> Tuple[str, str]:
|
| 19 |
+
subject, body = "",text
|
| 20 |
+
lower = text.replace(":",":")
|
| 21 |
+
if "件名:"in lower and "本文:"in lower:
|
| 22 |
+
s = lower.split("件名:",1)[1]
|
| 23 |
+
parts = s.split("本文:",1)
|
| 24 |
+
subject = parts[0].strip()
|
| 25 |
+
body = parts[1].strip()if len(parts)>1 else""
|
| 26 |
+
return subject.strip(), body.strip()
|
backend:app:prompt_templates.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
EMIL_SYSTEM="""あなたは一流のB2B営業コピーライターです。
|
| 2 |
+
-誤解を避けるため、簡潔で読みやすく、相手中心で書きます。
|
| 3 |
+
-日本のビジネス慣習を配慮し、丁寧で過度な断定や誇張を避けます。
|
| 4 |
+
-スパム判定を避ける表現に留意します。
|
| 5 |
+
-必ず件名と本文(挨拶→課題の共感→価値提案→次のアクション→署名)の順に出力します。
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
EMAIL_USER="""出力言語:{language}
|
| 9 |
+
業界:{industry}
|
| 10 |
+
相手企業:{target_company}
|
| 11 |
+
担当者:{target_persona}
|
| 12 |
+
課題:{pain_points}
|
| 13 |
+
提供価値:{value_prop}
|
| 14 |
+
製品名:{product_name}
|
| 15 |
+
CTA:{cta}
|
| 16 |
+
企業トーン:{tone}
|
| 17 |
+
文章ボリューム目安:{length_hint}
|
| 18 |
+
差し込み変数:{variables}
|
| 19 |
+
追加指示:{extra_instr}
|
| 20 |
+
|
| 21 |
+
#出力フォーマット
|
| 22 |
+
件名:
|
| 23 |
+
本文
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
PROPOSAL_SYSTEM="""あなたはB2B提案書のストラクチャ設計に長けたコンサルタントです。
|
| 27 |
+
-相手企業の課題に即した章立てとエグゼクティブサマリーを日本語で作成します。
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
PROPOSAL_USER="""相手業界:{industry}
|
| 31 |
+
相手企業:{target_company}
|
| 32 |
+
課題:{pain_points}
|
| 33 |
+
提供価値:{value_prop}
|
| 34 |
+
製品名:{product_name}
|
| 35 |
+
CTA:{cta}
|
| 36 |
+
希望トーン:{tone}
|
| 37 |
+
|
| 38 |
+
#出力
|
| 39 |
+
-章立て(5~8章)
|
| 40 |
+
-エグゼクティブサマリー(200~300字)
|
| 41 |
+
"""
|
backend:app:schemas.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional, Dict, List
|
| 3 |
+
|
| 4 |
+
class GenerateRequest(BaseModel):
|
| 5 |
+
language: str = Field("ja", description="出力言語 'ja' or 'en'")
|
| 6 |
+
industry: str = Field(..., description="相手の業界")
|
| 7 |
+
target_company: str = Field(..., description="相手企業名")
|
| 8 |
+
target_persona: str = Field(..., description="担当者ペルソナ(役職など)")
|
| 9 |
+
pain_points: List[str] = Field(..., description="相手の課題(箇条書きOK)")
|
| 10 |
+
value_prop: str = Field(..., description="自社の提供価値")
|
| 11 |
+
product_name: str = Field(..., description="製品/サービス名")
|
| 12 |
+
cta: str = Field("15分のオンライン面談をご提案", description="コールトゥアクション")
|
| 13 |
+
tone: str = Field("フォーマルで間血", description="希望トーン")
|
| 14 |
+
variables: Dict[str, str] = Field(default_factory=dict, description="差し込み変数")
|
| 15 |
+
length_hint: str = Field("中", description="短/中/長")
|
| 16 |
+
extra_instr: Optional[str] = Field(None, description="追加指示(禁止表現など)")
|
| 17 |
+
|
| 18 |
+
class EmailRequest(BaseModel):
|
| 19 |
+
subject: str
|
| 20 |
+
body: str
|
| 21 |
+
quality: Dict[str, float] # e.g., {"relevance": 0.92, "clarity": 0.01}
|
| 22 |
+
warnings: List[str] = []
|
| 23 |
+
|
| 24 |
+
class ProposalResponse(BaseModel):
|
| 25 |
+
outline: List[str]
|
| 26 |
+
executive_summary: str
|
| 27 |
+
|
| 28 |
+
class LintResponse(BaseModel):
|
| 29 |
+
text: str
|
| 30 |
+
|
| 31 |
+
class LintResponse(BaseModel):
|
| 32 |
+
issues: List[str]
|
| 33 |
+
toxicity: float
|
backend:requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.112.0
|
| 2 |
+
uvicorn[standard]==0.30.6
|
| 3 |
+
pydantic==2.8.2
|
| 4 |
+
python-dotenv==1.0.1
|
| 5 |
+
httpx==0.27.0
|
| 6 |
+
openai==1.50.0
|
frontend:index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="ja">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8"/>
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
+
<title>営業メール/提案書ジェネレーター</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="root"> </div>
|
| 10 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|