177 lines
6.6 KiB
Python
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")
|