diff --git a/RELEASE_NOTES_v2.2.64.md b/RELEASE_NOTES_v2.2.64.md new file mode 100644 index 0000000..55b8917 --- /dev/null +++ b/RELEASE_NOTES_v2.2.64.md @@ -0,0 +1,18 @@ +# Release Notes v2.2.64 + +Dato: 18. marts 2026 + +## Fixes + +- Forbedret QuickCreate robusthed når AI/LLM er utilgængelig. +- Tilføjet lokal heuristisk fallback i `CaseAnalysisService`, så brugeren stadig får: + - foreslået titel + - foreslået prioritet + - simple tags + - kunde-match forsøg +- Fjernet afhængighed af at Ollama altid svarer, så QuickCreate ikke længere ender i tom AI-unavailable flow ved midlertidige AI-fejl. + +## Berørte filer + +- `app/services/case_analysis_service.py` +- `RELEASE_NOTES_v2.2.64.md` diff --git a/app/services/case_analysis_service.py b/app/services/case_analysis_service.py index 4b00ef6..d923ad6 100644 --- a/app/services/case_analysis_service.py +++ b/app/services/case_analysis_service.py @@ -67,12 +67,12 @@ class CaseAnalysisService: return analysis else: - logger.warning("⚠️ Ollama returned no result, using empty analysis") - return self._empty_analysis(text) + logger.warning("⚠️ Ollama returned no result, using heuristic fallback analysis") + return await self._heuristic_fallback_analysis(text) except Exception as e: logger.error(f"❌ Case analysis failed: {e}", exc_info=True) - return self._empty_analysis(text) + return await self._heuristic_fallback_analysis(text) def _build_analysis_prompt(self) -> str: """Build Danish system prompt for case analysis""" @@ -470,6 +470,73 @@ Returner JSON med suggested_title, suggested_description, priority, customer_hin confidence=0.0, ai_reasoning="AI unavailable - fill fields manually" ) + + async def _heuristic_fallback_analysis(self, text: str) -> QuickCreateAnalysis: + """Local fallback when AI service is unavailable.""" + cleaned_text = (text or "").strip() + if not cleaned_text: + return self._empty_analysis(text) + + lowered = cleaned_text.lower() + + # Priority heuristics based on urgency wording. + urgent_terms = ["nede", "kritisk", "asap", "omgående", "straks", "akut", "haster"] + high_terms = ["hurtigt", "vigtigt", "snarest", "prioriter"] + low_terms = ["når i får tid", "ikke hastende", "lavprioriteret"] + + if any(term in lowered for term in urgent_terms): + priority = SagPriority.URGENT + elif any(term in lowered for term in high_terms): + priority = SagPriority.HIGH + elif any(term in lowered for term in low_terms): + priority = SagPriority.LOW + else: + priority = SagPriority.NORMAL + + # Basic title heuristic: first non-empty line/sentence, clipped to 80 chars. + first_line = cleaned_text.splitlines()[0].strip() + first_sentence = re.split(r"[.!?]", first_line)[0].strip() + title_source = first_sentence or first_line or cleaned_text + title = title_source[:80].strip() + if not title: + title = "Ny sag" + + # Lightweight keyword tags. + keyword_tags = { + "printer": "printer", + "mail": "mail", + "email": "mail", + "vpn": "vpn", + "net": "netværk", + "wifi": "wifi", + "server": "server", + "laptop": "laptop", + "adgang": "adgang", + "onboarding": "onboarding", + } + suggested_tags: List[str] = [] + for key, tag in keyword_tags.items(): + if key in lowered and tag not in suggested_tags: + suggested_tags.append(tag) + + # Try simple customer matching from long words in text. + candidate_hints = [] + for token in re.findall(r"[A-Za-z0-9ÆØÅæøå._-]{3,}", cleaned_text): + if token.lower() in {"ring", "kunde", "sag", "skal", "have", "virker", "ikke"}: + continue + candidate_hints.append(token) + customer_id, customer_name = await self._match_customer(candidate_hints[:8]) + + return QuickCreateAnalysis( + suggested_title=title, + suggested_description=cleaned_text, + suggested_priority=priority, + suggested_customer_id=customer_id, + suggested_customer_name=customer_name, + suggested_tags=suggested_tags, + confidence=0.35, + ai_reasoning="AI service unavailable - using local fallback suggestions" + ) def _get_cached_analysis(self, text: str) -> Optional[QuickCreateAnalysis]: """Get cached analysis if available and not expired"""