| |
| """ |
| 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 |
|
|
| |
| from fastapi import APIRouter, Depends, HTTPException, Header |
| from pydantic import BaseModel, Field |
| from openai import OpenAI |
|
|
| try: |
| from main import require_api_key |
| 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_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() |
| |
| 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]] |
|
|
| |
| 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 |
| ) |
|
|
| |
| @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}") |
|
|