diff --git a/app/ai/action_mapper.py b/app/ai/action_mapper.py index c257db3..fc5bfcd 100644 --- a/app/ai/action_mapper.py +++ b/app/ai/action_mapper.py @@ -6,9 +6,12 @@ Supports keyword-based intent detection for common CRM operations. from __future__ import annotations +import logging import re from typing import Any +logger = logging.getLogger(__name__) + # Precompiled patterns for intent detection _PATTERNS = { "create_contact": re.compile( @@ -27,6 +30,37 @@ _PATTERNS = { "help": re.compile(r"\b(help|what can you do|assist)\b", re.IGNORECASE), } +# Plugin-contributed intents: pattern -> callable(query, context) -> list[dict] | None. +# Registered via ``register_intent_pattern`` so plugins can extend the fallback +# mapper without touching core code (Block H / HC-A). +_CONTRIBUTED_INTENTS: list[tuple[re.Pattern[str], Any]] = [] + + +def register_intent_pattern( + pattern: str | re.Pattern[str], + handler: Any, + *, + owner: str = "", +) -> None: + """Register a plugin-contributed intent for the fallback action mapper. + + Args: + pattern: Regex (compiled or raw string) matching the user query. + handler: Callable ``(query, context) -> list[dict] | None`` producing + proposed actions when the pattern matches. + owner: Optional plugin name, used by ``unregister_intent_patterns``. + """ + compiled = re.compile(pattern) if isinstance(pattern, str) else pattern + _CONTRIBUTED_INTENTS.append((compiled, handler)) + + +def unregister_intent_patterns(owner: str) -> None: + """Remove all intents contributed by ``owner`` (plugin deactivation).""" + global _CONTRIBUTED_INTENTS + _CONTRIBUTED_INTENTS = [ + entry for entry in _CONTRIBUTED_INTENTS if getattr(entry[1], "owner_tag", None) != owner + ] + # Name extraction patterns - using single-quoted strings to avoid escaping issues _NAME_PATTERNS = [ re.compile(r"\b(?:named|called|for)\s+['\"]?([^'\".,]+)['\"]?", re.IGNORECASE), @@ -147,6 +181,16 @@ def map_query_to_actions(query: str, context: dict[str, Any] | None = None) -> l } ) + # --- Plugin-contributed intents (Block H / HC-A) --- + for pattern, handler in _CONTRIBUTED_INTENTS: + try: + if pattern.search(q): + contributed = handler(query, context) + if contributed: + actions.extend(contributed) + except Exception: + logger.warning("Contributed intent handler failed", exc_info=True) + # --- Generic fallback --- if not actions: if _PATTERNS["help"].search(q):