diff --git a/VERSION b/VERSION index 5859406..530cdd9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.3 +2.2.4 diff --git a/app/settings/backend/router.py b/app/settings/backend/router.py index 8ac10e7..00d3912 100644 --- a/app/settings/backend/router.py +++ b/app/settings/backend/router.py @@ -6,6 +6,9 @@ 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__) @@ -474,15 +477,11 @@ Output: [Liste af opgaver]""", } - -@router.get("/ai-prompts", tags=["Settings"]) -async def get_ai_prompts(): - """Get all AI prompts (defaults merged with custom overrides)""" +def _get_prompts_with_overrides() -> Dict: + """Get AI prompts with DB overrides applied""" prompts = _get_default_prompts() - + try: - # Check for custom overrides in DB - # Note: Table ai_prompts must rely on migration 066 rows = execute_query("SELECT key, prompt_text FROM ai_prompts") if rows: for row in rows: @@ -491,14 +490,40 @@ async def get_ai_prompts(): 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""" @@ -533,3 +558,84 @@ async def reset_ai_prompt(key: str): 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)}") + + diff --git a/app/settings/frontend/settings.html b/app/settings/frontend/settings.html index 46e1922..38adfc9 100644 --- a/app/settings/frontend/settings.html +++ b/app/settings/frontend/settings.html @@ -2775,6 +2775,9 @@ async function loadAIPrompts() { ` : ''} + @@ -2788,6 +2791,8 @@ async function loadAIPrompts() { style="max-height: 400px; overflow-y: auto; font-size: 0.85rem; white-space: pre-wrap; border-radius: 0;">${escapeHtml(prompt.prompt)} + +