ai-agent / src /ai_agent /ui /app.py
katospiegel's picture
Deploy develop: FastAPI+React frontend, multi-stage Docker (ai_agent serve)
07c2476 verified
Raw
History Blame Contribute Delete
7.22 kB
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
# Ensure ai_agent is in path
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
# Load environment
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
# ============================================================================
# Tool registration
# ============================================================================
from ai_agent.agent.tools import ensure_tools_registered
# ============================================================================
# Pipeline initialization
# ============================================================================
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")
# ============================================================================
# Launch configuration
# ============================================================================
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()