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