Ctrl K
ComunidadGuías…

app.py

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""
Research tool — entry point
===========================
Autonomous web research with LLM-assisted analysis and synthesis.

Privacy-related start-up settings:
  - Telemetry switches are set BEFORE `import gradio` (process level).
  - The theme uses system fonts only; no Google Fonts.
  - `share=False`, `pwa=False`, `flagging_mode="never"`.
  - Upload limits, path allow/block lists; Gradio cleans up uploads and
    downloads, stored runs are deleted after CLEANUP_MAX_AGE_DAYS.
  - HTTPS via an SSL certificate or a reverse proxy.
"""

# ════════════════════════════════════════════════════════════════════
# 1. Telemetry switches and paths — set BEFORE `import gradio`
# ════════════════════════════════════════════════════════════════════
import os
import sys

os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1")
os.environ.setdefault("DO_NOT_TRACK", "1")
os.environ.setdefault("GRADIO_FLAGGING_MODE", "never")
# GRADIO_TEMP_DIR: dedicated directory instead of /tmp.
# Overridden by AppConfig.from_env() when set there.
# The default lives in the system temp directory so that a start without
# root privileges (IDE, local venv) works. The Docker image sets
# APP_TEMP_DIR=/var/cache/research-toolset.
import tempfile


def _env_renamed(new: str, old: str, default=None):
    """Read `new`; fall back to the former name `old` for existing installations."""
    value = os.environ.get(new)
    if value is None and os.environ.get(old) is not None:
        print(f"Note: {old} is deprecated, please rename it to {new}.", file=sys.stderr)
        value = os.environ.get(old)
    return default if value is None else value


os.environ.setdefault(
    "GRADIO_TEMP_DIR",
    _env_renamed(
        "APP_TEMP_DIR", "RECHERCHE_TEMP_DIR",
        os.path.join(tempfile.gettempdir(), "research-toolset", "cache"),
    ),
)

# ════════════════════════════════════════════════════════════════════
# 2. Imports
# ════════════════════════════════════════════════════════════════════
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__)

# Imported explicitly here, after the telemetry switches above, so that the
# ordering is visible (and checked by tests/test_privacy_config.py).
import gradio as gr  # noqa: F401,E402
from src.about import TOOL_NAME, VERSION
from src.config import AppConfig
from src.ui.gradio_app import create_app, APP_THEME
from src.ui.css import CUSTOM_CSS


# ════════════════════════════════════════════════════════════════════
# 3. Main
# ════════════════════════════════════════════════════════════════════
def main():
    config = AppConfig.from_env()

    logger.info("=" * 55)
    logger.info(f"{TOOL_NAME} {VERSION}")
    logger.info("=" * 55)
    logger.info(f"Server:       {config.ui.server_name}:{config.ui.server_port}")
    logger.info(f"Path:         {config.ui.root_path}")
    logger.info(f"Primary LLM:  {config.llm.primary.model_name} @ {config.llm.primary.api_base}")
    logger.info(f"Harvest LLM:  {config.llm.harvest.model_name} @ {config.llm.harvest.api_base}")
    logger.info(f"Harvest ‖:    {config.llm.harvest_max_parallel} parallel")
    logger.info(f"SearXNG:      {config.connectors.searxng.base_url}")
    logger.info(f"Data:         {config.pipeline.data_dir}")
    logger.info(f"Temp:         {os.environ.get('GRADIO_TEMP_DIR')}")
    logger.info("=" * 55)

    # Create the directory for stored research data.
    # Gradio cleans up user uploads and download caches automatically
    # via `delete_cache=(3600, 14400)` in gr.Blocks(...)
    # and its component lifecycle management.
    os.makedirs(config.pipeline.data_dir, exist_ok=True)

    # Fail at start-up, not mid-report, if OUTPUT_LANGUAGES names a
    # language without a (valid) catalog.
    from src.output_language import enabled_languages
    logger.info("Output languages: %s", ", ".join(enabled_languages()))

    # Stored runs older than CLEANUP_MAX_AGE_DAYS are deleted at start-up
    # and periodically afterwards (0 disables this).
    from src.core.retention import start_retention_worker
    start_retention_worker(
        config.pipeline.data_dir, config.pipeline.cleanup_max_age_days,
    )

    # ──────────────────────────────────────────────────────────────
    # App + queue (configure before launch!)
    # ──────────────────────────────────────────────────────────────
    app = create_app(config)
    app.queue(
        default_concurrency_limit=4,
        max_size=64,
        api_open=False,            # do not bypass the queue via REST
        status_update_rate="auto",
    )

    # ──────────────────────────────────────────────────────────────
    # SSL configuration (optional, otherwise behind a reverse proxy)
    # ──────────────────────────────────────────────────────────────
    ssl_kwargs = {}
    ssl_certfile = _env_renamed("SERVER_SSL_CERTFILE", "RECHERCHE_SSL_CERTFILE")
    ssl_keyfile = _env_renamed("SERVER_SSL_KEYFILE", "RECHERCHE_SSL_KEYFILE")
    if ssl_certfile and ssl_keyfile:
        ssl_kwargs.update({
            "ssl_certfile": ssl_certfile,
            "ssl_keyfile": ssl_keyfile,
            "ssl_keyfile_password": _env_renamed("SERVER_SSL_PASSWORD", "RECHERCHE_SSL_PASSWORD"),
            "ssl_verify": True,
        })
        logger.info("SSL active: %s", ssl_certfile)
    else:
        logger.info("No SSL — serve HTTPS via a reverse proxy")

    # ──────────────────────────────────────────────────────────────
    # Auth (optional via Env)
    # ──────────────────────────────────────────────────────────────
    auth_kwargs = {}
    auth_user = _env_renamed("SERVER_AUTH_USER", "RECHERCHE_AUTH_USER")
    auth_pass = _env_renamed("SERVER_AUTH_PASSWORD", "RECHERCHE_AUTH_PASSWORD")
    if auth_user and auth_pass:
        auth_kwargs["auth"] = (auth_user, auth_pass)
        logger.info("Auth active for user %r", auth_user)

    # ──────────────────────────────────────────────────────────────
    # Launch — privacy-preserving parameters
    # ──────────────────────────────────────────────────────────────
    app.launch(
        # Theme and CSS — in Gradio 6 passed to launch(),
        # no longer to gr.Blocks(). System fonts instead of Google Fonts.
        theme=APP_THEME,
        css=CUSTOM_CSS,

        # Server
        server_name=config.ui.server_name,
        server_port=config.ui.server_port,
        root_path=config.ui.root_path,
        **ssl_kwargs,

        # Auth
        **auth_kwargs,

        # File-Limits
        max_file_size="20mb",
        # allowed_paths empty = only the defaults (cwd, tempfile, cache).
        # If static assets are needed, add them here explicitly.
        allowed_paths=[],
        # Safety net against path traversal. /home is deliberately not
        # listed: Gradio checks the block list before its own created
        # files, so a temp directory under /home would otherwise block
        # every download and upload.
        blocked_paths=["/etc", "/root"],

        # Privacy / Logging
        show_error=False,         # no tracebacks in the browser modal
        quiet=True,               # less server output
        footer_links=[],          # no "Built with Gradio", no API docs

        # prohibitions
        share=False,              # no external tunnels
        pwa=False,                # PWA off
        ssr_mode=False,           # SSR off (needs Node 20+)
        inbrowser=False,
        strict_cors=True,         # default, do not switch off
    )


if __name__ == "__main__":
    main()