Ctrl K
ComunidadGuías…

archived/rex_assistant.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Command-line entry point for the Rex assistant.

This module intentionally mirrors the historical top-level script so existing
...
"""

# ruff: noqa: E402

import asyncio
import logging
from collections.abc import Callable, Iterable, Sequence

from utils.env_loader import load as _load_env

# Load .env before accessing any environment variables
_load_env()

from rex import settings
from rex.assistant import Assistant
from rex.llm_client import LanguageModel
from rex.logging_utils import configure_logging
from rex.plugins import PluginSpec, load_plugins, shutdown_plugins

logger = logging.getLogger(__name__)


class FunctionRouter:
    """Simple command router that maps predicates to handlers."""

    def __init__(self) -> None:
        self._routes: list[tuple[Callable[[str], bool], Callable[[str], str]]] = []

    def register(self, predicate: Callable[[str], bool], handler: Callable[[str], str]) -> None:
        self._routes.append((predicate, handler))

    def route(self, text: str) -> str | None:
        for predicate, handler in self._routes:
            try:
                if predicate(text):
                    return handler(text)
            except Exception as exc:  # pragma: no cover
                logger.exception("Command handler failed: %s", exc)
        return None


ROUTER = FunctionRouter()
LLM: LanguageModel | None = None


def _ensure_llm() -> LanguageModel:
    global LLM
    if LLM is None or not hasattr(LLM, "generate"):
        LLM = LanguageModel()
    return LLM


def handle_command(text: str) -> str | None:
    """Dispatch `text` through the registered command handlers."""
    return ROUTER.route(text)


def generate_response(prompt: str, *, messages: Sequence[dict] | None = None) -> str:
    """Generate an assistant response, with safe fallback on failure."""
    routed = handle_command(prompt)
    if routed is not None:
        return routed

    try:
        if messages is not None:
            normalised = [
                {
                    "role": str(entry.get("role", "")),
                    "content": str(entry.get("content", "")),
                }
                for entry in messages
                if isinstance(entry, dict)
            ]
            return _ensure_llm().generate(messages=normalised)
        return _ensure_llm().generate(prompt)
    except Exception as exc:  # pragma: no cover
        logger.exception("LLM generation failed: %s", exc)
        cleaned = prompt.strip()
        return f"I heard you say: {cleaned or '(silence)'}"


async def _chat_loop(assistant: Assistant) -> None:
    """Interactive CLI loop for chatting with Rex."""
    print("🎤 Rex assistant ready. Type 'exit' or 'quit' to stop.")
    while True:
        try:
            user_input = input("You: ")
        except EOFError:
            break

        if user_input.strip().lower() in {"exit", "quit"}:
            break
        if not user_input.strip():
            print("(please enter a prompt)")
            continue

        try:
            reply = await assistant.generate_reply(user_input)
        except Exception as exc:  # pragma: no cover
            logger.exception("Assistant failed to generate a reply: %s", exc)
            print(f"[error] {exc}")
            continue

        print(f"Rex: {reply}")


async def _run() -> None:
    """Configure logging, load plugins, and run the assistant loop."""
    configure_logging()
    plugin_specs: Iterable[PluginSpec] = load_plugins()
    assistant = Assistant(history_limit=settings.max_memory_items, plugins=plugin_specs)
    try:
        await _chat_loop(assistant)
    finally:
        shutdown_plugins(plugin_specs)


def main() -> int:
    """Main entry point for Rex assistant CLI."""
    try:
        asyncio.run(_run())
    except KeyboardInterrupt:
        print("\nInterrupted.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())