2025-12-06 11:04:19 +01:00
"""
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
2026-03-01 02:56:38 +01:00
from app . core . config import settings
import httpx
import time
2025-12-06 11:04:19 +01:00
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 , ) )
2026-02-09 15:30:07 +01:00
2026-02-15 11:12:58 +01:00
if not result and key in { " case_types " , " case_type_module_defaults " } :
2026-02-09 15:30:07 +01:00
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
"""
2026-02-15 11:12:58 +01:00
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 ,
)
2026-02-09 15:30:07 +01:00
)
2026-02-15 11:12:58 +01:00
2026-02-09 15:30:07 +01:00
result = execute_query ( query , ( key , ) )
2025-12-06 11:04:19 +01:00
if not result :
raise HTTPException ( status_code = 404 , detail = " Setting not found " )
2026-02-09 15:30:07 +01:00
2025-12-06 11:04:19 +01:00
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 [ ]
2025-12-22 11:04:09 +01:00
@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
}
2025-12-06 11:04:19 +01:00
# User Management Endpoints
@router.get ( " /users " , response_model = List [ User ] , tags = [ " Users " ] )
async def get_users ( is_active : Optional [ bool ] = None ) :
""" Get all users """
2026-03-07 02:39:57 +01:00
query = " SELECT user_id as id, username, email, full_name, is_active, last_login_at as last_login, created_at FROM users "
2025-12-06 11:04:19 +01:00
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 """
2026-03-07 02:39:57 +01:00
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 "
2025-12-06 11:04:19 +01:00
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 )
2026-03-07 02:39:57 +01:00
RETURNING user_id as id , username , email , full_name , is_active , last_login_at as last_login , created_at
2025-12-06 11:04:19 +01:00
"""
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
2026-03-07 02:39:57 +01:00
RETURNING user_id as id , username , email , full_name , is_active , last_login_at as last_login , created_at
2025-12-06 11:04:19 +01:00
"""
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 " }
2025-12-08 09:15:52 +01:00
2026-01-11 19:23:21 +01:00
# AI Prompts Management
def _get_default_prompts ( ) :
""" Helper to get default system prompts """
2025-12-08 09:15:52 +01:00
from app . services . ollama_service import OllamaService
ollama_service = OllamaService ( )
2026-01-11 19:23:21 +01:00
return {
2025-12-08 09:15:52 +01:00
" invoice_extraction " : {
2026-01-11 19:23:21 +01:00
" 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. " ,
2025-12-08 09:15:52 +01:00
" 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
}
2026-01-11 19:23:21 +01:00
} ,
" 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
}
2025-12-08 09:15:52 +01:00
}
}
2026-01-11 19:23:21 +01:00
2026-03-01 02:56:38 +01:00
def _get_prompts_with_overrides ( ) - > Dict :
""" Get AI prompts with DB overrides applied """
2026-01-11 19:23:21 +01:00
prompts = _get_default_prompts ( )
2026-03-01 02:56:38 +01:00
2026-01-11 19:23:21 +01:00
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 } " )
2026-03-01 02:56:38 +01:00
2025-12-08 09:15:52 +01:00
return prompts
2026-01-11 19:23:21 +01:00
2026-03-01 02:56:38 +01:00
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 ( )
2026-01-11 19:23:21 +01:00
class PromptUpdate ( BaseModel ) :
prompt_text : str
2026-03-01 02:56:38 +01:00
class PromptTestRequest ( BaseModel ) :
test_input : Optional [ str ] = None
prompt_text : Optional [ str ] = None
2026-01-11 19:23:21 +01:00
@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 " )
2026-03-01 02:56:38 +01:00
@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 :
2026-03-18 13:49:33 +01:00
model_normalized = ( model or " " ) . strip ( ) . lower ( )
# qwen models are more reliable with /api/chat than /api/generate.
use_chat_api = model_normalized . startswith ( " qwen " )
2026-03-01 02:56:38 +01:00
2026-03-18 13:49:33 +01:00
timeout = httpx . Timeout ( connect = 10.0 , read = 180.0 , write = 30.0 , pool = 10.0 )
async with httpx . AsyncClient ( timeout = timeout ) as client :
2026-03-01 02:56:38 +01:00
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 \n Brugerinput: \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 ] } " ,
)
2026-03-18 13:49:33 +01:00
try :
data = response . json ( )
except Exception as parse_error :
raise HTTPException (
status_code = 502 ,
detail = f " AI endpoint returnerede ugyldig JSON: { str ( parse_error ) } " ,
)
2026-03-01 02:56:38 +01:00
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
2026-03-18 13:49:33 +01:00
except httpx . TimeoutException as e :
logger . error ( f " ❌ AI prompt test timed out for { key } : { repr ( e ) } " )
raise HTTPException ( status_code = 504 , detail = " AI test timed out (model svarer for langsomt) " )
2026-03-01 02:56:38 +01:00
except Exception as e :
2026-03-18 13:49:33 +01:00
logger . error ( f " ❌ AI prompt test failed for { key } : { repr ( e ) } " )
err = str ( e ) or e . __class__ . __name__
raise HTTPException ( status_code = 500 , detail = f " Kunne ikke teste AI prompt: { err } " )
2026-03-01 02:56:38 +01:00