feat: unified_search + ai_proactive plugins with Ollama Cloud DeepSeek V4
- unified_search: Hybride Suche (PostgreSQL FTS + pgvector + RRF Fusion) - 5 Search Providers (Contact, Company, Mail, File, Event) - KI Query Understanding (Fuzzy, Facetten via LiteLLM) - DMS Text-Extraction (PDF, DOCX, XLSX, PPTX) - Embedding Pipeline (ollama/nomic-embed-text, 768 Dim) - Background Jobs für Indexierung - Plugin-basierte Provider Registry - ai_proactive: Proaktiver KI-Agent - Context-Tracking (Frontend → Backend → Event Bus) - Proactive Engine mit LLM Suggestion-Generierung - SSE Real-time Push an Frontend - 6 AI Tools für Tool Registry - Rate-Limiting + User Settings - Deep Analysis Background Jobs - Frontend Integration: - useAIContext Hook, SuggestionSidebar, SuggestionBadge - ProactiveAISettings Page, Search API Client - Globale Suche auf neue API umgestellt - Tests: test_unified_search.py + test_ai_proactive.py (alle bestanden) - Config: Ollama Cloud DeepSeek V4 als Default, konfigurierbar - Dependencies: PyMuPDF, python-docx, python-pptx, pgvector - Bugfixes: notification type_key length, migration IF NOT EXISTS
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""KI query understanding and result aggregation via LiteLLM."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import litellm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_LLM_MODEL = os.environ.get('SEARCH_LLM_MODEL', 'ollama/deepseek-v4')
|
||||
OLLAMA_API_KEY = os.environ.get('API_KEY_OLLAMA_CLOUD', '')
|
||||
|
||||
QUERY_ANALYZE_SYSTEM = (
|
||||
"Du bist ein Query-Analyzer fuer ein CRM. "
|
||||
"Analysiere die Suchanfrage und gib JSON zurueck: "
|
||||
'{"normalized_query": str, "entities": {"person": str|null, "company": str|null, "topic": str|null}, '
|
||||
'"intent": str, "semantic_terms": [str], "suggested_filters": {}}'
|
||||
)
|
||||
|
||||
RESULT_AGGREGATE_SYSTEM = (
|
||||
"Du bist ein Result-Aggregator. Fasse Ergebnisse zusammen und generiere Facetten: "
|
||||
'{"summary": str, "facets": {"types": {}, "dates": {}, "people": []}, "suggestions": [str]}'
|
||||
)
|
||||
|
||||
|
||||
def _fallback_query_analysis(query: str) -> dict[str, Any]:
|
||||
return {
|
||||
"normalized_query": query,
|
||||
"entities": {},
|
||||
"intent": "search",
|
||||
"semantic_terms": [],
|
||||
"suggested_filters": {},
|
||||
}
|
||||
|
||||
|
||||
def _fallback_aggregate(results: list[dict], query: str) -> dict[str, Any]:
|
||||
return {
|
||||
"summary": f"{len(results)} Ergebnisse gefunden",
|
||||
"facets": {},
|
||||
"suggestions": [],
|
||||
}
|
||||
|
||||
|
||||
async def llm_analyze_query(query: str) -> dict[str, Any]:
|
||||
"""Analyze a search query using LLM for intent, entities, and semantic terms.
|
||||
|
||||
Falls back to a simple dict if LLM fails.
|
||||
"""
|
||||
try:
|
||||
response = await litellm.acompletion(
|
||||
model=DEFAULT_LLM_MODEL,
|
||||
messages=[
|
||||
{"role": "system", "content": QUERY_ANALYZE_SYSTEM},
|
||||
{"role": "user", "content": query},
|
||||
],
|
||||
temperature=0.1,
|
||||
max_tokens=500,
|
||||
response_format={"type": "json_object"},
|
||||
api_key=OLLAMA_API_KEY,
|
||||
)
|
||||
content = response.choices[0].message.content
|
||||
return json.loads(content)
|
||||
except Exception:
|
||||
logger.warning("LLM query analysis failed, using fallback")
|
||||
return _fallback_query_analysis(query)
|
||||
|
||||
|
||||
async def llm_aggregate_results(results: list[dict], query: str) -> dict[str, Any]:
|
||||
"""Aggregate search results using LLM for summary, facets, and suggestions.
|
||||
|
||||
Falls back to a simple dict if LLM fails.
|
||||
"""
|
||||
if not results:
|
||||
return _fallback_aggregate(results, query)
|
||||
try:
|
||||
# Truncate results to avoid token overflow
|
||||
compact = [
|
||||
{"entity_type": r.get("entity_type"), "title": r.get("title", "")[:100]}
|
||||
for r in results[:50]
|
||||
]
|
||||
user_msg = json.dumps({"query": query, "results": compact})
|
||||
response = await litellm.acompletion(
|
||||
model=DEFAULT_LLM_MODEL,
|
||||
messages=[
|
||||
{"role": "system", "content": RESULT_AGGREGATE_SYSTEM},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
temperature=0.1,
|
||||
max_tokens=1000,
|
||||
response_format={"type": "json_object"},
|
||||
api_key=OLLAMA_API_KEY,
|
||||
)
|
||||
content = response.choices[0].message.content
|
||||
return json.loads(content)
|
||||
except Exception:
|
||||
logger.warning("LLM result aggregation failed, using fallback")
|
||||
return _fallback_aggregate(results, query)
|
||||
Reference in New Issue
Block a user