161 lines
5.7 KiB
Python
161 lines
5.7 KiB
Python
"""ObsiGate AI — API routes for AI-powered editor features."""
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional
|
|
import logging
|
|
|
|
from backend.ai import (
|
|
ai_improve_writing, ai_fix_spelling, ai_make_shorter, ai_make_longer,
|
|
ai_simplify, ai_change_tone, ai_translate, ai_explain, ai_summarize,
|
|
ai_continue_writing, ai_custom_rewrite, ai_convert_to_list,
|
|
ai_convert_to_table, ai_generate_frontmatter, ai_inline_complete,
|
|
ai_convert_to_canvas, DEFAULT_PROVIDER, PROVIDERS,
|
|
)
|
|
|
|
logger = logging.getLogger("obsigate.ai_routes")
|
|
router = APIRouter(prefix="/api/ai", tags=["AI"])
|
|
|
|
|
|
@router.get("/status")
|
|
async def api_status():
|
|
"""Check if AI is configured and which providers are available."""
|
|
providers = {}
|
|
for name, cfg in PROVIDERS.items():
|
|
providers[name] = {
|
|
"available": bool(cfg["api_key"]),
|
|
"model": cfg["model"] if cfg["api_key"] else None,
|
|
}
|
|
return {
|
|
"configured": any(p["available"] for p in providers.values()),
|
|
"default_provider": DEFAULT_PROVIDER,
|
|
"providers": providers,
|
|
}
|
|
|
|
|
|
class AIRequest(BaseModel):
|
|
text: str = Field(..., description="Input text to process", min_length=1)
|
|
instruction: Optional[str] = Field(None, description="Custom instruction for rewrite")
|
|
target_lang: Optional[str] = Field(None, description="Target language for translation")
|
|
tone: Optional[str] = Field(None, description="Target tone (professional, casual, etc.)")
|
|
provider: Optional[str] = Field(None, description="AI provider override")
|
|
|
|
|
|
class AIResponse(BaseModel):
|
|
result: str = Field(..., description="Processed text result")
|
|
provider: str = Field(..., description="AI provider used")
|
|
|
|
|
|
async def _handle(action, request: AIRequest):
|
|
"""Wrapper with error handling."""
|
|
try:
|
|
result = await action(request.text, request.provider)
|
|
return AIResponse(result=result, provider=request.provider or "default")
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"AI error: {e}")
|
|
raise HTTPException(status_code=500, detail=f"AI service error: {str(e)}")
|
|
|
|
|
|
@router.post("/improve", response_model=AIResponse)
|
|
async def api_improve(req: AIRequest):
|
|
"""Improve writing quality."""
|
|
return await _handle(ai_improve_writing, req)
|
|
|
|
|
|
@router.post("/fix-spelling", response_model=AIResponse)
|
|
async def api_fix_spelling(req: AIRequest):
|
|
"""Fix spelling and grammar."""
|
|
return await _handle(ai_fix_spelling, req)
|
|
|
|
|
|
@router.post("/make-shorter", response_model=AIResponse)
|
|
async def api_make_shorter(req: AIRequest):
|
|
"""Make text more concise."""
|
|
return await _handle(ai_make_shorter, req)
|
|
|
|
|
|
@router.post("/make-longer", response_model=AIResponse)
|
|
async def api_make_longer(req: AIRequest):
|
|
"""Expand text with more detail."""
|
|
return await _handle(ai_make_longer, req)
|
|
|
|
|
|
@router.post("/simplify", response_model=AIResponse)
|
|
async def api_simplify(req: AIRequest):
|
|
"""Simplify language."""
|
|
return await _handle(ai_simplify, req)
|
|
|
|
|
|
@router.post("/tone", response_model=AIResponse)
|
|
async def api_tone(req: AIRequest):
|
|
"""Change text tone. Requires `tone` field (e.g., 'professional', 'casual')."""
|
|
if not req.tone:
|
|
raise HTTPException(status_code=400, detail="Field 'tone' is required (e.g., 'professional', 'casual')")
|
|
return await _handle(lambda text, p: ai_change_tone(text, req.tone, p), req)
|
|
|
|
|
|
@router.post("/translate", response_model=AIResponse)
|
|
async def api_translate(req: AIRequest):
|
|
"""Translate text. Requires `target_lang` field (e.g., 'French', 'English', 'Japanese')."""
|
|
if not req.target_lang:
|
|
raise HTTPException(status_code=400, detail="Field 'target_lang' is required")
|
|
return await _handle(lambda text, p: ai_translate(text, req.target_lang, p), req)
|
|
|
|
|
|
@router.post("/explain", response_model=AIResponse)
|
|
async def api_explain(req: AIRequest):
|
|
"""Explain the selected text."""
|
|
return await _handle(ai_explain, req)
|
|
|
|
|
|
@router.post("/summarize", response_model=AIResponse)
|
|
async def api_summarize(req: AIRequest):
|
|
"""Summarize text."""
|
|
return await _handle(ai_summarize, req)
|
|
|
|
|
|
@router.post("/continue", response_model=AIResponse)
|
|
async def api_continue(req: AIRequest):
|
|
"""Continue writing from the selected text."""
|
|
return await _handle(ai_continue_writing, req)
|
|
|
|
|
|
@router.post("/rewrite", response_model=AIResponse)
|
|
async def api_rewrite(req: AIRequest):
|
|
"""Custom rewrite with instruction. Requires `instruction` field."""
|
|
if not req.instruction:
|
|
raise HTTPException(status_code=400, detail="Field 'instruction' is required")
|
|
return await _handle(lambda text, p: ai_custom_rewrite(text, req.instruction, p), req)
|
|
|
|
|
|
@router.post("/to-list", response_model=AIResponse)
|
|
async def api_to_list(req: AIRequest):
|
|
"""Convert text to a markdown list."""
|
|
return await _handle(ai_convert_to_list, req)
|
|
|
|
|
|
@router.post("/to-table", response_model=AIResponse)
|
|
async def api_to_table(req: AIRequest):
|
|
"""Convert text to a markdown table."""
|
|
return await _handle(ai_convert_to_table, req)
|
|
|
|
|
|
@router.post("/frontmatter", response_model=AIResponse)
|
|
async def api_frontmatter(req: AIRequest):
|
|
"""Generate YAML frontmatter."""
|
|
return await _handle(ai_generate_frontmatter, req)
|
|
|
|
|
|
@router.post("/inline-complete", response_model=AIResponse)
|
|
async def api_inline_complete(req: AIRequest):
|
|
"""Inline completion."""
|
|
return await _handle(ai_inline_complete, req)
|
|
|
|
|
|
@router.post("/to-canvas", response_model=AIResponse)
|
|
async def api_to_canvas(req: AIRequest):
|
|
"""Convert to Mermaid diagram or outline."""
|
|
return await _handle(ai_convert_to_canvas, req)
|