Upload 4 files
Browse files- API_KEY=dev +5 -0
- main.py +226 -0
- openai_integration.py +130 -0
- requirements +6 -0
API_KEY=dev
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
API_KEY=dev
|
| 2 |
+
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
|
| 3 |
+
OAI_CHAT_MODEL=gpt-4o-mini
|
| 4 |
+
OAI_EMB_MODEL=text-embedding-3-small
|
| 5 |
+
DATABASE_URL=sqlite:///./app.db
|
main.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# main.py
|
| 2 |
+
"""
|
| 3 |
+
Mini Invoice/Estimate (Quote) SaaS — single-file FastAPI app
|
| 4 |
+
Beginner-friendly: SQLite + SQLModel (SQLAlchemy) + Pydantic.
|
| 5 |
+
|
| 6 |
+
Features (MVP)
|
| 7 |
+
- Customers CRUD
|
| 8 |
+
- Products CRUD
|
| 9 |
+
- Quotes: create, add/remove line items, compute totals
|
| 10 |
+
- Invoices: create (or convert from quote), mark paid, compute totals
|
| 11 |
+
- Simple API-key auth via X-API-Key header (set API_KEY env var; defaults to "dev")
|
| 12 |
+
- Autogenerated docs at /docs
|
| 13 |
+
|
| 14 |
+
Run locally:
|
| 15 |
+
python -m venv .venv && source .venv/bin/activate
|
| 16 |
+
pip install -r requirements.txt
|
| 17 |
+
export API_KEY=dev
|
| 18 |
+
uvicorn main:app --reload
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
from typing import Optional, List
|
| 23 |
+
from datetime import datetime, date
|
| 24 |
+
import os
|
| 25 |
+
|
| 26 |
+
from fastapi import FastAPI, Depends, HTTPException, Header, Query
|
| 27 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 28 |
+
from pydantic import BaseModel
|
| 29 |
+
from sqlmodel import Field as SQLField, Session, SQLModel, create_engine, select, Relationship
|
| 30 |
+
|
| 31 |
+
from openai_integration import router as ai_router
|
| 32 |
+
app.include_router(ai_router, prefix="/ai", tags=["ai"])
|
| 33 |
+
|
| 34 |
+
# --------------------------
|
| 35 |
+
# Auth
|
| 36 |
+
# --------------------------
|
| 37 |
+
API_KEY = os.getenv("API_KEY", "dev")
|
| 38 |
+
|
| 39 |
+
async def require_api_key(x_api_key: str | None = Header(default=None)):
|
| 40 |
+
if x_api_key != API_KEY:
|
| 41 |
+
raise HTTPException(status_code=401, detail="Invalid or missing X-API-Key")
|
| 42 |
+
|
| 43 |
+
# --------------------------
|
| 44 |
+
# DB
|
| 45 |
+
# --------------------------
|
| 46 |
+
DB_URL = os.getenv("DATABASE_URL", "sqlite:///./app.db")
|
| 47 |
+
engine = create_engine(DB_URL, echo=False)
|
| 48 |
+
|
| 49 |
+
def get_session():
|
| 50 |
+
with Session(engine) as session:
|
| 51 |
+
yield session
|
| 52 |
+
|
| 53 |
+
# --------------------------
|
| 54 |
+
# Models
|
| 55 |
+
# --------------------------
|
| 56 |
+
class Customer(SQLModel, table=True):
|
| 57 |
+
id: Optional[int] = SQLField(default=None, primary_key=True)
|
| 58 |
+
name: str = SQLField(index=True)
|
| 59 |
+
email: Optional[str] = None
|
| 60 |
+
phone: Optional[str] = None
|
| 61 |
+
address: Optional[str] = None
|
| 62 |
+
city: Optional[str] = None
|
| 63 |
+
country: Optional[str] = None
|
| 64 |
+
|
| 65 |
+
quotes: List["Quote"] = Relationship(back_populates="customer")
|
| 66 |
+
invoices: List["Invoice"] = Relationship(back_populates="customer")
|
| 67 |
+
|
| 68 |
+
class Product(SQLModel, table=True):
|
| 69 |
+
id: Optional[int] = SQLField(default=None, primary_key=True)
|
| 70 |
+
name: str
|
| 71 |
+
unit_price: float = SQLField(ge=0)
|
| 72 |
+
currency: str = SQLField(default="JPY")
|
| 73 |
+
sku: Optional[str] = None
|
| 74 |
+
description: Optional[str] = None
|
| 75 |
+
|
| 76 |
+
class Quote(SQLModel, table=True):
|
| 77 |
+
id: Optional[int] = SQLField(default=None, primary_key=True)
|
| 78 |
+
customer_id: int = SQLField(foreign_key="customer.id")
|
| 79 |
+
status: str = SQLField(default="draft", index=True) # draft/sent/accepted/expired
|
| 80 |
+
issue_date: date = SQLField(default_factory=lambda: datetime.utcnow().date())
|
| 81 |
+
valid_until: Optional[date] = None
|
| 82 |
+
notes: Optional[str] = None
|
| 83 |
+
|
| 84 |
+
customer: Optional[Customer] = Relationship(back_populates="quotes")
|
| 85 |
+
items: List["QuoteItem"] = Relationship(back_populates="quote")
|
| 86 |
+
|
| 87 |
+
class QuoteItem(SQLModel, table=True):
|
| 88 |
+
id: Optional[int] = SQLField(default=None, primary_key=True)
|
| 89 |
+
quote_id: int = SQLField(foreign_key="quote.id")
|
| 90 |
+
product_id: Optional[int] = SQLField(foreign_key="product.id", default=None)
|
| 91 |
+
description: str
|
| 92 |
+
quantity: float = SQLField(gt=0, default=1)
|
| 93 |
+
unit_price: float = SQLField(ge=0)
|
| 94 |
+
tax_rate: float = SQLField(ge=0, default=0.0)
|
| 95 |
+
|
| 96 |
+
quote: Optional[Quote] = Relationship(back_populates="items")
|
| 97 |
+
|
| 98 |
+
class Invoice(SQLModel, table=True):
|
| 99 |
+
id: Optional[int] = SQLField(default=None, primary_key=True)
|
| 100 |
+
customer_id: int = SQLField(foreign_key="customer.id")
|
| 101 |
+
status: str = SQLField(default="unpaid", index=True) # unpaid/paid/void
|
| 102 |
+
issue_date: date = SQLField(default_factory=lambda: datetime.utcnow().date())
|
| 103 |
+
due_date: Optional[date] = None
|
| 104 |
+
notes: Optional[str] = None
|
| 105 |
+
|
| 106 |
+
customer: Optional[Customer] = Relationship(back_populates="invoices")
|
| 107 |
+
items: List["InvoiceItem"] = Relationship(back_populates="invoice")
|
| 108 |
+
|
| 109 |
+
class InvoiceItem(SQLModel, table=True):
|
| 110 |
+
id: Optional[int] = SQLField(default=None, primary_key=True)
|
| 111 |
+
invoice_id: int = SQLField(foreign_key="invoice.id")
|
| 112 |
+
product_id: Optional[int] = SQLField(foreign_key="product.id", default=None)
|
| 113 |
+
description: str
|
| 114 |
+
quantity: float = SQLField(gt=0, default=1)
|
| 115 |
+
unit_price: float = SQLField(ge=0)
|
| 116 |
+
tax_rate: float = SQLField(ge=0, default=0.0)
|
| 117 |
+
|
| 118 |
+
invoice: Optional[Invoice] = Relationship(back_populates="items")
|
| 119 |
+
|
| 120 |
+
# --------------------------
|
| 121 |
+
# Totals
|
| 122 |
+
# --------------------------
|
| 123 |
+
class MoneyBreakdown(BaseModel):
|
| 124 |
+
subtotal: float
|
| 125 |
+
tax: float
|
| 126 |
+
total: float
|
| 127 |
+
|
| 128 |
+
def round2(v: float) -> float:
|
| 129 |
+
return float(f"{v:.2f}")
|
| 130 |
+
|
| 131 |
+
def compute_totals(items: list[dict]) -> MoneyBreakdown:
|
| 132 |
+
subtotal = 0.0
|
| 133 |
+
tax = 0.0
|
| 134 |
+
for it in items:
|
| 135 |
+
line = it["quantity"] * it["unit_price"]
|
| 136 |
+
subtotal += line
|
| 137 |
+
tax += line * it.get("tax_rate", 0.0)
|
| 138 |
+
return MoneyBreakdown(subtotal=round2(subtotal), tax=round2(tax), total=round2(subtotal + tax))
|
| 139 |
+
|
| 140 |
+
# --------------------------
|
| 141 |
+
# App
|
| 142 |
+
# --------------------------
|
| 143 |
+
app = FastAPI(title="Mini Invoice/Estimate SaaS", version="0.1.0")
|
| 144 |
+
app.add_middleware(
|
| 145 |
+
CORSMiddleware,
|
| 146 |
+
allow_origins=["*"], allow_credentials=True,
|
| 147 |
+
allow_methods=["*"], allow_headers=["*"],
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
@app.on_event("startup")
|
| 151 |
+
def on_startup():
|
| 152 |
+
SQLModel.metadata.create_all(engine)
|
| 153 |
+
|
| 154 |
+
# -------- Customers --------
|
| 155 |
+
@app.post("/customers", dependencies=[Depends(require_api_key)])
|
| 156 |
+
def create_customer(payload: Customer, session: Session = Depends(get_session)):
|
| 157 |
+
session.add(payload); session.commit(); session.refresh(payload)
|
| 158 |
+
return payload
|
| 159 |
+
|
| 160 |
+
@app.get("/customers", dependencies=[Depends(require_api_key)])
|
| 161 |
+
def list_customers(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), session: Session = Depends(get_session)):
|
| 162 |
+
rows = session.exec(select(Customer).offset(offset).limit(limit)).all()
|
| 163 |
+
total = session.exec(select(Customer)).count()
|
| 164 |
+
return {"data": rows, "pagination": {"total": total, "limit": limit, "offset": offset}}
|
| 165 |
+
|
| 166 |
+
# -------- Products --------
|
| 167 |
+
@app.post("/products", dependencies=[Depends(require_api_key)])
|
| 168 |
+
def create_product(payload: Product, session: Session = Depends(get_session)):
|
| 169 |
+
session.add(payload); session.commit(); session.refresh(payload)
|
| 170 |
+
return payload
|
| 171 |
+
|
| 172 |
+
@app.get("/products", dependencies=[Depends(require_api_key)])
|
| 173 |
+
def list_products(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), session: Session = Depends(get_session)):
|
| 174 |
+
rows = session.exec(select(Product).offset(offset).limit(limit)).all()
|
| 175 |
+
total = session.exec(select(Product)).count()
|
| 176 |
+
return {"data": rows, "pagination": {"total": total, "limit": limit, "offset": offset}}
|
| 177 |
+
|
| 178 |
+
# -------- Quotes --------
|
| 179 |
+
@app.post("/quotes", dependencies=[Depends(require_api_key)])
|
| 180 |
+
def create_quote(payload: Quote, session: Session = Depends(get_session)):
|
| 181 |
+
if not session.get(Customer, payload.customer_id):
|
| 182 |
+
raise HTTPException(400, "Customer not found")
|
| 183 |
+
session.add(payload); session.commit(); session.refresh(payload)
|
| 184 |
+
return payload
|
| 185 |
+
|
| 186 |
+
@app.get("/quotes/{quote_id}", dependencies=[Depends(require_api_key)])
|
| 187 |
+
def get_quote(quote_id: int, session: Session = Depends(get_session)):
|
| 188 |
+
q = session.get(Quote, quote_id)
|
| 189 |
+
if not q: raise HTTPException(404, "Quote not found")
|
| 190 |
+
items = session.exec(select(QuoteItem).where(QuoteItem.quote_id == quote_id)).all()
|
| 191 |
+
totals = compute_totals([it.dict() for it in items])
|
| 192 |
+
return {"quote": q, "items": items, "totals": totals}
|
| 193 |
+
|
| 194 |
+
@app.post("/quotes/{quote_id}/items", dependencies=[Depends(require_api_key)])
|
| 195 |
+
def add_quote_item(quote_id: int, payload: QuoteItem, session: Session = Depends(get_session)):
|
| 196 |
+
if not session.get(Quote, quote_id): raise HTTPException(404, "Quote not found")
|
| 197 |
+
payload.quote_id = quote_id
|
| 198 |
+
session.add(payload); session.commit(); session.refresh(payload)
|
| 199 |
+
return payload
|
| 200 |
+
|
| 201 |
+
# -------- Invoices --------
|
| 202 |
+
@app.post("/invoices", dependencies=[Depends(require_api_key)])
|
| 203 |
+
def create_invoice(payload: Invoice, session: Session = Depends(get_session)):
|
| 204 |
+
if not session.get(Customer, payload.customer_id):
|
| 205 |
+
raise HTTPException(400, "Customer not found")
|
| 206 |
+
session.add(payload); session.commit(); session.refresh(payload)
|
| 207 |
+
return payload
|
| 208 |
+
|
| 209 |
+
@app.get("/invoices/{invoice_id}", dependencies=[Depends(require_api_key)])
|
| 210 |
+
def get_invoice(invoice_id: int, session: Session = Depends(get_session)):
|
| 211 |
+
inv = session.get(Invoice, invoice_id)
|
| 212 |
+
if not inv: raise HTTPException(404, "Invoice not found")
|
| 213 |
+
items = session.exec(select(InvoiceItem).where(InvoiceItem.invoice_id == invoice_id)).all()
|
| 214 |
+
totals = compute_totals([it.dict() for it in items])
|
| 215 |
+
return {"invoice": inv, "items": items, "totals": totals}
|
| 216 |
+
|
| 217 |
+
@app.post("/invoices/{invoice_id}/items", dependencies=[Depends(require_api_key)])
|
| 218 |
+
def add_invoice_item(invoice_id: int, payload: InvoiceItem, session: Session = Depends(get_session)):
|
| 219 |
+
if not session.get(Invoice, invoice_id): raise HTTPException(404, "Invoice not found")
|
| 220 |
+
payload.invoice_id = invoice_id
|
| 221 |
+
session.add(payload); session.commit(); session.refresh(payload)
|
| 222 |
+
return payload
|
| 223 |
+
|
| 224 |
+
@app.get("/")
|
| 225 |
+
def root():
|
| 226 |
+
return {"ok": True, "app": "mini-invoice-saas", "docs": "/docs"}
|
openai_integration.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# openai_integration.py
|
| 2 |
+
"""
|
| 3 |
+
OpenAI (ChatGPT) integration for the Mini Invoice/Estimate SaaS (FastAPI)
|
| 4 |
+
- Uses OpenAI Python SDK v1 (chat completions + embeddings)
|
| 5 |
+
- Auth via env var: OPENAI_API_KEY
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
import os
|
| 10 |
+
from typing import List, Optional
|
| 11 |
+
|
| 12 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 13 |
+
from pydantic import BaseModel, Field
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
from main import require_api_key # reuse API-key header guard
|
| 17 |
+
except Exception:
|
| 18 |
+
async def require_api_key():
|
| 19 |
+
return None
|
| 20 |
+
|
| 21 |
+
from openai import OpenAI
|
| 22 |
+
|
| 23 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 24 |
+
if not OPENAI_API_KEY:
|
| 25 |
+
raise RuntimeError("Set OPENAI_API_KEY before importing openai_integration")
|
| 26 |
+
|
| 27 |
+
client = OpenAI(api_key=OPENAI_API_KEY)
|
| 28 |
+
|
| 29 |
+
OAI_CHAT_MODEL = os.getenv("OAI_CHAT_MODEL", "gpt-4o-mini") # 軽量モデル推奨
|
| 30 |
+
OAI_EMB_MODEL = os.getenv("OAI_EMB_MODEL", "text-embedding-3-small")
|
| 31 |
+
|
| 32 |
+
router = APIRouter()
|
| 33 |
+
|
| 34 |
+
# -------- Schemas --------
|
| 35 |
+
class LineItem(BaseModel):
|
| 36 |
+
description: str
|
| 37 |
+
quantity: float = 1
|
| 38 |
+
unit_price: float
|
| 39 |
+
tax_rate: float = 0.1
|
| 40 |
+
|
| 41 |
+
class GenerateEmailRequest(BaseModel):
|
| 42 |
+
kind: str = Field(pattern="^(quote|invoice)$")
|
| 43 |
+
company_name: str
|
| 44 |
+
customer_name: str
|
| 45 |
+
language: str = Field("ja", description="ja or en")
|
| 46 |
+
items: List[LineItem]
|
| 47 |
+
due_date: Optional[str] = None
|
| 48 |
+
notes: Optional[str] = None
|
| 49 |
+
tone: str = Field("polite", description="polite|friendly|concise")
|
| 50 |
+
|
| 51 |
+
class SummarizeRequest(BaseModel):
|
| 52 |
+
text: str
|
| 53 |
+
language: str = "ja"
|
| 54 |
+
max_points: int = 5
|
| 55 |
+
|
| 56 |
+
class EmbeddingsRequest(BaseModel):
|
| 57 |
+
texts: List[str]
|
| 58 |
+
|
| 59 |
+
class EmbeddingsResponse(BaseModel):
|
| 60 |
+
vectors: List[List[float]]
|
| 61 |
+
|
| 62 |
+
# -------- Helpers --------
|
| 63 |
+
EMAIL_SYS = (
|
| 64 |
+
"You are a helpful business assistant. Write concise, professional emails. "
|
| 65 |
+
"Output a subject line and a body."
|
| 66 |
+
)
|
| 67 |
+
SUM_SYS = (
|
| 68 |
+
"You are a world-class note taker. Produce clean bullet points and an 'Action Items' list."
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
def _chat(messages: list[dict], max_tokens: int = 600, temperature: float = 0.3) -> str:
|
| 72 |
+
resp = client.chat.completions.create(
|
| 73 |
+
model=OAI_CHAT_MODEL, messages=messages, max_tokens=max_tokens, temperature=temperature
|
| 74 |
+
)
|
| 75 |
+
return resp.choices[0].message.content.strip()
|
| 76 |
+
|
| 77 |
+
def _format_items(items: List[LineItem]) -> str:
|
| 78 |
+
return "\n".join(
|
| 79 |
+
f"- {it.description}: 数量 {it.quantity}, 単価 {it.unit_price:.2f}, 税率 {it.tax_rate*100:.0f}%"
|
| 80 |
+
for it in items
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# -------- Routes --------
|
| 84 |
+
@router.post("/generate-email", dependencies=[Depends(require_api_key)])
|
| 85 |
+
async def generate_email(req: GenerateEmailRequest):
|
| 86 |
+
kind_ja = "御見積書" if req.kind == "quote" else "請求書"
|
| 87 |
+
items_block = _format_items(req.items)
|
| 88 |
+
user_prompt = f"""
|
| 89 |
+
以下の情報を用いて、{kind_ja}送付メールの本文を{req.language}で作成してください。
|
| 90 |
+
制約:
|
| 91 |
+
- 件名(Subject)と本文を出力
|
| 92 |
+
- 本文は宛名、要点の箇条書き、締め、署名の順
|
| 93 |
+
- 不要な装飾は避け、{req.tone}な口調
|
| 94 |
+
|
| 95 |
+
会社名: {req.company_name}
|
| 96 |
+
顧客名: {req.customer_name}
|
| 97 |
+
支払期日: {req.due_date or '記載なし'}
|
| 98 |
+
明細:
|
| 99 |
+
{items_block}
|
| 100 |
+
特記事項: {req.notes or 'なし'}
|
| 101 |
+
""".strip()
|
| 102 |
+
|
| 103 |
+
text = _chat(
|
| 104 |
+
[{"role": "system", "content": EMAIL_SYS}, {"role": "user", "content": user_prompt}],
|
| 105 |
+
max_tokens=500,
|
| 106 |
+
)
|
| 107 |
+
return {"email": text}
|
| 108 |
+
|
| 109 |
+
@router.post("/summarize-notes", dependencies=[Depends(require_api_key)])
|
| 110 |
+
async def summarize_notes(req: SummarizeRequest):
|
| 111 |
+
user_prompt = f"""
|
| 112 |
+
次のメモを{req.language}で要約してください。箇条書きで最大{req.max_points}点。最後に"Action Items:"として実行項目を列挙。
|
| 113 |
+
---
|
| 114 |
+
{req.text}
|
| 115 |
+
---
|
| 116 |
+
""".strip()
|
| 117 |
+
text = _chat(
|
| 118 |
+
[{"role": "system", "content": SUM_SYS}, {"role": "user", "content": user_prompt}],
|
| 119 |
+
max_tokens=400,
|
| 120 |
+
)
|
| 121 |
+
return {"summary": text}
|
| 122 |
+
|
| 123 |
+
@router.post("/embeddings", response_model=EmbeddingsResponse, dependencies=[Depends(require_api_key)])
|
| 124 |
+
async def embeddings(req: EmbeddingsRequest):
|
| 125 |
+
try:
|
| 126 |
+
resp = client.embeddings.create(model=OAI_EMB_MODEL, input=req.texts)
|
| 127 |
+
vectors = [d.embedding for d in resp.data]
|
| 128 |
+
return {"vectors": vectors}
|
| 129 |
+
except Exception as e:
|
| 130 |
+
raise HTTPException(500, f"Embeddings failed: {e}")
|
requirements
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.110
|
| 2 |
+
uvicorn[standard]>=0.27
|
| 3 |
+
sqlmodel>=0.0.16
|
| 4 |
+
pydantic>=2.6
|
| 5 |
+
python-multipart>=0.0.9
|
| 6 |
+
openai>=1.40.0
|