# 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}")