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
|
.DS_Store
|
||||||
|
|
||||||
config/
|
config/
|
||||||
|
|
||||||
|
.docling.pid
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ services:
|
||||||
# build:
|
# build:
|
||||||
# context: .
|
# context: .
|
||||||
# dockerfile: Dockerfile.backend
|
# dockerfile: Dockerfile.backend
|
||||||
# container_name: openrag-backend
|
container_name: openrag-backend
|
||||||
depends_on:
|
depends_on:
|
||||||
- langflow
|
- langflow
|
||||||
environment:
|
environment:
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ services:
|
||||||
# build:
|
# build:
|
||||||
# context: .
|
# context: .
|
||||||
# dockerfile: Dockerfile.backend
|
# dockerfile: Dockerfile.backend
|
||||||
# container_name: openrag-backend
|
container_name: openrag-backend
|
||||||
depends_on:
|
depends_on:
|
||||||
- langflow
|
- langflow
|
||||||
environment:
|
environment:
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,6 @@ import {
|
||||||
type Nudge = string;
|
type Nudge = string;
|
||||||
|
|
||||||
const DEFAULT_NUDGES = [
|
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 = (
|
export const useGetNudgesQuery = (
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import type { ColDef, GetRowIdParams } from "ag-grid-community";
|
import type { ColDef, GetRowIdParams } from "ag-grid-community";
|
||||||
import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react";
|
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 { useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
|
|
@ -21,7 +21,6 @@ import "@/components/AgGrid/registerAgGridModules";
|
||||||
import "@/components/AgGrid/agGridStyles.css";
|
import "@/components/AgGrid/agGridStyles.css";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdown";
|
import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdown";
|
||||||
import { filterAccentClasses } from "@/components/knowledge-filter-panel";
|
|
||||||
import { StatusBadge } from "@/components/ui/status-badge";
|
import { StatusBadge } from "@/components/ui/status-badge";
|
||||||
import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog";
|
import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog";
|
||||||
import { useDeleteDocument } from "../api/mutations/useDeleteDocument";
|
import { useDeleteDocument } from "../api/mutations/useDeleteDocument";
|
||||||
|
|
@ -43,6 +42,8 @@ function getSourceIcon(connectorType?: string) {
|
||||||
return (
|
return (
|
||||||
<SharePointIcon className="h-4 w-4 text-foreground flex-shrink-0" />
|
<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":
|
case "s3":
|
||||||
return <Cloud className="h-4 w-4 text-foreground flex-shrink-0" />;
|
return <Cloud className="h-4 w-4 text-foreground flex-shrink-0" />;
|
||||||
default:
|
default:
|
||||||
|
|
@ -54,14 +55,8 @@ function getSourceIcon(connectorType?: string) {
|
||||||
|
|
||||||
function SearchPage() {
|
function SearchPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isMenuOpen, files: taskFiles, refreshTasks } = useTask();
|
const { files: taskFiles, refreshTasks } = useTask();
|
||||||
const {
|
const { parsedFilterData, queryOverride } = useKnowledgeFilter();
|
||||||
selectedFilter,
|
|
||||||
setSelectedFilter,
|
|
||||||
parsedFilterData,
|
|
||||||
isPanelOpen,
|
|
||||||
queryOverride,
|
|
||||||
} = useKnowledgeFilter();
|
|
||||||
const [selectedRows, setSelectedRows] = useState<File[]>([]);
|
const [selectedRows, setSelectedRows] = useState<File[]>([]);
|
||||||
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
|
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
|
||||||
|
|
||||||
|
|
@ -191,6 +186,7 @@ function SearchPage() {
|
||||||
{
|
{
|
||||||
field: "avgScore",
|
field: "avgScore",
|
||||||
headerName: "Avg score",
|
headerName: "Avg score",
|
||||||
|
initialFlex: 0.5,
|
||||||
cellRenderer: ({ value }: CustomCellRendererProps<File>) => {
|
cellRenderer: ({ value }: CustomCellRendererProps<File>) => {
|
||||||
return (
|
return (
|
||||||
<span className="text-xs text-accent-emerald-foreground bg-accent-emerald px-2 py-1 rounded">
|
<span className="text-xs text-accent-emerald-foreground bg-accent-emerald px-2 py-1 rounded">
|
||||||
|
|
|
||||||
|
|
@ -633,30 +633,54 @@ function KnowledgeSourcesPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Conditional Sync Settings or No-Auth Message */}
|
{/* Conditional Sync Settings or No-Auth Message */}
|
||||||
{
|
{
|
||||||
isNoAuthMode ? (
|
isNoAuthMode ? (
|
||||||
<Card className="border-yellow-500/50 bg-yellow-500/5">
|
<Card className="border-yellow-500">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg text-yellow-600">
|
<CardTitle className="text-lg">
|
||||||
Cloud connectors are only available with auth mode enabled
|
Cloud connectors require authentication
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-sm">
|
<CardDescription className="text-sm">
|
||||||
Please provide the following environment variables and
|
Add the Google OAuth variables below to your <code>.env</code>{" "}
|
||||||
restart:
|
then restart the OpenRAG containers.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="bg-muted rounded-md p-4 font-mono text-sm">
|
<div className="bg-muted rounded-md p-4 font-mono text-sm">
|
||||||
<div className="text-muted-foreground mb-2">
|
<div className="text-muted-foreground">
|
||||||
# make here
|
<div>
|
||||||
https://console.cloud.google.com/apis/credentials
|
<span className="mr-3 text-placeholder-foreground">
|
||||||
</div>
|
27
|
||||||
<div>GOOGLE_OAUTH_CLIENT_ID=</div>
|
</span>
|
||||||
<div>GOOGLE_OAUTH_CLIENT_SECRET=</div>
|
<span># Google OAuth</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
<div>
|
||||||
</Card>
|
<span className="mr-3 text-placeholder-foreground">
|
||||||
) : null
|
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 className="flex items-center justify-between py-4">
|
||||||
// <div>
|
// <div>
|
||||||
// <h3 className="text-lg font-medium">Sync Settings</h3>
|
// <h3 className="text-lg font-medium">Sync Settings</h3>
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,14 @@ class DoclingManager:
|
||||||
|
|
||||||
self._process: Optional[subprocess.Popen] = None
|
self._process: Optional[subprocess.Popen] = None
|
||||||
self._port = 5001
|
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._running = False
|
||||||
self._external_process = 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
|
# Log storage - simplified, no queue
|
||||||
self._log_buffer: List[str] = []
|
self._log_buffer: List[str] = []
|
||||||
self._max_log_lines = 1000
|
self._max_log_lines = 1000
|
||||||
|
|
@ -42,22 +46,198 @@ class DoclingManager:
|
||||||
|
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
def cleanup(self):
|
# Try to recover existing process from PID file
|
||||||
"""Cleanup resources and stop any running processes."""
|
self._recover_from_pid_file()
|
||||||
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}")
|
|
||||||
|
|
||||||
self._running = False
|
def _get_host_for_containers(self) -> str:
|
||||||
self._process = None
|
"""
|
||||||
|
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:
|
def _add_log_entry(self, message: str) -> None:
|
||||||
"""Add a log entry to the buffer (thread-safe)."""
|
"""Add a log entry to the buffer (thread-safe)."""
|
||||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
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:]
|
self._log_buffer = self._log_buffer[-self._max_log_lines:]
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
"""Check if docling serve is running."""
|
"""Check if docling serve is running (by PID only)."""
|
||||||
# First check our internal state
|
# Check if we have a direct process handle
|
||||||
internal_running = self._running and self._process is not None and self._process.poll() is None
|
if self._process is not None and self._process.poll() is None:
|
||||||
|
self._running = True
|
||||||
# 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
|
|
||||||
self._external_process = False
|
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]:
|
def get_status(self) -> Dict[str, Any]:
|
||||||
"""Get current status of docling serve."""
|
"""Get current status of docling serve."""
|
||||||
if self.is_running():
|
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 {
|
return {
|
||||||
"status": "running",
|
"status": "running",
|
||||||
"port": self._port,
|
"port": self._port,
|
||||||
|
|
@ -127,13 +299,28 @@ class DoclingManager:
|
||||||
"pid": None
|
"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."""
|
"""Start docling serve as external process."""
|
||||||
if self.is_running():
|
if self.is_running():
|
||||||
return False, "Docling serve is already running"
|
return False, "Docling serve is already running"
|
||||||
|
|
||||||
self._port = port
|
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
|
# Clear log buffer when starting
|
||||||
self._log_buffer = []
|
self._log_buffer = []
|
||||||
|
|
@ -146,14 +333,14 @@ class DoclingManager:
|
||||||
if shutil.which("uv") and (os.path.exists("pyproject.toml") or os.getenv("VIRTUAL_ENV")):
|
if shutil.which("uv") and (os.path.exists("pyproject.toml") or os.getenv("VIRTUAL_ENV")):
|
||||||
cmd = [
|
cmd = [
|
||||||
"uv", "run", "python", "-m", "docling_serve", "run",
|
"uv", "run", "python", "-m", "docling_serve", "run",
|
||||||
"--host", host,
|
"--host", self._host,
|
||||||
"--port", str(port),
|
"--port", str(self._port),
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
cmd = [
|
cmd = [
|
||||||
sys.executable, "-m", "docling_serve", "run",
|
sys.executable, "-m", "docling_serve", "run",
|
||||||
"--host", host,
|
"--host", self._host,
|
||||||
"--port", str(port),
|
"--port", str(self._port),
|
||||||
]
|
]
|
||||||
|
|
||||||
if enable_ui:
|
if enable_ui:
|
||||||
|
|
@ -173,6 +360,9 @@ class DoclingManager:
|
||||||
self._running = True
|
self._running = True
|
||||||
self._add_log_entry("External process started")
|
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
|
# Start a thread to capture output
|
||||||
self._start_output_capture()
|
self._start_output_capture()
|
||||||
|
|
||||||
|
|
@ -192,11 +382,11 @@ class DoclingManager:
|
||||||
import socket
|
import socket
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
s.settimeout(0.5)
|
s.settimeout(0.5)
|
||||||
result = s.connect_ex((host, port))
|
result = s.connect_ex((self._host, self._port))
|
||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
if result == 0:
|
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
|
break
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
@ -298,9 +488,12 @@ class DoclingManager:
|
||||||
try:
|
try:
|
||||||
self._add_log_entry("Stopping docling-serve process")
|
self._add_log_entry("Stopping docling-serve process")
|
||||||
|
|
||||||
|
pid_to_stop = None
|
||||||
|
|
||||||
if self._process:
|
if self._process:
|
||||||
# We started this process, so we can stop it directly
|
# We have a direct process handle
|
||||||
self._add_log_entry(f"Terminating our process (PID: {self._process.pid})")
|
pid_to_stop = self._process.pid
|
||||||
|
self._add_log_entry(f"Terminating our process (PID: {pid_to_stop})")
|
||||||
self._process.terminate()
|
self._process.terminate()
|
||||||
|
|
||||||
# Wait for it to stop
|
# Wait for it to stop
|
||||||
|
|
@ -315,16 +508,32 @@ class DoclingManager:
|
||||||
self._add_log_entry("Process force killed")
|
self._add_log_entry("Process force killed")
|
||||||
|
|
||||||
elif self._external_process:
|
elif self._external_process:
|
||||||
# This is an external process, we can't stop it directly
|
# This is a process we recovered from PID file
|
||||||
self._add_log_entry("Cannot stop external docling-serve process - it was started outside the TUI")
|
pid_to_stop = self._load_pid()
|
||||||
self._running = False
|
if pid_to_stop and self._is_process_running(pid_to_stop):
|
||||||
self._external_process = False
|
self._add_log_entry(f"Stopping process from PID file (PID: {pid_to_stop})")
|
||||||
return False, "Cannot stop external docling-serve process. Please stop it manually."
|
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._running = False
|
||||||
self._process = None
|
self._process = None
|
||||||
self._external_process = False
|
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")
|
self._add_log_entry("Docling serve stopped successfully")
|
||||||
return True, "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(ascii_art, style="bold white")
|
||||||
welcome_text.append("Terminal User Interface for OpenRAG\n\n", style="dim")
|
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(
|
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:
|
elif self.has_oauth_config:
|
||||||
welcome_text.append(
|
welcome_text.append(
|
||||||
|
|
@ -140,16 +147,19 @@ class WelcomeScreen(Screen):
|
||||||
|
|
||||||
buttons = []
|
buttons = []
|
||||||
|
|
||||||
if self.services_running:
|
# Check if all services (native + container) are running
|
||||||
# Services running - show app link first, then stop services
|
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(
|
buttons.append(
|
||||||
Button("Launch OpenRAG", variant="success", id="open-app-btn")
|
Button("Launch OpenRAG", variant="success", id="open-app-btn")
|
||||||
)
|
)
|
||||||
buttons.append(
|
buttons.append(
|
||||||
Button("Stop Container Services", variant="error", id="stop-services-btn")
|
Button("Stop All Services", variant="error", id="stop-all-services-btn")
|
||||||
)
|
)
|
||||||
else:
|
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 has_oauth:
|
||||||
# If OAuth is configured, only show advanced setup
|
# If OAuth is configured, only show advanced setup
|
||||||
buttons.append(
|
buttons.append(
|
||||||
|
|
@ -165,25 +175,7 @@ class WelcomeScreen(Screen):
|
||||||
)
|
)
|
||||||
|
|
||||||
buttons.append(
|
buttons.append(
|
||||||
Button("Start Container Services", variant="primary", id="start-services-btn")
|
Button("Start All Services", variant="primary", id="start-all-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",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Always show status option
|
# Always show status option
|
||||||
|
|
@ -213,7 +205,7 @@ class WelcomeScreen(Screen):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set default button focus
|
# Set default button focus
|
||||||
if self.services_running:
|
if self.services_running and self.docling_running:
|
||||||
self.default_button_id = "open-app-btn"
|
self.default_button_id = "open-app-btn"
|
||||||
elif self.has_oauth_config:
|
elif self.has_oauth_config:
|
||||||
self.default_button_id = "advanced-setup-btn"
|
self.default_button_id = "advanced-setup-btn"
|
||||||
|
|
@ -234,7 +226,7 @@ class WelcomeScreen(Screen):
|
||||||
def _focus_appropriate_button(self) -> None:
|
def _focus_appropriate_button(self) -> None:
|
||||||
"""Focus the appropriate button based on current state."""
|
"""Focus the appropriate button based on current state."""
|
||||||
try:
|
try:
|
||||||
if self.services_running:
|
if self.services_running and self.docling_running:
|
||||||
self.query_one("#open-app-btn").focus()
|
self.query_one("#open-app-btn").focus()
|
||||||
elif self.has_oauth_config:
|
elif self.has_oauth_config:
|
||||||
self.query_one("#advanced-setup-btn").focus()
|
self.query_one("#advanced-setup-btn").focus()
|
||||||
|
|
@ -253,20 +245,16 @@ class WelcomeScreen(Screen):
|
||||||
self.action_monitor()
|
self.action_monitor()
|
||||||
elif event.button.id == "diagnostics-btn":
|
elif event.button.id == "diagnostics-btn":
|
||||||
self.action_diagnostics()
|
self.action_diagnostics()
|
||||||
elif event.button.id == "start-services-btn":
|
elif event.button.id == "start-all-services-btn":
|
||||||
self.action_start_stop_services()
|
self.action_start_all_services()
|
||||||
elif event.button.id == "stop-services-btn":
|
elif event.button.id == "stop-all-services-btn":
|
||||||
self.action_start_stop_services()
|
self.action_stop_all_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 == "open-app-btn":
|
elif event.button.id == "open-app-btn":
|
||||||
self.action_open_app()
|
self.action_open_app()
|
||||||
|
|
||||||
def action_default_action(self) -> None:
|
def action_default_action(self) -> None:
|
||||||
"""Handle Enter key - go to default action based on state."""
|
"""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()
|
self.action_open_app()
|
||||||
elif self.has_oauth_config:
|
elif self.has_oauth_config:
|
||||||
self.action_full_setup()
|
self.action_full_setup()
|
||||||
|
|
@ -297,28 +285,13 @@ class WelcomeScreen(Screen):
|
||||||
|
|
||||||
self.app.push_screen(DiagnosticsScreen())
|
self.app.push_screen(DiagnosticsScreen())
|
||||||
|
|
||||||
def action_start_stop_services(self) -> None:
|
def action_start_all_services(self) -> None:
|
||||||
"""Start or stop all services (containers + docling)."""
|
"""Start all services (native first, then containers)."""
|
||||||
if self.services_running:
|
self.run_worker(self._start_all_services())
|
||||||
# Stop services - show modal with progress
|
|
||||||
if self.container_manager.is_available():
|
def action_stop_all_services(self) -> None:
|
||||||
command_generator = self.container_manager.stop_services()
|
"""Stop all services (containers first, then native)."""
|
||||||
modal = CommandOutputModal(
|
self.run_worker(self._stop_all_services())
|
||||||
"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)
|
|
||||||
|
|
||||||
async def _on_services_operation_complete(self) -> None:
|
async def _on_services_operation_complete(self) -> None:
|
||||||
"""Handle completion of services start/stop operation."""
|
"""Handle completion of services start/stop operation."""
|
||||||
|
|
@ -334,7 +307,7 @@ class WelcomeScreen(Screen):
|
||||||
|
|
||||||
def _update_default_button(self) -> None:
|
def _update_default_button(self) -> None:
|
||||||
"""Update the default button target based on state."""
|
"""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"
|
self.default_button_id = "open-app-btn"
|
||||||
elif self.has_oauth_config:
|
elif self.has_oauth_config:
|
||||||
self.default_button_id = "advanced-setup-btn"
|
self.default_button_id = "advanced-setup-btn"
|
||||||
|
|
@ -362,51 +335,84 @@ class WelcomeScreen(Screen):
|
||||||
|
|
||||||
self.call_after_refresh(self._focus_appropriate_button)
|
self.call_after_refresh(self._focus_appropriate_button)
|
||||||
|
|
||||||
def action_start_native_services(self) -> None:
|
async def _start_all_services(self) -> None:
|
||||||
"""Start native services (docling)."""
|
"""Start all services: containers first, then native services."""
|
||||||
if self.docling_running:
|
# Step 1: Start container services first (to create the network)
|
||||||
self.notify("Native services are already running.", severity="warning")
|
if self.container_manager.is_available():
|
||||||
return
|
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:
|
# Now start native services (docling-serve can now detect the compose network)
|
||||||
"""Worker task to start native services."""
|
await self._start_native_services_after_containers()
|
||||||
try:
|
|
||||||
|
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()
|
success, message = await self.docling_manager.start()
|
||||||
if success:
|
if success:
|
||||||
self.docling_running = True
|
|
||||||
self.notify(message, severity="information")
|
self.notify(message, severity="information")
|
||||||
else:
|
else:
|
||||||
self.notify(f"Failed to start native services: {message}", severity="error")
|
self.notify(f"Failed to start native services: {message}", severity="error")
|
||||||
except Exception as exc:
|
else:
|
||||||
self.notify(f"Error starting native services: {exc}", severity="error")
|
self.notify("Native services already running", severity="information")
|
||||||
finally:
|
|
||||||
self.docling_running = self.docling_manager.is_running()
|
|
||||||
await self._refresh_welcome_content()
|
|
||||||
|
|
||||||
def action_stop_native_services(self) -> None:
|
# Update state
|
||||||
"""Stop native services (docling)."""
|
self.docling_running = self.docling_manager.is_running()
|
||||||
if not self.docling_running and not self.docling_manager.is_running():
|
await self._refresh_welcome_content()
|
||||||
self.notify("Native services are not running.", severity="warning")
|
|
||||||
return
|
|
||||||
|
|
||||||
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:
|
async def _on_stop_containers_complete(self) -> None:
|
||||||
"""Worker task to stop native services."""
|
"""Called after containers are stopped, now stop native services."""
|
||||||
try:
|
# 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()
|
success, message = await self.docling_manager.stop()
|
||||||
if success:
|
if success:
|
||||||
self.docling_running = False
|
|
||||||
self.notify(message, severity="information")
|
self.notify(message, severity="information")
|
||||||
else:
|
else:
|
||||||
self.notify(f"Failed to stop native services: {message}", severity="error")
|
self.notify(f"Failed to stop native services: {message}", severity="error")
|
||||||
except Exception as exc:
|
else:
|
||||||
self.notify(f"Error stopping native services: {exc}", severity="error")
|
self.notify("Native services already stopped", severity="information")
|
||||||
finally:
|
|
||||||
self.docling_running = self.docling_manager.is_running()
|
# Update state
|
||||||
await self._refresh_welcome_content()
|
self.docling_running = self.docling_manager.is_running()
|
||||||
|
await self._refresh_welcome_content()
|
||||||
|
|
||||||
def action_open_app(self) -> None:
|
def action_open_app(self) -> None:
|
||||||
"""Open the OpenRAG app in the default browser."""
|
"""Open the OpenRAG app in the default browser."""
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue