jira-ai-fixer/app/api/settings.py

177 lines
6.6 KiB
Python

"""Organization settings endpoints."""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel
import httpx
import base64
from app.core.database import get_db
from app.models.organization import Organization, OrganizationMember
from app.api.deps import require_role
router = APIRouter()
class AIConfig(BaseModel):
provider: str = "openrouter"
apiKey: str = ""
model: str = "meta-llama/llama-3.3-70b-instruct"
autoAnalyze: bool = True
autoCreatePR: bool = True
confidenceThreshold: int = 70
class SettingsUpdate(BaseModel):
ai_config: Optional[AIConfig] = None
class TestLLMRequest(BaseModel):
provider: str
api_key: str
model: str
def encrypt_key(key: str) -> str:
"""Simple obfuscation - in production use proper encryption."""
return base64.b64encode(key.encode()).decode()
def decrypt_key(encrypted: str) -> str:
"""Simple deobfuscation."""
try:
return base64.b64decode(encrypted.encode()).decode()
except:
return ""
@router.get("/{org_id}/settings")
async def get_settings(
org_id: int,
member: OrganizationMember = Depends(require_role("viewer")),
db: AsyncSession = Depends(get_db)
):
"""Get organization settings."""
result = await db.execute(select(Organization).where(Organization.id == org_id))
org = result.scalar_one_or_none()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
return {
"ai_config": {
"provider": org.ai_provider or "openrouter",
"apiKey": "***configured***" if org.ai_api_key_encrypted else "",
"model": org.ai_model or "meta-llama/llama-3.3-70b-instruct",
"autoAnalyze": org.ai_auto_analyze if org.ai_auto_analyze is not None else True,
"autoCreatePR": org.ai_auto_create_pr if org.ai_auto_create_pr is not None else True,
"confidenceThreshold": org.ai_confidence_threshold or 70,
}
}
@router.put("/{org_id}/settings")
async def update_settings(
org_id: int,
settings: SettingsUpdate,
member: OrganizationMember = Depends(require_role("admin")),
db: AsyncSession = Depends(get_db)
):
"""Update organization settings."""
result = await db.execute(select(Organization).where(Organization.id == org_id))
org = result.scalar_one_or_none()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
if settings.ai_config:
org.ai_provider = settings.ai_config.provider
org.ai_model = settings.ai_config.model
org.ai_auto_analyze = settings.ai_config.autoAnalyze
org.ai_auto_create_pr = settings.ai_config.autoCreatePR
org.ai_confidence_threshold = settings.ai_config.confidenceThreshold
# Only update key if provided and not masked
if settings.ai_config.apiKey and settings.ai_config.apiKey != "***configured***":
org.ai_api_key_encrypted = encrypt_key(settings.ai_config.apiKey)
await db.commit()
return {"message": "Settings updated"}
@router.post("/{org_id}/test-llm")
async def test_llm_connection(
org_id: int,
request: TestLLMRequest,
member: OrganizationMember = Depends(require_role("admin")),
db: AsyncSession = Depends(get_db)
):
"""Test LLM API connection."""
# Build request based on provider
if request.provider == "openrouter":
url = "https://openrouter.ai/api/v1/chat/completions"
headers = {
"Authorization": f"Bearer {request.api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://jira-fixer.startdata.com.br",
"X-Title": "JIRA AI Fixer"
}
payload = {
"model": request.model,
"messages": [{"role": "user", "content": "Say 'OK' if you can read this."}],
"max_tokens": 10
}
elif request.provider == "anthropic":
url = "https://api.anthropic.com/v1/messages"
headers = {
"x-api-key": request.api_key,
"Content-Type": "application/json",
"anthropic-version": "2023-06-01"
}
payload = {
"model": request.model,
"max_tokens": 10,
"messages": [{"role": "user", "content": "Say 'OK' if you can read this."}]
}
elif request.provider == "openai":
url = "https://api.openai.com/v1/chat/completions"
headers = {
"Authorization": f"Bearer {request.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": request.model,
"messages": [{"role": "user", "content": "Say 'OK' if you can read this."}],
"max_tokens": 10
}
elif request.provider == "groq":
url = "https://api.groq.com/openai/v1/chat/completions"
headers = {
"Authorization": f"Bearer {request.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": request.model,
"messages": [{"role": "user", "content": "Say 'OK' if you can read this."}],
"max_tokens": 10
}
elif request.provider == "google":
url = f"https://generativelanguage.googleapis.com/v1beta/models/{request.model}:generateContent?key={request.api_key}"
headers = {"Content-Type": "application/json"}
payload = {
"contents": [{"parts": [{"text": "Say 'OK' if you can read this."}]}],
"generationConfig": {"maxOutputTokens": 10}
}
else:
raise HTTPException(status_code=400, detail=f"Unsupported provider: {request.provider}")
try:
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=headers, json=payload, timeout=15.0)
if response.status_code == 200:
return {"success": True, "message": "Connection successful"}
elif response.status_code == 401:
raise HTTPException(status_code=400, detail="Invalid API key")
elif response.status_code == 403:
raise HTTPException(status_code=400, detail="API key lacks permissions")
else:
error_detail = response.json().get("error", {}).get("message", response.text[:200])
raise HTTPException(status_code=400, detail=f"API error: {error_detail}")
except httpx.TimeoutException:
raise HTTPException(status_code=400, detail="Connection timeout")
except httpx.ConnectError:
raise HTTPException(status_code=400, detail="Could not connect to API")