Files
ObsiGate/backend/ai_routes.py
T
bruno feeb46b873
CI / lint (push) Failing after 7s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / security (push) Failing after -7s
feat: finalisation éditeur Forge + mode hors-ligne PWA + consolidation roadmap
- Forge: Tab indent, Ctrl+K, auto-focus, AI status, messages erreur FR
- Forge: slash commands clean, cache-bust iframe, preview serveur
- Forge: iframe 82vh, hauteur auto, table inline, sélecteur langage
- PWA hors-ligne: IndexedDB (offline-db.js + offline.js)
- Mermaid: thème dark complet, highlight.js preview
- Backend: màj CSP, rename chain, bookmarks/history/shares
- Roadmap: consolidation dans docs/ROADMAP.md (75 items)
- Infra: docker-compose.podman.yml, suppression test-vault
- .gitignore: exclusion odysseus/
2026-06-14 20:27:37 -04:00

172 lines
6.0 KiB
Python

"""ObsiGate AI — API routes for AI-powered editor features."""
from fastapi import APIRouter, HTTPException
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.
Reads environment variables at runtime so that changes to
.env or Docker environment are reflected immediately.
"""
from backend.ai import get_ai_key
provider_keys = {
"deepseek": "DEEPSEEK_API_KEY",
"openrouter": "OPENROUTER_API_KEY",
"gemini": "GEMINI_API_KEY",
}
providers = {}
for name, env_var in provider_keys.items():
has_key = bool(get_ai_key(env_var))
providers[name] = {
"available": has_key,
"model": PROVIDERS[name]["model"] if has_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)