File size: 4,552 Bytes
403c37c 83155de 403c37c 83155de 403c37c 4ae243e 403c37c 4ae243e 403c37c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# openai_integration.py
"""
OpenAI (ChatGPT) integration for the Mini Invoice/Estimate SaaS (FastAPI)
- Uses OpenAI Python SDK v1 (chat completions + embeddings)
- Auth via env var: OPENAI_API_KEY
"""
from __future__ import annotations
import os
from typing import List, Optional
# Header を忘れずに import
from fastapi import APIRouter, Depends, HTTPException, Header
from pydantic import BaseModel, Field
from openai import OpenAI
try:
from main import require_api_key # reuse API-key header guard
except Exception:
async def require_api_key():
return None
from openai import OpenAI
API_KEY = os.getenv("API_KEY", "dev")
async def require_api_key(x_api_key: str | None = Header(default=None)):
if x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid or missing X-API-Key")
# --- OpenAI クライアント ---
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
raise RuntimeError("Set OPENAI_API_KEY before importing openai_integration")
client = OpenAI(api_key=OPENAI_API_KEY)
OAI_CHAT_MODEL = os.getenv("OAI_CHAT_MODEL", "gpt-4o-mini")
OAI_EMB_MODEL = os.getenv("OAI_EMB_MODEL", "text-embedding-3-small")
router = APIRouter()
# -------- Schemas --------
class LineItem(BaseModel):
description: str
quantity: float = 1
unit_price: float
tax_rate: float = 0.1
class GenerateEmailRequest(BaseModel):
kind: str = Field(pattern="^(quote|invoice)$")
company_name: str
customer_name: str
language: str = Field("ja", description="ja or en")
items: List[LineItem]
due_date: Optional[str] = None
notes: Optional[str] = None
tone: str = Field("polite", description="polite|friendly|concise")
class SummarizeRequest(BaseModel):
text: str
language: str = "ja"
max_points: int = 5
class EmbeddingsRequest(BaseModel):
texts: List[str]
class EmbeddingsResponse(BaseModel):
vectors: List[List[float]]
# -------- Helpers --------
EMAIL_SYS = (
"You are a helpful business assistant. Write concise, professional emails. "
"Output a subject line and a body."
)
SUM_SYS = (
"You are a world-class note taker. Produce clean bullet points and an 'Action Items' list."
)
def _chat(messages: list[dict], max_tokens: int = 600, temperature: float = 0.3) -> str:
resp = client.chat.completions.create(
model=OAI_CHAT_MODEL, messages=messages, max_tokens=max_tokens, temperature=temperature
)
return resp.choices[0].message.content.strip()
def _format_items(items: List[LineItem]) -> str:
return "\n".join(
f"- {it.description}: 数量 {it.quantity}, 単価 {it.unit_price:.2f}, 税率 {it.tax_rate*100:.0f}%"
for it in items
)
# -------- Routes --------
@router.post("/generate-email", dependencies=[Depends(require_api_key)])
async def generate_email(req: GenerateEmailRequest):
kind_ja = "御見積書" if req.kind == "quote" else "請求書"
items_block = _format_items(req.items)
user_prompt = f"""
以下の情報を用いて、{kind_ja}送付メールの本文を{req.language}で作成してください。
制約:
- 件名(Subject)と本文を出力
- 本文は宛名、要点の箇条書き、締め、署名の順
- 不要な装飾は避け、{req.tone}な口調
会社名: {req.company_name}
顧客名: {req.customer_name}
支払期日: {req.due_date or '記載なし'}
明細:
{items_block}
特記事項: {req.notes or 'なし'}
""".strip()
text = _chat(
[{"role": "system", "content": EMAIL_SYS}, {"role": "user", "content": user_prompt}],
max_tokens=500,
)
return {"email": text}
@router.post("/summarize-notes", dependencies=[Depends(require_api_key)])
async def summarize_notes(req: SummarizeRequest):
user_prompt = f"""
次のメモを{req.language}で要約してください。箇条書きで最大{req.max_points}点。最後に"Action Items:"として実行項目を列挙。
---
{req.text}
---
""".strip()
text = _chat(
[{"role": "system", "content": SUM_SYS}, {"role": "user", "content": user_prompt}],
max_tokens=400,
)
return {"summary": text}
@router.post("/embeddings", response_model=EmbeddingsResponse, dependencies=[Depends(require_api_key)])
async def embeddings(req: EmbeddingsRequest):
try:
resp = client.embeddings.create(model=OAI_EMB_MODEL, input=req.texts)
vectors = [d.embedding for d in resp.data]
return {"vectors": vectors}
except Exception as e:
raise HTTPException(500, f"Embeddings failed: {e}")
|