Invoice / openai_integration.py
Corin1998's picture
Upload 4 files
83155de verified
# 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}")