"""Unified Search plugin contract — public interface for cross-plugin access.""" from __future__ import annotations from typing import Any from app.plugins.builtins.contracts import get_contract_registry from app.plugins.builtins.unified_search.base_provider import BaseSearchProvider from app.plugins.builtins.unified_search.embedding import generate_embedding from app.plugins.builtins.unified_search.provider_registry import get_search_registry from app.plugins.builtins.unified_search.query_understanding import llm_analyze_query from app.plugins.builtins.unified_search.search_engine import find_similar_all_types, hybrid_search async def simple_search( db: Any, query: str, tenant_id: Any, entity_types: list[str] | None = None, limit: int = 20, user_id: Any | None = None, is_system_admin: bool = False, ) -> list[dict[str, Any]]: """Convenience search entry point: analyze a raw query string and run the hybrid search over all registered providers. Falls back to a plain normalized-query analysis when the LLM is unavailable. """ analysis = await llm_analyze_query(query, db=db, tenant_id=tenant_id) return await hybrid_search( db=db, query_analysis=analysis, tenant_id=tenant_id, entity_types=entity_types, limit=limit, user_id=user_id, is_system_admin=is_system_admin, ) class UnifiedSearchContract: """Public contract for the unified_search plugin.""" contract_name = "unified_search" generate_embedding = staticmethod(generate_embedding) hybrid_search = staticmethod(hybrid_search) find_similar_all_types = staticmethod(find_similar_all_types) get_search_registry = staticmethod(get_search_registry) llm_analyze_query = staticmethod(llm_analyze_query) simple_search = staticmethod(simple_search) BaseSearchProvider = BaseSearchProvider @staticmethod async def auto_register_providers(db: Any) -> None: """Register providers for all active plugins (worker startup path).""" from app.plugins.builtins.unified_search.provider_registry import ( auto_register_providers as _auto_register, ) await _auto_register(db) # ─── Workspace Scopes contribution (Phase N4) ─── @staticmethod def workspace_scopes() -> list[dict]: """Scope-Dimensionen des search-Moduls: Suchbereiche (N4). Options come from the live search provider registry; when it has not been initialized yet (sync context before activation), the built-in provider classes are the deterministic fallback source (same classes auto_register_providers registers at activation). """ entity_types = list(get_search_registry().get_entity_types()) if not entity_types: from app.plugins.builtins.unified_search.providers import ( agent_memory_provider, ai_chat_provider, company_provider, contact_provider, contactperson_provider, conversation_provider, event_provider, file_provider, mail_provider, tag_provider, task_provider, user_provider, workflow_provider, ) for module in ( agent_memory_provider, ai_chat_provider, company_provider, contact_provider, contactperson_provider, conversation_provider, event_provider, file_provider, mail_provider, tag_provider, task_provider, user_provider, workflow_provider, ): for attr in dir(module): obj = getattr(module, attr) if ( isinstance(obj, type) and attr.endswith("Provider") and attr != "BaseSearchProvider" and getattr(obj, "entity_type", "") ): entity_types.append(obj.entity_type) entity_types = sorted(set(entity_types)) return [ { "module_key": "search", "dimensions": [ { "key": "entity_types", "label": "Suchbereiche", "control": "multiselect", "options": [ {"value": et, "label": et.replace("_", " ").title()} for et in entity_types ], }, ], } ] @classmethod def get_function(cls, name: str): """Return a callable exposed by this contract, or None if absent.""" return getattr(cls, name, None) # ─── self-registration ─── _contract = UnifiedSearchContract() get_contract_registry().register("unified_search", _contract) # Backward-compatible local accessor _contract_instance: UnifiedSearchContract | None = None def get_contract() -> UnifiedSearchContract: global _contract_instance if _contract_instance is None: _contract_instance = UnifiedSearchContract() return _contract_instance __all__ = ["UnifiedSearchContract", "generate_embedding", "hybrid_search", "find_similar_all_types", "get_search_registry", "BaseSearchProvider"]