| from __future__ import annotations
|
|
|
| import os
|
| import sys
|
| import socket
|
| import logging
|
| import logging.handlers
|
| from typing import List, Dict
|
| from datetime import datetime
|
|
|
| from dotenv import load_dotenv, find_dotenv
|
|
|
|
|
| ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
| if ROOT not in sys.path:
|
| sys.path.insert(0, ROOT)
|
|
|
|
|
| load_dotenv(find_dotenv(), override=False)
|
|
|
| log = logging.getLogger("chat_app")
|
|
|
| from ai_agent.api.pipeline import RAGImagingPipeline
|
| from ai_agent.core.pipeline_registry import get_pipeline as get_shared_pipeline
|
| from ai_agent.retriever.software_doc import SoftwareDoc
|
| from ai_agent.ui.components import create_chat_interface
|
|
|
|
|
|
|
|
|
| from ai_agent.agent.tools import ensure_tools_registered
|
|
|
|
|
|
|
|
|
| INDEX_DIR = os.getenv("RAG_INDEX_DIR", "artifacts/rag_index")
|
|
|
| _pipe: RAGImagingPipeline | None = None
|
| _DOCS: List[SoftwareDoc] = []
|
| _DOC_BY_NAME: Dict[str, SoftwareDoc] = {}
|
| _logging_initialized = False
|
| _tools_registered = False
|
|
|
|
|
| def _setup_logging() -> None:
|
| """Initialize logging once for the UI process."""
|
| global _logging_initialized
|
| if _logging_initialized:
|
| return
|
|
|
| LOG_DIR = os.getenv("LOG_DIR", "logs")
|
| os.makedirs(LOG_DIR, exist_ok=True)
|
|
|
| debug_on = str(os.getenv("DEBUG", "0")).lower() in ("1", "true", "yes", "on")
|
| console_level = os.getenv("LOGLEVEL_CONSOLE", "INFO").upper()
|
| file_level = os.getenv("LOGLEVEL_FILE", "DEBUG" if debug_on else "INFO").upper()
|
| file_log_enabled = str(os.getenv("FILE_LOG", "1")).lower() in (
|
| "1",
|
| "true",
|
| "yes",
|
| "on",
|
| )
|
|
|
| fmt = logging.Formatter("%(asctime)s | %(levelname)s | %(name)s | %(message)s")
|
| root = logging.getLogger()
|
| root.handlers.clear()
|
| root.setLevel(logging.DEBUG)
|
|
|
| ch = logging.StreamHandler(sys.stdout)
|
| ch.setLevel(console_level)
|
| ch.setFormatter(fmt)
|
| root.addHandler(ch)
|
|
|
| if file_log_enabled:
|
| logfile = os.path.join(LOG_DIR, f"app_{datetime.now():%Y%m%d}.log")
|
| fh = logging.handlers.TimedRotatingFileHandler(
|
| logfile, when="midnight", backupCount=14, encoding="utf-8"
|
| )
|
| fh.setLevel(file_level)
|
| fh.setFormatter(fmt)
|
| root.addHandler(fh)
|
|
|
| _logging_initialized = True
|
| log.info("Starting Chat-based Gradio UI")
|
|
|
|
|
| def ensure_logging_initialized() -> None:
|
| """Public hook for callers that need logging before other UI setup."""
|
| _setup_logging()
|
|
|
|
|
| def _ensure_tools_registered_once() -> None:
|
| global _tools_registered
|
| if _tools_registered:
|
| return
|
| ensure_tools_registered()
|
| _tools_registered = True
|
|
|
|
|
| def get_pipeline() -> RAGImagingPipeline:
|
| """Get or initialize the pipeline singleton."""
|
| global _pipe, _DOCS, _DOC_BY_NAME
|
| if _pipe is None:
|
| _pipe = get_shared_pipeline(index_dir=INDEX_DIR)
|
| try:
|
| _DOCS = list(_pipe.index.docs.values())
|
| _DOC_BY_NAME = {d.name: d for d in _DOCS if getattr(d, "name", None)}
|
| log.info("Loaded %d tools from index", len(_DOCS))
|
| except Exception:
|
| _DOCS, _DOC_BY_NAME = [], {}
|
| log.exception("Failed to load docs from index")
|
| log.info("Pipeline ready")
|
| return _pipe
|
|
|
|
|
| def refresh_ui_docs_from_index():
|
| """Refresh doc index from FAISS."""
|
| global _pipe, _DOCS, _DOC_BY_NAME
|
| if _pipe is None:
|
| return
|
| try:
|
| _DOCS = list(_pipe.index.docs.values())
|
| _DOC_BY_NAME = {d.name: d for d in _DOCS if getattr(d, "name", None)}
|
| log.info("UI docs refreshed from FAISS: %d tools", len(_DOCS))
|
| except Exception:
|
| _DOCS, _DOC_BY_NAME = [], {}
|
| log.exception("Failed to refresh UI docs from FAISS")
|
|
|
|
|
|
|
|
|
|
|
| def _bind_host() -> str:
|
| """Determine bind host based on environment."""
|
| bind_host = os.getenv("BIND_HOST")
|
| if bind_host:
|
| return bind_host
|
| in_docker = os.path.exists("/.dockerenv")
|
| return "0.0.0.0" if in_docker else "127.0.0.1"
|
|
|
|
|
| def launch():
|
| """Launch the chat interface."""
|
| _setup_logging()
|
| _ensure_tools_registered_once()
|
| host = _bind_host()
|
| preferred = int(os.getenv("PORT", "7860"))
|
| max_tries = int(os.getenv("PORT_TRIES", "10"))
|
| allow_fallback = str(os.getenv("ALLOW_PORT_FALLBACK", "1")).lower() in (
|
| "1",
|
| "true",
|
| "yes",
|
| "on",
|
| )
|
|
|
| ui = create_chat_interface(_DOC_BY_NAME)
|
|
|
| def _is_port_available(port: int) -> bool:
|
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
| s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
| try:
|
| s.bind((host, port))
|
| return True
|
| except OSError:
|
| return False
|
|
|
| if allow_fallback:
|
| chosen_port = None
|
| for attempt in range(max_tries):
|
| candidate = preferred + attempt
|
| if _is_port_available(candidate):
|
| chosen_port = candidate
|
| break
|
| if chosen_port is not None:
|
| ui.queue(max_size=10).launch(
|
| server_name=host,
|
| server_port=chosen_port,
|
| inbrowser=False,
|
| show_error=True,
|
| share=bool(os.getenv("SHARE", False)),
|
| )
|
| if chosen_port != preferred:
|
| log.info(
|
| "Launched on fallback port %d (preferred %d was busy)",
|
| chosen_port,
|
| preferred,
|
| )
|
| return
|
|
|
| last_err = None
|
| for attempt in range(max_tries if allow_fallback else 1):
|
| port = preferred + attempt
|
| try:
|
| ui.queue(max_size=10).launch(
|
| server_name=host,
|
| server_port=port,
|
| inbrowser=False,
|
| show_error=True,
|
| share=bool(os.getenv("SHARE", False)),
|
| )
|
| if attempt > 0:
|
| log.info(
|
| "Launched on fallback port %d (preferred %d was busy)",
|
| port,
|
| preferred,
|
| )
|
| return
|
| except OSError as e:
|
| last_err = e
|
| busy = "Cannot find empty port" in str(e)
|
| if not busy or attempt == (max_tries - 1) or not allow_fallback:
|
| raise
|
| log.warning("Port %d busy; trying %d", port, port + 1)
|
|
|
| if last_err:
|
| raise last_err
|
|
|
|
|
| if __name__ == "__main__":
|
| launch()
|
|
|