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