Merge branch 'main' into feat/knowledge-page-sweep
This commit is contained in:
commit
c579bd9a1a
8 changed files with 423 additions and 189 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -21,3 +21,5 @@ wheels/
|
|||
.DS_Store
|
||||
|
||||
config/
|
||||
|
||||
.docling.pid
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ services:
|
|||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile.backend
|
||||
# container_name: openrag-backend
|
||||
container_name: openrag-backend
|
||||
depends_on:
|
||||
- langflow
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ services:
|
|||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile.backend
|
||||
# container_name: openrag-backend
|
||||
container_name: openrag-backend
|
||||
depends_on:
|
||||
- langflow
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ import {
|
|||
type Nudge = string;
|
||||
|
||||
const DEFAULT_NUDGES = [
|
||||
"Show me this quarter's top 10 deals",
|
||||
"Summarize recent client interactions",
|
||||
"Search OpenSearch for mentions of our competitors",
|
||||
];
|
||||
|
||||
export const useGetNudgesQuery = (
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { ColDef, GetRowIdParams } from "ag-grid-community";
|
||||
import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react";
|
||||
import { Cloud, FileIcon, Search, Trash2, X } from "lucide-react";
|
||||
import { Cloud, FileIcon, Globe } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
type ChangeEvent,
|
||||
|
|
@ -21,7 +21,6 @@ import "@/components/AgGrid/registerAgGridModules";
|
|||
import "@/components/AgGrid/agGridStyles.css";
|
||||
import { toast } from "sonner";
|
||||
import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdown";
|
||||
import { filterAccentClasses } from "@/components/knowledge-filter-panel";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog";
|
||||
import { useDeleteDocument } from "../api/mutations/useDeleteDocument";
|
||||
|
|
@ -43,6 +42,8 @@ function getSourceIcon(connectorType?: string) {
|
|||
return (
|
||||
<SharePointIcon className="h-4 w-4 text-foreground flex-shrink-0" />
|
||||
);
|
||||
case "url":
|
||||
return <Globe className="h-4 w-4 text-muted-foreground flex-shrink-0" />;
|
||||
case "s3":
|
||||
return <Cloud className="h-4 w-4 text-foreground flex-shrink-0" />;
|
||||
default:
|
||||
|
|
@ -54,14 +55,8 @@ function getSourceIcon(connectorType?: string) {
|
|||
|
||||
function SearchPage() {
|
||||
const router = useRouter();
|
||||
const { isMenuOpen, files: taskFiles, refreshTasks } = useTask();
|
||||
const {
|
||||
selectedFilter,
|
||||
setSelectedFilter,
|
||||
parsedFilterData,
|
||||
isPanelOpen,
|
||||
queryOverride,
|
||||
} = useKnowledgeFilter();
|
||||
const { files: taskFiles, refreshTasks } = useTask();
|
||||
const { parsedFilterData, queryOverride } = useKnowledgeFilter();
|
||||
const [selectedRows, setSelectedRows] = useState<File[]>([]);
|
||||
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
|
||||
|
||||
|
|
@ -191,6 +186,7 @@ function SearchPage() {
|
|||
{
|
||||
field: "avgScore",
|
||||
headerName: "Avg score",
|
||||
initialFlex: 0.5,
|
||||
cellRenderer: ({ value }: CustomCellRendererProps<File>) => {
|
||||
return (
|
||||
<span className="text-xs text-accent-emerald-foreground bg-accent-emerald px-2 py-1 rounded">
|
||||
|
|
|
|||
|
|
@ -633,30 +633,54 @@ function KnowledgeSourcesPage() {
|
|||
</div>
|
||||
|
||||
{/* Conditional Sync Settings or No-Auth Message */}
|
||||
{
|
||||
isNoAuthMode ? (
|
||||
<Card className="border-yellow-500/50 bg-yellow-500/5">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg text-yellow-600">
|
||||
Cloud connectors are only available with auth mode enabled
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
Please provide the following environment variables and
|
||||
restart:
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-muted rounded-md p-4 font-mono text-sm">
|
||||
<div className="text-muted-foreground mb-2">
|
||||
# make here
|
||||
https://console.cloud.google.com/apis/credentials
|
||||
</div>
|
||||
<div>GOOGLE_OAUTH_CLIENT_ID=</div>
|
||||
<div>GOOGLE_OAUTH_CLIENT_SECRET=</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null
|
||||
{
|
||||
isNoAuthMode ? (
|
||||
<Card className="border-yellow-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">
|
||||
Cloud connectors require authentication
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
Add the Google OAuth variables below to your <code>.env</code>{" "}
|
||||
then restart the OpenRAG containers.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-muted rounded-md p-4 font-mono text-sm">
|
||||
<div className="text-muted-foreground">
|
||||
<div>
|
||||
<span className="mr-3 text-placeholder-foreground">
|
||||
27
|
||||
</span>
|
||||
<span># Google OAuth</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="mr-3 text-placeholder-foreground">
|
||||
28
|
||||
</span>
|
||||
<span># Create credentials here:</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="mr-3 text-placeholder-foreground">
|
||||
29
|
||||
</span>
|
||||
<span>
|
||||
# https://console.cloud.google.com/apis/credentials
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="mr-3 text-placeholder-foreground">30</span>
|
||||
<span>GOOGLE_OAUTH_CLIENT_ID=</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="mr-3 text-placeholder-foreground">31</span>
|
||||
<span>GOOGLE_OAUTH_CLIENT_SECRET=</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null
|
||||
// <div className="flex items-center justify-between py-4">
|
||||
// <div>
|
||||
// <h3 className="text-lg font-medium">Sync Settings</h3>
|
||||
|
|
|
|||
|
|
@ -31,10 +31,14 @@ class DoclingManager:
|
|||
|
||||
self._process: Optional[subprocess.Popen] = None
|
||||
self._port = 5001
|
||||
self._host = "127.0.0.1"
|
||||
self._host = self._get_host_for_containers() # Get appropriate host IP based on runtime
|
||||
self._running = False
|
||||
self._external_process = False
|
||||
|
||||
# PID file to track docling-serve across sessions (in current working directory)
|
||||
from pathlib import Path
|
||||
self._pid_file = Path.cwd() / ".docling.pid"
|
||||
|
||||
# Log storage - simplified, no queue
|
||||
self._log_buffer: List[str] = []
|
||||
self._max_log_lines = 1000
|
||||
|
|
@ -42,22 +46,198 @@ class DoclingManager:
|
|||
|
||||
self._initialized = True
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup resources and stop any running processes."""
|
||||
if self._process and self._process.poll() is None:
|
||||
self._add_log_entry("Cleaning up docling-serve process on exit")
|
||||
try:
|
||||
self._process.terminate()
|
||||
self._process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._process.kill()
|
||||
self._process.wait()
|
||||
except Exception as e:
|
||||
self._add_log_entry(f"Error during cleanup: {e}")
|
||||
# Try to recover existing process from PID file
|
||||
self._recover_from_pid_file()
|
||||
|
||||
self._running = False
|
||||
self._process = None
|
||||
def _get_host_for_containers(self) -> str:
|
||||
"""
|
||||
Return a host IP that containers can reach (a bridge/CNI gateway).
|
||||
Prefers Docker/Podman network gateways; falls back to bridge interfaces.
|
||||
"""
|
||||
import subprocess, json, shutil, re, logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def run(cmd, timeout=2, text=True):
|
||||
return subprocess.run(cmd, capture_output=True, text=text, timeout=timeout)
|
||||
|
||||
gateways = []
|
||||
compose_gateways = [] # Highest priority - compose project networks
|
||||
active_gateways = [] # Medium priority - networks with containers
|
||||
|
||||
# ---- Docker: enumerate networks and collect gateways
|
||||
if shutil.which("docker"):
|
||||
try:
|
||||
ls = run(["docker", "network", "ls", "--format", "{{.Name}}"])
|
||||
if ls.returncode == 0:
|
||||
for name in filter(None, ls.stdout.splitlines()):
|
||||
try:
|
||||
insp = run(["docker", "network", "inspect", name, "--format", "{{json .}}"])
|
||||
if insp.returncode == 0 and insp.stdout.strip():
|
||||
nw = json.loads(insp.stdout)[0] if insp.stdout.strip().startswith("[") else json.loads(insp.stdout)
|
||||
ipam = nw.get("IPAM", {})
|
||||
containers = nw.get("Containers", {})
|
||||
for cfg in ipam.get("Config", []) or []:
|
||||
gw = cfg.get("Gateway")
|
||||
if gw:
|
||||
# Highest priority: compose networks (ending in _default)
|
||||
if name.endswith("_default"):
|
||||
compose_gateways.append(gw)
|
||||
# Medium priority: networks with active containers
|
||||
elif len(containers) > 0:
|
||||
active_gateways.append(gw)
|
||||
# Low priority: empty networks
|
||||
else:
|
||||
gateways.append(gw)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---- Podman: enumerate networks and collect gateways (netavark)
|
||||
if shutil.which("podman"):
|
||||
try:
|
||||
# modern podman supports JSON format
|
||||
ls = run(["podman", "network", "ls", "--format", "json"])
|
||||
if ls.returncode == 0 and ls.stdout.strip():
|
||||
for net in json.loads(ls.stdout):
|
||||
name = net.get("name") or net.get("Name")
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
insp = run(["podman", "network", "inspect", name, "--format", "json"])
|
||||
if insp.returncode == 0 and insp.stdout.strip():
|
||||
arr = json.loads(insp.stdout)
|
||||
for item in (arr if isinstance(arr, list) else [arr]):
|
||||
for sn in item.get("subnets", []) or []:
|
||||
gw = sn.get("gateway")
|
||||
if gw:
|
||||
# Prioritize compose/project networks
|
||||
if name.endswith("_default") or "_" in name:
|
||||
compose_gateways.append(gw)
|
||||
else:
|
||||
gateways.append(gw)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---- Fallback: parse host interfaces for common bridges
|
||||
if not gateways:
|
||||
try:
|
||||
if shutil.which("ip"):
|
||||
show = run(["ip", "-o", "-4", "addr", "show"])
|
||||
if show.returncode == 0:
|
||||
for line in show.stdout.splitlines():
|
||||
# e.g. "12: br-3f0f... inet 172.18.0.1/16 ..."
|
||||
m = re.search(r"^\d+:\s+([a-zA-Z0-9_.:-]+)\s+.*\binet\s+(\d+\.\d+\.\d+\.\d+)/", line)
|
||||
if not m:
|
||||
continue
|
||||
ifname, ip = m.group(1), m.group(2)
|
||||
if ifname == "docker0" or ifname.startswith(("br-", "cni")):
|
||||
gateways.append(ip)
|
||||
else:
|
||||
# As a last resort, try net-tools ifconfig output
|
||||
if shutil.which("ifconfig"):
|
||||
show = run(["ifconfig"])
|
||||
for block in show.stdout.split("\n\n"):
|
||||
if any(block.strip().startswith(n) for n in ("docker0", "cni", "br-")):
|
||||
m = re.search(r"inet (?:addr:)?(\d+\.\d+\.\d+\.\d+)", block)
|
||||
if m:
|
||||
gateways.append(m.group(1))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Dedup, prioritizing: 1) compose networks, 2) active networks, 3) all others
|
||||
seen, uniq = set(), []
|
||||
# First: compose project networks (_default suffix)
|
||||
for ip in compose_gateways:
|
||||
if ip not in seen:
|
||||
uniq.append(ip)
|
||||
seen.add(ip)
|
||||
# Second: networks with active containers
|
||||
for ip in active_gateways:
|
||||
if ip not in seen:
|
||||
uniq.append(ip)
|
||||
seen.add(ip)
|
||||
# Third: all other gateways
|
||||
for ip in gateways:
|
||||
if ip not in seen:
|
||||
uniq.append(ip)
|
||||
seen.add(ip)
|
||||
|
||||
if uniq:
|
||||
if len(uniq) > 1:
|
||||
logger.info("Container-reachable host IP candidates: %s", ", ".join(uniq))
|
||||
else:
|
||||
logger.info("Container-reachable host IP: %s", uniq[0])
|
||||
return uniq[0]
|
||||
|
||||
# Nothing found: warn clearly
|
||||
logger.warning(
|
||||
"No container bridge IP found. If using rootless Podman (slirp4netns), there is no host bridge; publish ports or use 10.0.2.2 from the container."
|
||||
)
|
||||
# Returning localhost is honest only for same-namespace; keep it explicit:
|
||||
return "127.0.0.1"
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup resources but keep docling-serve running across sessions."""
|
||||
# Don't stop the process on exit - let it persist
|
||||
# Just clean up our references
|
||||
self._add_log_entry("TUI exiting - docling-serve will continue running")
|
||||
# Note: We keep the PID file so we can reconnect in future sessions
|
||||
|
||||
def _save_pid(self, pid: int) -> None:
|
||||
"""Save the process PID to a file for persistence across sessions."""
|
||||
try:
|
||||
self._pid_file.write_text(str(pid))
|
||||
self._add_log_entry(f"Saved PID {pid} to {self._pid_file}")
|
||||
except Exception as e:
|
||||
self._add_log_entry(f"Failed to save PID file: {e}")
|
||||
|
||||
def _load_pid(self) -> Optional[int]:
|
||||
"""Load the process PID from file."""
|
||||
try:
|
||||
if self._pid_file.exists():
|
||||
pid_str = self._pid_file.read_text().strip()
|
||||
if pid_str.isdigit():
|
||||
return int(pid_str)
|
||||
except Exception as e:
|
||||
self._add_log_entry(f"Failed to load PID file: {e}")
|
||||
return None
|
||||
|
||||
def _clear_pid_file(self) -> None:
|
||||
"""Remove the PID file."""
|
||||
try:
|
||||
if self._pid_file.exists():
|
||||
self._pid_file.unlink()
|
||||
self._add_log_entry("Cleared PID file")
|
||||
except Exception as e:
|
||||
self._add_log_entry(f"Failed to clear PID file: {e}")
|
||||
|
||||
def _is_process_running(self, pid: int) -> bool:
|
||||
"""Check if a process with the given PID is running."""
|
||||
try:
|
||||
# Send signal 0 to check if process exists (doesn't actually send a signal)
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def _recover_from_pid_file(self) -> None:
|
||||
"""Try to recover connection to existing docling-serve process from PID file."""
|
||||
pid = self._load_pid()
|
||||
if pid is not None:
|
||||
if self._is_process_running(pid):
|
||||
self._add_log_entry(f"Recovered existing docling-serve process (PID: {pid})")
|
||||
# Mark as external process since we didn't start it in this session
|
||||
self._external_process = True
|
||||
self._running = True
|
||||
# Note: We don't have a Popen object for this process, but that's OK
|
||||
# We'll detect it's running via port check
|
||||
else:
|
||||
self._add_log_entry(f"Stale PID file found (PID: {pid} not running)")
|
||||
self._clear_pid_file()
|
||||
|
||||
def _add_log_entry(self, message: str) -> None:
|
||||
"""Add a log entry to the buffer (thread-safe)."""
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
|
@ -70,43 +250,35 @@ class DoclingManager:
|
|||
self._log_buffer = self._log_buffer[-self._max_log_lines:]
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Check if docling serve is running."""
|
||||
# First check our internal state
|
||||
internal_running = self._running and self._process is not None and self._process.poll() is None
|
||||
|
||||
# If we think it's not running, check if something is listening on the port
|
||||
# This handles cases where docling-serve was started outside the TUI
|
||||
if not internal_running:
|
||||
try:
|
||||
import socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(0.5)
|
||||
result = s.connect_ex((self._host, self._port))
|
||||
s.close()
|
||||
|
||||
# If port is in use, something is running there
|
||||
if result == 0:
|
||||
# Only log this once when we first detect external process
|
||||
if not self._external_process:
|
||||
self._add_log_entry(f"Detected external docling-serve running on {self._host}:{self._port}")
|
||||
# Set a flag to indicate this is an external process
|
||||
self._external_process = True
|
||||
return True
|
||||
except Exception as e:
|
||||
# Only log errors occasionally to avoid spam
|
||||
if not hasattr(self, '_last_port_error') or self._last_port_error != str(e):
|
||||
self._add_log_entry(f"Error checking port: {e}")
|
||||
self._last_port_error = str(e)
|
||||
else:
|
||||
# If we started it, it's not external
|
||||
"""Check if docling serve is running (by PID only)."""
|
||||
# Check if we have a direct process handle
|
||||
if self._process is not None and self._process.poll() is None:
|
||||
self._running = True
|
||||
self._external_process = False
|
||||
return True
|
||||
|
||||
return internal_running
|
||||
# Check if we have a PID from file
|
||||
pid = self._load_pid()
|
||||
if pid is not None and self._is_process_running(pid):
|
||||
self._running = True
|
||||
self._external_process = True
|
||||
return True
|
||||
|
||||
# No running process found
|
||||
self._running = False
|
||||
self._external_process = False
|
||||
return False
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get current status of docling serve."""
|
||||
if self.is_running():
|
||||
pid = self._process.pid if self._process else None
|
||||
# Try to get PID from process handle first, then from PID file
|
||||
pid = None
|
||||
if self._process:
|
||||
pid = self._process.pid
|
||||
else:
|
||||
pid = self._load_pid()
|
||||
|
||||
return {
|
||||
"status": "running",
|
||||
"port": self._port,
|
||||
|
|
@ -127,13 +299,28 @@ class DoclingManager:
|
|||
"pid": None
|
||||
}
|
||||
|
||||
async def start(self, port: int = 5001, host: str = "127.0.0.1", enable_ui: bool = False) -> Tuple[bool, str]:
|
||||
async def start(self, port: int = 5001, host: Optional[str] = None, enable_ui: bool = False) -> Tuple[bool, str]:
|
||||
"""Start docling serve as external process."""
|
||||
if self.is_running():
|
||||
return False, "Docling serve is already running"
|
||||
|
||||
self._port = port
|
||||
self._host = host
|
||||
# Use provided host or the bridge IP we detected in __init__
|
||||
if host is not None:
|
||||
self._host = host
|
||||
# else: keep self._host as already set in __init__
|
||||
|
||||
# Check if port is already in use before trying to start
|
||||
import socket
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(0.5)
|
||||
result = s.connect_ex((self._host, self._port))
|
||||
s.close()
|
||||
if result == 0:
|
||||
return False, f"Port {self._port} on {self._host} is already in use by another process. Please stop it first."
|
||||
except Exception as e:
|
||||
self._add_log_entry(f"Error checking port availability: {e}")
|
||||
|
||||
# Clear log buffer when starting
|
||||
self._log_buffer = []
|
||||
|
|
@ -146,14 +333,14 @@ class DoclingManager:
|
|||
if shutil.which("uv") and (os.path.exists("pyproject.toml") or os.getenv("VIRTUAL_ENV")):
|
||||
cmd = [
|
||||
"uv", "run", "python", "-m", "docling_serve", "run",
|
||||
"--host", host,
|
||||
"--port", str(port),
|
||||
"--host", self._host,
|
||||
"--port", str(self._port),
|
||||
]
|
||||
else:
|
||||
cmd = [
|
||||
sys.executable, "-m", "docling_serve", "run",
|
||||
"--host", host,
|
||||
"--port", str(port),
|
||||
"--host", self._host,
|
||||
"--port", str(self._port),
|
||||
]
|
||||
|
||||
if enable_ui:
|
||||
|
|
@ -173,6 +360,9 @@ class DoclingManager:
|
|||
self._running = True
|
||||
self._add_log_entry("External process started")
|
||||
|
||||
# Save the PID to file for persistence
|
||||
self._save_pid(self._process.pid)
|
||||
|
||||
# Start a thread to capture output
|
||||
self._start_output_capture()
|
||||
|
||||
|
|
@ -192,11 +382,11 @@ class DoclingManager:
|
|||
import socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(0.5)
|
||||
result = s.connect_ex((host, port))
|
||||
result = s.connect_ex((self._host, self._port))
|
||||
s.close()
|
||||
|
||||
if result == 0:
|
||||
self._add_log_entry(f"Docling-serve is now listening on {host}:{port}")
|
||||
self._add_log_entry(f"Docling-serve is now listening on {self._host}:{self._port}")
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
|
@ -298,9 +488,12 @@ class DoclingManager:
|
|||
try:
|
||||
self._add_log_entry("Stopping docling-serve process")
|
||||
|
||||
pid_to_stop = None
|
||||
|
||||
if self._process:
|
||||
# We started this process, so we can stop it directly
|
||||
self._add_log_entry(f"Terminating our process (PID: {self._process.pid})")
|
||||
# We have a direct process handle
|
||||
pid_to_stop = self._process.pid
|
||||
self._add_log_entry(f"Terminating our process (PID: {pid_to_stop})")
|
||||
self._process.terminate()
|
||||
|
||||
# Wait for it to stop
|
||||
|
|
@ -315,16 +508,32 @@ class DoclingManager:
|
|||
self._add_log_entry("Process force killed")
|
||||
|
||||
elif self._external_process:
|
||||
# This is an external process, we can't stop it directly
|
||||
self._add_log_entry("Cannot stop external docling-serve process - it was started outside the TUI")
|
||||
self._running = False
|
||||
self._external_process = False
|
||||
return False, "Cannot stop external docling-serve process. Please stop it manually."
|
||||
# This is a process we recovered from PID file
|
||||
pid_to_stop = self._load_pid()
|
||||
if pid_to_stop and self._is_process_running(pid_to_stop):
|
||||
self._add_log_entry(f"Stopping process from PID file (PID: {pid_to_stop})")
|
||||
try:
|
||||
os.kill(pid_to_stop, 15) # SIGTERM
|
||||
# Wait a bit for graceful shutdown
|
||||
await asyncio.sleep(2)
|
||||
if self._is_process_running(pid_to_stop):
|
||||
# Still running, force kill
|
||||
self._add_log_entry(f"Force killing process (PID: {pid_to_stop})")
|
||||
os.kill(pid_to_stop, 9) # SIGKILL
|
||||
except Exception as e:
|
||||
self._add_log_entry(f"Error stopping external process: {e}")
|
||||
return False, f"Error stopping external process: {str(e)}"
|
||||
else:
|
||||
self._add_log_entry("External process not found")
|
||||
return False, "Process not found"
|
||||
|
||||
self._running = False
|
||||
self._process = None
|
||||
self._external_process = False
|
||||
|
||||
# Clear the PID file since we intentionally stopped the service
|
||||
self._clear_pid_file()
|
||||
|
||||
self._add_log_entry("Docling serve stopped successfully")
|
||||
return True, "Docling serve stopped successfully"
|
||||
|
||||
|
|
|
|||
|
|
@ -118,9 +118,16 @@ class WelcomeScreen(Screen):
|
|||
welcome_text.append(ascii_art, style="bold white")
|
||||
welcome_text.append("Terminal User Interface for OpenRAG\n\n", style="dim")
|
||||
|
||||
if self.services_running:
|
||||
# Check if all services are running
|
||||
all_services_running = self.services_running and self.docling_running
|
||||
|
||||
if all_services_running:
|
||||
welcome_text.append(
|
||||
"✓ Services are currently running\n\n", style="bold green"
|
||||
"✓ All services are running\n\n", style="bold green"
|
||||
)
|
||||
elif self.services_running or self.docling_running:
|
||||
welcome_text.append(
|
||||
"⚠ Some services are running\n\n", style="bold yellow"
|
||||
)
|
||||
elif self.has_oauth_config:
|
||||
welcome_text.append(
|
||||
|
|
@ -140,16 +147,19 @@ class WelcomeScreen(Screen):
|
|||
|
||||
buttons = []
|
||||
|
||||
if self.services_running:
|
||||
# Services running - show app link first, then stop services
|
||||
# Check if all services (native + container) are running
|
||||
all_services_running = self.services_running and self.docling_running
|
||||
|
||||
if all_services_running:
|
||||
# All services running - show app link first, then stop all
|
||||
buttons.append(
|
||||
Button("Launch OpenRAG", variant="success", id="open-app-btn")
|
||||
)
|
||||
buttons.append(
|
||||
Button("Stop Container Services", variant="error", id="stop-services-btn")
|
||||
Button("Stop All Services", variant="error", id="stop-all-services-btn")
|
||||
)
|
||||
else:
|
||||
# Services not running - show setup options and start services
|
||||
# Some or no services running - show setup options and start all
|
||||
if has_oauth:
|
||||
# If OAuth is configured, only show advanced setup
|
||||
buttons.append(
|
||||
|
|
@ -165,25 +175,7 @@ class WelcomeScreen(Screen):
|
|||
)
|
||||
|
||||
buttons.append(
|
||||
Button("Start Container Services", variant="primary", id="start-services-btn")
|
||||
)
|
||||
|
||||
# Native services controls
|
||||
if self.docling_running:
|
||||
buttons.append(
|
||||
Button(
|
||||
"Stop Native Services",
|
||||
variant="warning",
|
||||
id="stop-native-services-btn",
|
||||
)
|
||||
)
|
||||
else:
|
||||
buttons.append(
|
||||
Button(
|
||||
"Start Native Services",
|
||||
variant="primary",
|
||||
id="start-native-services-btn",
|
||||
)
|
||||
Button("Start All Services", variant="primary", id="start-all-services-btn")
|
||||
)
|
||||
|
||||
# Always show status option
|
||||
|
|
@ -213,7 +205,7 @@ class WelcomeScreen(Screen):
|
|||
)
|
||||
|
||||
# Set default button focus
|
||||
if self.services_running:
|
||||
if self.services_running and self.docling_running:
|
||||
self.default_button_id = "open-app-btn"
|
||||
elif self.has_oauth_config:
|
||||
self.default_button_id = "advanced-setup-btn"
|
||||
|
|
@ -234,7 +226,7 @@ class WelcomeScreen(Screen):
|
|||
def _focus_appropriate_button(self) -> None:
|
||||
"""Focus the appropriate button based on current state."""
|
||||
try:
|
||||
if self.services_running:
|
||||
if self.services_running and self.docling_running:
|
||||
self.query_one("#open-app-btn").focus()
|
||||
elif self.has_oauth_config:
|
||||
self.query_one("#advanced-setup-btn").focus()
|
||||
|
|
@ -253,20 +245,16 @@ class WelcomeScreen(Screen):
|
|||
self.action_monitor()
|
||||
elif event.button.id == "diagnostics-btn":
|
||||
self.action_diagnostics()
|
||||
elif event.button.id == "start-services-btn":
|
||||
self.action_start_stop_services()
|
||||
elif event.button.id == "stop-services-btn":
|
||||
self.action_start_stop_services()
|
||||
elif event.button.id == "start-native-services-btn":
|
||||
self.action_start_native_services()
|
||||
elif event.button.id == "stop-native-services-btn":
|
||||
self.action_stop_native_services()
|
||||
elif event.button.id == "start-all-services-btn":
|
||||
self.action_start_all_services()
|
||||
elif event.button.id == "stop-all-services-btn":
|
||||
self.action_stop_all_services()
|
||||
elif event.button.id == "open-app-btn":
|
||||
self.action_open_app()
|
||||
|
||||
def action_default_action(self) -> None:
|
||||
"""Handle Enter key - go to default action based on state."""
|
||||
if self.services_running:
|
||||
if self.services_running and self.docling_running:
|
||||
self.action_open_app()
|
||||
elif self.has_oauth_config:
|
||||
self.action_full_setup()
|
||||
|
|
@ -297,28 +285,13 @@ class WelcomeScreen(Screen):
|
|||
|
||||
self.app.push_screen(DiagnosticsScreen())
|
||||
|
||||
def action_start_stop_services(self) -> None:
|
||||
"""Start or stop all services (containers + docling)."""
|
||||
if self.services_running:
|
||||
# Stop services - show modal with progress
|
||||
if self.container_manager.is_available():
|
||||
command_generator = self.container_manager.stop_services()
|
||||
modal = CommandOutputModal(
|
||||
"Stopping Services",
|
||||
command_generator,
|
||||
on_complete=self._on_services_operation_complete,
|
||||
)
|
||||
self.app.push_screen(modal)
|
||||
else:
|
||||
# Start services - show modal with progress
|
||||
if self.container_manager.is_available():
|
||||
command_generator = self.container_manager.start_services()
|
||||
modal = CommandOutputModal(
|
||||
"Starting Services",
|
||||
command_generator,
|
||||
on_complete=self._on_services_operation_complete,
|
||||
)
|
||||
self.app.push_screen(modal)
|
||||
def action_start_all_services(self) -> None:
|
||||
"""Start all services (native first, then containers)."""
|
||||
self.run_worker(self._start_all_services())
|
||||
|
||||
def action_stop_all_services(self) -> None:
|
||||
"""Stop all services (containers first, then native)."""
|
||||
self.run_worker(self._stop_all_services())
|
||||
|
||||
async def _on_services_operation_complete(self) -> None:
|
||||
"""Handle completion of services start/stop operation."""
|
||||
|
|
@ -334,7 +307,7 @@ class WelcomeScreen(Screen):
|
|||
|
||||
def _update_default_button(self) -> None:
|
||||
"""Update the default button target based on state."""
|
||||
if self.services_running:
|
||||
if self.services_running and self.docling_running:
|
||||
self.default_button_id = "open-app-btn"
|
||||
elif self.has_oauth_config:
|
||||
self.default_button_id = "advanced-setup-btn"
|
||||
|
|
@ -362,51 +335,84 @@ class WelcomeScreen(Screen):
|
|||
|
||||
self.call_after_refresh(self._focus_appropriate_button)
|
||||
|
||||
def action_start_native_services(self) -> None:
|
||||
"""Start native services (docling)."""
|
||||
if self.docling_running:
|
||||
self.notify("Native services are already running.", severity="warning")
|
||||
return
|
||||
async def _start_all_services(self) -> None:
|
||||
"""Start all services: containers first, then native services."""
|
||||
# Step 1: Start container services first (to create the network)
|
||||
if self.container_manager.is_available():
|
||||
command_generator = self.container_manager.start_services()
|
||||
modal = CommandOutputModal(
|
||||
"Starting Container Services",
|
||||
command_generator,
|
||||
on_complete=self._on_containers_started_start_native,
|
||||
)
|
||||
self.app.push_screen(modal)
|
||||
else:
|
||||
self.notify("No container runtime available", severity="warning")
|
||||
# Still try to start native services
|
||||
await self._start_native_services_after_containers()
|
||||
|
||||
self.run_worker(self._start_native_services())
|
||||
async def _on_containers_started_start_native(self) -> None:
|
||||
"""Called after containers start successfully, now start native services."""
|
||||
# Update container state
|
||||
self._detect_services_sync()
|
||||
|
||||
async def _start_native_services(self) -> None:
|
||||
"""Worker task to start native services."""
|
||||
try:
|
||||
# Now start native services (docling-serve can now detect the compose network)
|
||||
await self._start_native_services_after_containers()
|
||||
|
||||
async def _start_native_services_after_containers(self) -> None:
|
||||
"""Start native services after containers have been started."""
|
||||
if not self.docling_manager.is_running():
|
||||
self.notify("Starting native services...", severity="information")
|
||||
success, message = await self.docling_manager.start()
|
||||
if success:
|
||||
self.docling_running = True
|
||||
self.notify(message, severity="information")
|
||||
else:
|
||||
self.notify(f"Failed to start native services: {message}", severity="error")
|
||||
except Exception as exc:
|
||||
self.notify(f"Error starting native services: {exc}", severity="error")
|
||||
finally:
|
||||
self.docling_running = self.docling_manager.is_running()
|
||||
await self._refresh_welcome_content()
|
||||
else:
|
||||
self.notify("Native services already running", severity="information")
|
||||
|
||||
def action_stop_native_services(self) -> None:
|
||||
"""Stop native services (docling)."""
|
||||
if not self.docling_running and not self.docling_manager.is_running():
|
||||
self.notify("Native services are not running.", severity="warning")
|
||||
return
|
||||
# Update state
|
||||
self.docling_running = self.docling_manager.is_running()
|
||||
await self._refresh_welcome_content()
|
||||
|
||||
self.run_worker(self._stop_native_services())
|
||||
async def _stop_all_services(self) -> None:
|
||||
"""Stop all services: containers first, then native."""
|
||||
# Step 1: Stop container services
|
||||
if self.container_manager.is_available() and self.services_running:
|
||||
command_generator = self.container_manager.stop_services()
|
||||
modal = CommandOutputModal(
|
||||
"Stopping Container Services",
|
||||
command_generator,
|
||||
on_complete=self._on_stop_containers_complete,
|
||||
)
|
||||
self.app.push_screen(modal)
|
||||
else:
|
||||
# No containers to stop, go directly to stopping native services
|
||||
await self._stop_native_services_after_containers()
|
||||
|
||||
async def _stop_native_services(self) -> None:
|
||||
"""Worker task to stop native services."""
|
||||
try:
|
||||
async def _on_stop_containers_complete(self) -> None:
|
||||
"""Called after containers are stopped, now stop native services."""
|
||||
# Update container state
|
||||
self._detect_services_sync()
|
||||
|
||||
# Now stop native services
|
||||
await self._stop_native_services_after_containers()
|
||||
|
||||
async def _stop_native_services_after_containers(self) -> None:
|
||||
"""Stop native services after containers have been stopped."""
|
||||
if self.docling_manager.is_running():
|
||||
self.notify("Stopping native services...", severity="information")
|
||||
success, message = await self.docling_manager.stop()
|
||||
if success:
|
||||
self.docling_running = False
|
||||
self.notify(message, severity="information")
|
||||
else:
|
||||
self.notify(f"Failed to stop native services: {message}", severity="error")
|
||||
except Exception as exc:
|
||||
self.notify(f"Error stopping native services: {exc}", severity="error")
|
||||
finally:
|
||||
self.docling_running = self.docling_manager.is_running()
|
||||
await self._refresh_welcome_content()
|
||||
else:
|
||||
self.notify("Native services already stopped", severity="information")
|
||||
|
||||
# Update state
|
||||
self.docling_running = self.docling_manager.is_running()
|
||||
await self._refresh_welcome_content()
|
||||
|
||||
def action_open_app(self) -> None:
|
||||
"""Open the OpenRAG app in the default browser."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue