642 lines
23 KiB
Python
642 lines
23 KiB
Python
"""
|
|
Settings and User Management API Router
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from typing import List, Optional, Dict
|
|
from pydantic import BaseModel
|
|
from app.core.database import execute_query
|
|
from app.core.config import settings
|
|
import httpx
|
|
import time
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
|
|
# Pydantic Models
|
|
class Setting(BaseModel):
|
|
id: int
|
|
key: str
|
|
value: Optional[str]
|
|
category: str
|
|
description: Optional[str]
|
|
value_type: str
|
|
is_public: bool
|
|
|
|
|
|
class SettingUpdate(BaseModel):
|
|
value: str
|
|
|
|
|
|
class User(BaseModel):
|
|
id: int
|
|
username: str
|
|
email: Optional[str]
|
|
full_name: Optional[str]
|
|
is_active: bool
|
|
last_login: Optional[str]
|
|
created_at: str
|
|
|
|
|
|
class UserCreate(BaseModel):
|
|
username: str
|
|
email: str
|
|
password: str
|
|
full_name: Optional[str] = None
|
|
|
|
|
|
class UserUpdate(BaseModel):
|
|
email: Optional[str] = None
|
|
full_name: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
# Settings Endpoints
|
|
@router.get("/settings", response_model=List[Setting], tags=["Settings"])
|
|
async def get_settings(category: Optional[str] = None):
|
|
"""Get all settings or filter by category"""
|
|
query = "SELECT * FROM settings"
|
|
params = []
|
|
|
|
if category:
|
|
query += " WHERE category = %s"
|
|
params.append(category)
|
|
|
|
query += " ORDER BY category, key"
|
|
result = execute_query(query, tuple(params) if params else None)
|
|
return result or []
|
|
|
|
|
|
@router.get("/settings/{key}", response_model=Setting, tags=["Settings"])
|
|
async def get_setting(key: str):
|
|
"""Get a specific setting by key"""
|
|
query = "SELECT * FROM settings WHERE key = %s"
|
|
result = execute_query(query, (key,))
|
|
|
|
if not result and key in {"case_types", "case_type_module_defaults"}:
|
|
seed_query = """
|
|
INSERT INTO settings (key, value, category, description, value_type, is_public)
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT (key) DO NOTHING
|
|
"""
|
|
|
|
if key == "case_types":
|
|
execute_query(
|
|
seed_query,
|
|
(
|
|
"case_types",
|
|
'["ticket", "opgave", "ordre", "projekt", "service"]',
|
|
"system",
|
|
"Sags-typer",
|
|
"json",
|
|
True,
|
|
)
|
|
)
|
|
|
|
if key == "case_type_module_defaults":
|
|
execute_query(
|
|
seed_query,
|
|
(
|
|
"case_type_module_defaults",
|
|
'{"ticket": ["relations", "call-history", "files", "emails", "hardware", "locations", "contacts", "customers", "wiki", "todo-steps", "time", "solution", "sales", "subscription", "reminders", "calendar"], "opgave": ["relations", "call-history", "files", "emails", "hardware", "locations", "contacts", "customers", "wiki", "todo-steps", "time", "solution", "sales", "subscription", "reminders", "calendar"], "ordre": ["relations", "call-history", "files", "emails", "hardware", "locations", "contacts", "customers", "wiki", "todo-steps", "time", "solution", "sales", "subscription", "reminders", "calendar"], "projekt": ["relations", "call-history", "files", "emails", "hardware", "locations", "contacts", "customers", "wiki", "todo-steps", "time", "solution", "sales", "subscription", "reminders", "calendar"], "service": ["relations", "call-history", "files", "emails", "hardware", "locations", "contacts", "customers", "wiki", "todo-steps", "time", "solution", "sales", "subscription", "reminders", "calendar"]}',
|
|
"system",
|
|
"Standard moduler pr. sagstype",
|
|
"json",
|
|
True,
|
|
)
|
|
)
|
|
|
|
result = execute_query(query, (key,))
|
|
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Setting not found")
|
|
|
|
return result[0]
|
|
|
|
|
|
@router.put("/settings/{key}", response_model=Setting, tags=["Settings"])
|
|
async def update_setting(key: str, setting: SettingUpdate):
|
|
"""Update a setting value"""
|
|
query = """
|
|
UPDATE settings
|
|
SET value = %s, updated_at = CURRENT_TIMESTAMP
|
|
WHERE key = %s
|
|
RETURNING *
|
|
"""
|
|
result = execute_query(query, (setting.value, key))
|
|
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Setting not found")
|
|
|
|
logger.info(f"✅ Updated setting: {key}")
|
|
return result[0]
|
|
|
|
|
|
@router.get("/settings/categories/list", tags=["Settings"])
|
|
async def get_setting_categories():
|
|
"""Get list of all setting categories"""
|
|
query = "SELECT DISTINCT category FROM settings ORDER BY category"
|
|
result = execute_query(query)
|
|
return [row['category'] for row in result] if result else []
|
|
|
|
|
|
@router.post("/settings/sync-from-env", tags=["Settings"])
|
|
async def sync_settings_from_env():
|
|
"""Sync settings from .env file into database (only updates empty values)"""
|
|
from app.core.config import settings as env_settings
|
|
|
|
mapping = {
|
|
'vtiger_enabled': str(env_settings.VTIGER_ENABLED).lower(),
|
|
'vtiger_url': env_settings.VTIGER_URL or '',
|
|
'vtiger_username': env_settings.VTIGER_USERNAME or '',
|
|
'economic_enabled': str(env_settings.ECONOMIC_ENABLED).lower(),
|
|
'economic_app_secret': env_settings.ECONOMIC_APP_SECRET_TOKEN or '',
|
|
'economic_agreement_token': env_settings.ECONOMIC_AGREEMENT_GRANT_TOKEN or '',
|
|
}
|
|
|
|
updated_count = 0
|
|
for key, value in mapping.items():
|
|
# Only update if current value is empty or NULL
|
|
query = """
|
|
UPDATE settings
|
|
SET value = %s, updated_at = CURRENT_TIMESTAMP
|
|
WHERE key = %s AND (value IS NULL OR value = '')
|
|
RETURNING key
|
|
"""
|
|
result = execute_query(query, (value, key))
|
|
if result:
|
|
updated_count += 1
|
|
logger.info(f"✅ Synced {key} from .env")
|
|
|
|
return {
|
|
"message": f"Synced {updated_count} settings from .env file",
|
|
"updated_count": updated_count
|
|
}
|
|
|
|
|
|
# User Management Endpoints
|
|
@router.get("/users", response_model=List[User], tags=["Users"])
|
|
async def get_users(is_active: Optional[bool] = None):
|
|
"""Get all users"""
|
|
query = "SELECT user_id as id, username, email, full_name, is_active, last_login_at as last_login, created_at FROM users"
|
|
params = []
|
|
|
|
if is_active is not None:
|
|
query += " WHERE is_active = %s"
|
|
params.append(is_active)
|
|
|
|
query += " ORDER BY username"
|
|
result = execute_query(query, tuple(params) if params else None)
|
|
return result or []
|
|
|
|
|
|
@router.get("/users/{user_id}", response_model=User, tags=["Users"])
|
|
async def get_user(user_id: int):
|
|
"""Get user by ID"""
|
|
query = "SELECT user_id as id, username, email, full_name, is_active, last_login_at as last_login, created_at FROM users WHERE user_id = %s"
|
|
result = execute_query(query, (user_id,))
|
|
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
return result[0]
|
|
|
|
|
|
@router.post("/users", response_model=User, tags=["Users"])
|
|
async def create_user(user: UserCreate):
|
|
"""Create a new user"""
|
|
# Check if username exists
|
|
existing = execute_query("SELECT user_id FROM users WHERE username = %s", (user.username,))
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="Username already exists")
|
|
|
|
# Hash password (simple SHA256 for now - should use bcrypt in production)
|
|
import hashlib
|
|
password_hash = hashlib.sha256(user.password.encode()).hexdigest()
|
|
|
|
query = """
|
|
INSERT INTO users (username, email, password_hash, full_name, is_active)
|
|
VALUES (%s, %s, %s, %s, true)
|
|
RETURNING user_id as id, username, email, full_name, is_active, last_login_at as last_login, created_at
|
|
"""
|
|
result = execute_query(query, (user.username, user.email, password_hash, user.full_name))
|
|
|
|
if not result:
|
|
raise HTTPException(status_code=500, detail="Failed to create user")
|
|
|
|
logger.info(f"✅ Created user: {user.username}")
|
|
return result[0]
|
|
|
|
|
|
@router.put("/users/{user_id}", response_model=User, tags=["Users"])
|
|
async def update_user(user_id: int, user: UserUpdate):
|
|
"""Update user details"""
|
|
# Check if user exists
|
|
existing = execute_query("SELECT user_id FROM users WHERE user_id = %s", (user_id,))
|
|
if not existing:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
# Build update query
|
|
update_fields = []
|
|
params = []
|
|
|
|
if user.email is not None:
|
|
update_fields.append("email = %s")
|
|
params.append(user.email)
|
|
if user.full_name is not None:
|
|
update_fields.append("full_name = %s")
|
|
params.append(user.full_name)
|
|
if user.is_active is not None:
|
|
update_fields.append("is_active = %s")
|
|
params.append(user.is_active)
|
|
|
|
if not update_fields:
|
|
raise HTTPException(status_code=400, detail="No fields to update")
|
|
|
|
params.append(user_id)
|
|
query = f"""
|
|
UPDATE users
|
|
SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP
|
|
WHERE user_id = %s
|
|
RETURNING user_id as id, username, email, full_name, is_active, last_login_at as last_login, created_at
|
|
"""
|
|
|
|
result = execute_query(query, tuple(params))
|
|
|
|
if not result:
|
|
raise HTTPException(status_code=500, detail="Failed to update user")
|
|
|
|
logger.info(f"✅ Updated user: {user_id}")
|
|
return result[0]
|
|
|
|
|
|
@router.delete("/users/{user_id}", tags=["Users"])
|
|
async def deactivate_user(user_id: int):
|
|
"""Deactivate a user (soft delete)"""
|
|
query = """
|
|
UPDATE users
|
|
SET is_active = false, updated_at = CURRENT_TIMESTAMP
|
|
WHERE user_id = %s
|
|
RETURNING user_id as id
|
|
"""
|
|
result = execute_query(query, (user_id,))
|
|
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
logger.info(f"✅ Deactivated user: {user_id}")
|
|
return {"message": "User deactivated successfully"}
|
|
|
|
|
|
@router.post("/users/{user_id}/reset-password", tags=["Users"])
|
|
async def reset_user_password(user_id: int, new_password: str):
|
|
"""Reset user password"""
|
|
import hashlib
|
|
password_hash = hashlib.sha256(new_password.encode()).hexdigest()
|
|
|
|
query = """
|
|
UPDATE users
|
|
SET password_hash = %s, updated_at = CURRENT_TIMESTAMP
|
|
WHERE user_id = %s
|
|
RETURNING user_id as id
|
|
"""
|
|
result = execute_query(query, (password_hash, user_id))
|
|
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
logger.info(f"✅ Reset password for user: {user_id}")
|
|
return {"message": "Password reset successfully"}
|
|
|
|
|
|
# AI Prompts Management
|
|
|
|
def _get_default_prompts():
|
|
"""Helper to get default system prompts"""
|
|
from app.services.ollama_service import OllamaService
|
|
ollama_service = OllamaService()
|
|
|
|
return {
|
|
"invoice_extraction": {
|
|
"name": "📄 Faktura Udtrækning (Invoice Parser)",
|
|
"description": "System prompt brugt til at udtrække data fra fakturaer og kreditnotaer via Ollama LLM. Håndterer danske nummerformater, datoer og linjegenkendelse.",
|
|
"model": ollama_service.model,
|
|
"endpoint": ollama_service.endpoint,
|
|
"prompt": ollama_service._build_system_prompt(),
|
|
"parameters": {
|
|
"temperature": 0.1,
|
|
"top_p": 0.9,
|
|
"num_predict": 2000
|
|
}
|
|
},
|
|
"ticket_classification": {
|
|
"name": "🎫 Ticket Klassificering (Auto-Triage)",
|
|
"description": "Klassificerer indkomne tickets baseret på emne og indhold. Tildeler kategori, prioritet og ansvarlig team.",
|
|
"model": ollama_service.model,
|
|
"endpoint": ollama_service.endpoint,
|
|
"prompt": """Du er en erfaren IT-supporter der skal klassificere indkomne support-sager.
|
|
|
|
Dine opgaver er:
|
|
1. Analyser emne og beskrivelse
|
|
2. Bestem Kategori: [Hardware, Software, Netværk, Adgang, Andet]
|
|
3. Bestem Prioritet: [Lav, Mellem, Høj, Kritisk]
|
|
4. Foreslå handlingsplan (kort punktform)
|
|
|
|
Output skal være gyldig JSON:
|
|
{
|
|
"category": "string",
|
|
"priority": "string",
|
|
"summary": "string",
|
|
"suggested_actions": ["string"]
|
|
}""",
|
|
"parameters": {
|
|
"temperature": 0.3,
|
|
"top_p": 0.95,
|
|
"num_predict": 1000
|
|
}
|
|
},
|
|
"ticket_summary": {
|
|
"name": "📝 Ticket Summering (Fakturagrundlag)",
|
|
"description": "Analyserer alle kommentarer og noter i en ticket for at lave et kort, præcist resumé til fakturaen eller kunden.",
|
|
"model": ollama_service.model,
|
|
"endpoint": ollama_service.endpoint,
|
|
"prompt": """Du er en administrativ assistent der skal gøre en it-sag klar til fakturering.
|
|
|
|
Opgave: Læs historikken igennem og skriv et kort resumé af det udførte arbejde.
|
|
- Fokusér på løsningen, ikke problemet
|
|
- Brug professionelt sprog
|
|
- Undlad interne tekniske detaljer (medmindre relevant for faktura)
|
|
- Sprog: Dansk
|
|
- Længde: 2-3 sætninger
|
|
|
|
Input: [Liste af kommentarer]
|
|
Output: [Fakturastekst]""",
|
|
"parameters": {
|
|
"temperature": 0.2,
|
|
"top_p": 0.9,
|
|
"num_predict": 500
|
|
}
|
|
},
|
|
"kb_generation": {
|
|
"name": "📚 Vidensbank Generator (Solution to Article)",
|
|
"description": "Omdanner en løst ticket til en generel vejledning til vidensbanken.",
|
|
"model": ollama_service.model,
|
|
"endpoint": ollama_service.endpoint,
|
|
"prompt": """Du er teknisk forfatter. Din opgave er at omskrive en konkret support-sag til en generel vejledning.
|
|
|
|
Regler:
|
|
1. Fjern alle kunde-specifikke data (navne, IP-adresser, passwords)
|
|
2. Strukturer som:
|
|
- Problem
|
|
- Årsag (hvis kendt)
|
|
- Løsning (Trin-for-trin guide)
|
|
3. Brug letforståeligt dansk
|
|
4. Formater med Markdown
|
|
|
|
Input: [Ticket Beskrivelse + Løsning]
|
|
Output: [Markdown Guide]""",
|
|
"parameters": {
|
|
"temperature": 0.4,
|
|
"top_p": 0.9,
|
|
"num_predict": 2000
|
|
}
|
|
},
|
|
"troubleshooting_assistant": {
|
|
"name": "🔧 Fejlsøgnings Copilot (Tech Helper)",
|
|
"description": "Fungerer som en senior-tekniker der giver sparring på en fejlbeskrivelse. Foreslår konkrete fejlsøgningstrin og kommandoer.",
|
|
"model": ollama_service.model,
|
|
"endpoint": ollama_service.endpoint,
|
|
"prompt": """Du er en Senior Systemadministrator med 20 års erfaring.
|
|
En junior-tekniker spørger om hjælp til et problem.
|
|
|
|
Din opgave:
|
|
1. Analyser symptomerne
|
|
2. List de 3 mest sandsynlige årsager
|
|
3. Foreslå en trin-for-trin fejlsøgningsplan (start med det mest sandsynlige)
|
|
4. Nævn relevante værktøjer eller kommandoer (Windows/Linux/Network)
|
|
|
|
Vær kortfattet, teknisk præcis og handlingsorienteret.
|
|
Sprog: Dansk (men engelske fagtermer er OK).
|
|
|
|
Input: [Fejlbeskrivelse]
|
|
Output: [Markdown Guide]""",
|
|
"parameters": {
|
|
"temperature": 0.3,
|
|
"top_p": 0.9,
|
|
"num_predict": 1500
|
|
}
|
|
},
|
|
"sentiment_analysis": {
|
|
"name": "🌡️ Sentiment Analyse (Kunde-Humør)",
|
|
"description": "Analyserer tonen i en kundehenvendelse for at vurdere hast, frustration og risiko. Bruges til prioritering.",
|
|
"model": ollama_service.model,
|
|
"endpoint": ollama_service.endpoint,
|
|
"prompt": """Analyser tonen i følgende tekst fra en kunde.
|
|
|
|
Bestem følgende:
|
|
1. Sentiment: [Positiv, Neutral, Frustreret, Vred]
|
|
2. Hastegrad-indikatorer: Er der ord der indikerer panik eller kritisk hast?
|
|
3. Risikovurdering (0-10): Hvor stor risiko er der for at kunden forlader os? (10=Høj)
|
|
|
|
Returner resultatet som JSON format.
|
|
|
|
Input: [Kunde Tekst]
|
|
Output: { "sentiment": "...", "urgency": "...", "risk_score": 0 }""",
|
|
"parameters": {
|
|
"temperature": 0.1,
|
|
"top_p": 0.9,
|
|
"num_predict": 500
|
|
}
|
|
},
|
|
"meeting_action_items": {
|
|
"name": "📋 Mødenoter til Opgaver (Action Extraction)",
|
|
"description": "Scanner rå mødereferater eller notater og udtrækker konkrete 'Action Items', deadlines og ansvarlige personer.",
|
|
"model": ollama_service.model,
|
|
"endpoint": ollama_service.endpoint,
|
|
"prompt": """Du er en effektiv projektleder-assistent.
|
|
Din opgave er at scanne mødereferater og udtrække "Action Items".
|
|
|
|
For hver opgave skal du finde:
|
|
- Aktivitet (Hvad skal gøres?)
|
|
- Ansvarlig (Hvem?)
|
|
- Deadline (Hvornår? Hvis nævnt)
|
|
|
|
Ignorer løs snak og diskussioner. Fokusér kun på beslutninger og opgaver der skal udføres.
|
|
Outputtet skal være en punktopstilling.
|
|
|
|
Input: [Mødenoter]
|
|
Output: [Liste af opgaver]""",
|
|
"parameters": {
|
|
"temperature": 0.2,
|
|
"top_p": 0.9,
|
|
"num_predict": 1000
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def _get_prompts_with_overrides() -> Dict:
|
|
"""Get AI prompts with DB overrides applied"""
|
|
prompts = _get_default_prompts()
|
|
|
|
try:
|
|
rows = execute_query("SELECT key, prompt_text FROM ai_prompts")
|
|
if rows:
|
|
for row in rows:
|
|
if row['key'] in prompts:
|
|
prompts[row['key']]['prompt'] = row['prompt_text']
|
|
prompts[row['key']]['is_custom'] = True
|
|
except Exception as e:
|
|
logger.warning(f"Could not load custom ai prompts: {e}")
|
|
|
|
return prompts
|
|
|
|
|
|
def _get_test_input_for_prompt(key: str) -> str:
|
|
"""Default test input per prompt type"""
|
|
examples = {
|
|
"invoice_extraction": "FAKTURA 2026-1001 fra Demo A/S. CVR 12345678. Total 1.250,00 DKK inkl moms.",
|
|
"ticket_classification": "Emne: Kan ikke logge på VPN. Beskrivelse: Flere brugere er ramt siden i morges.",
|
|
"ticket_summary": "Bruger havde netværksfejl. Router genstartet og DNS opdateret. Forbindelse virker nu stabilt.",
|
|
"kb_generation": "Problem: Outlook åbner ikke. Løsning: Reparer Office installation og nulstil profil.",
|
|
"troubleshooting_assistant": "Server svarer langsomt efter opdatering. CPU er høj, disk IO er normal.",
|
|
"sentiment_analysis": "Jeg er meget frustreret, systemet er nede igen og vi mister kunder!",
|
|
"meeting_action_items": "Peter opdaterer firewall fredag. Anna sender status til kunden mandag.",
|
|
}
|
|
return examples.get(key, "Skriv kort: AI test OK")
|
|
|
|
|
|
|
|
@router.get("/ai-prompts", tags=["Settings"])
|
|
async def get_ai_prompts():
|
|
"""Get all AI prompts (defaults merged with custom overrides)"""
|
|
return _get_prompts_with_overrides()
|
|
|
|
|
|
class PromptUpdate(BaseModel):
|
|
prompt_text: str
|
|
|
|
|
|
class PromptTestRequest(BaseModel):
|
|
test_input: Optional[str] = None
|
|
prompt_text: Optional[str] = None
|
|
|
|
|
|
@router.put("/ai-prompts/{key}", tags=["Settings"])
|
|
async def update_ai_prompt(key: str, update: PromptUpdate):
|
|
"""Override a system prompt with a custom one"""
|
|
defaults = _get_default_prompts()
|
|
if key not in defaults:
|
|
raise HTTPException(status_code=404, detail="Unknown prompt key")
|
|
|
|
try:
|
|
# Upsert
|
|
query = """
|
|
INSERT INTO ai_prompts (key, prompt_text, updated_at)
|
|
VALUES (%s, %s, CURRENT_TIMESTAMP)
|
|
ON CONFLICT (key)
|
|
DO UPDATE SET prompt_text = EXCLUDED.prompt_text, updated_at = CURRENT_TIMESTAMP
|
|
RETURNING key
|
|
"""
|
|
execute_query(query, (key, update.prompt_text))
|
|
return {"message": "Prompt updated", "key": key}
|
|
except Exception as e:
|
|
logger.error(f"Error saving prompt: {e}")
|
|
raise HTTPException(status_code=500, detail="Could not save prompt")
|
|
|
|
|
|
@router.delete("/ai-prompts/{key}", tags=["Settings"])
|
|
async def reset_ai_prompt(key: str):
|
|
"""Reset a prompt to its system default"""
|
|
try:
|
|
execute_query("DELETE FROM ai_prompts WHERE key = %s", (key,))
|
|
return {"message": "Prompt reset to default"}
|
|
except Exception as e:
|
|
logger.error(f"Error resetting prompt: {e}")
|
|
raise HTTPException(status_code=500, detail="Could not reset prompt")
|
|
|
|
|
|
@router.post("/ai-prompts/{key}/test", tags=["Settings"])
|
|
async def test_ai_prompt(key: str, payload: PromptTestRequest):
|
|
"""Run a quick AI test for a specific system prompt"""
|
|
prompts = _get_prompts_with_overrides()
|
|
if key not in prompts:
|
|
raise HTTPException(status_code=404, detail="Unknown prompt key")
|
|
|
|
prompt_cfg = prompts[key]
|
|
model = prompt_cfg.get("model") or settings.OLLAMA_MODEL
|
|
endpoint = prompt_cfg.get("endpoint") or settings.OLLAMA_ENDPOINT
|
|
prompt_text = (payload.prompt_text or prompt_cfg.get("prompt") or "").strip()
|
|
if not prompt_text:
|
|
raise HTTPException(status_code=400, detail="Prompt text is empty")
|
|
|
|
test_input = (payload.test_input or _get_test_input_for_prompt(key)).strip()
|
|
if not test_input:
|
|
raise HTTPException(status_code=400, detail="Test input is empty")
|
|
|
|
start = time.perf_counter()
|
|
try:
|
|
use_chat_api = model.startswith("qwen3")
|
|
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
if use_chat_api:
|
|
response = await client.post(
|
|
f"{endpoint}/api/chat",
|
|
json={
|
|
"model": model,
|
|
"messages": [
|
|
{"role": "system", "content": prompt_text},
|
|
{"role": "user", "content": test_input},
|
|
],
|
|
"stream": False,
|
|
"options": {"temperature": 0.2, "num_predict": 600},
|
|
},
|
|
)
|
|
else:
|
|
response = await client.post(
|
|
f"{endpoint}/api/generate",
|
|
json={
|
|
"model": model,
|
|
"prompt": f"{prompt_text}\n\nBrugerinput:\n{test_input}",
|
|
"stream": False,
|
|
"options": {"temperature": 0.2, "num_predict": 600},
|
|
},
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
raise HTTPException(
|
|
status_code=502,
|
|
detail=f"AI endpoint fejl: {response.status_code} - {response.text[:300]}",
|
|
)
|
|
|
|
data = response.json()
|
|
if use_chat_api:
|
|
message_data = data.get("message", {})
|
|
ai_response = (message_data.get("content") or message_data.get("thinking") or "").strip()
|
|
else:
|
|
ai_response = (data.get("response") or "").strip()
|
|
|
|
if not ai_response:
|
|
raise HTTPException(status_code=502, detail="AI returnerede tomt svar")
|
|
|
|
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
return {
|
|
"ok": True,
|
|
"key": key,
|
|
"model": model,
|
|
"endpoint": endpoint,
|
|
"test_input": test_input,
|
|
"ai_response": ai_response,
|
|
"latency_ms": latency_ms,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"❌ AI prompt test failed for {key}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Kunne ikke teste AI prompt: {str(e)}")
|
|
|
|
|