cognee 0.3.0.dev0__py3-none-any.whl → 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cognee/__init__.py +1 -0
- cognee/api/v1/save/save.py +335 -0
- cognee/api/v1/search/routers/get_search_router.py +3 -3
- cognee/api/v1/ui/__init__.py +1 -0
- cognee/api/v1/ui/ui.py +624 -0
- cognee/cli/_cognee.py +102 -0
- cognee/modules/retrieval/graph_completion_context_extension_retriever.py +1 -1
- cognee/modules/retrieval/graph_completion_cot_retriever.py +1 -1
- cognee/modules/retrieval/graph_completion_retriever.py +1 -1
- cognee/modules/retrieval/insights_retriever.py +12 -11
- cognee/modules/retrieval/temporal_retriever.py +1 -1
- cognee/modules/search/methods/search.py +31 -8
- cognee/tests/test_permissions.py +3 -3
- cognee/tests/test_relational_db_migration.py +3 -5
- cognee/tests/test_save_export_path.py +116 -0
- cognee/tests/test_search_db.py +10 -7
- cognee/tests/unit/modules/retrieval/graph_completion_retriever_context_extension_test.py +12 -6
- cognee/tests/unit/modules/retrieval/graph_completion_retriever_cot_test.py +12 -6
- cognee/tests/unit/modules/retrieval/insights_retriever_test.py +2 -4
- {cognee-0.3.0.dev0.dist-info → cognee-0.3.2.dist-info}/METADATA +2 -2
- {cognee-0.3.0.dev0.dist-info → cognee-0.3.2.dist-info}/RECORD +34 -30
- distributed/pyproject.toml +1 -1
- /cognee/tests/{integration/cli → cli_tests/cli_integration_tests}/__init__.py +0 -0
- /cognee/tests/{integration/cli → cli_tests/cli_integration_tests}/test_cli_integration.py +0 -0
- /cognee/tests/{unit/cli → cli_tests/cli_unit_tests}/__init__.py +0 -0
- /cognee/tests/{unit/cli → cli_tests/cli_unit_tests}/test_cli_commands.py +0 -0
- /cognee/tests/{unit/cli → cli_tests/cli_unit_tests}/test_cli_edge_cases.py +0 -0
- /cognee/tests/{unit/cli → cli_tests/cli_unit_tests}/test_cli_main.py +0 -0
- /cognee/tests/{unit/cli → cli_tests/cli_unit_tests}/test_cli_runner.py +0 -0
- /cognee/tests/{unit/cli → cli_tests/cli_unit_tests}/test_cli_utils.py +0 -0
- {cognee-0.3.0.dev0.dist-info → cognee-0.3.2.dist-info}/WHEEL +0 -0
- {cognee-0.3.0.dev0.dist-info → cognee-0.3.2.dist-info}/entry_points.txt +0 -0
- {cognee-0.3.0.dev0.dist-info → cognee-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {cognee-0.3.0.dev0.dist-info → cognee-0.3.2.dist-info}/licenses/NOTICE.md +0 -0
cognee/api/v1/ui/ui.py
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import signal
|
|
3
|
+
import subprocess
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import webbrowser
|
|
7
|
+
import zipfile
|
|
8
|
+
import requests
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Callable, Optional, Tuple
|
|
11
|
+
import tempfile
|
|
12
|
+
import shutil
|
|
13
|
+
|
|
14
|
+
from cognee.shared.logging_utils import get_logger
|
|
15
|
+
from cognee.version import get_cognee_version
|
|
16
|
+
|
|
17
|
+
logger = get_logger()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def normalize_version_for_comparison(version: str) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Normalize version string for comparison.
|
|
23
|
+
Handles development versions and edge cases.
|
|
24
|
+
"""
|
|
25
|
+
# Remove common development suffixes for comparison
|
|
26
|
+
normalized = (
|
|
27
|
+
version.replace("-local", "").replace("-dev", "").replace("-alpha", "").replace("-beta", "")
|
|
28
|
+
)
|
|
29
|
+
return normalized.strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_frontend_cache_dir() -> Path:
|
|
33
|
+
"""
|
|
34
|
+
Get the directory where downloaded frontend assets are cached.
|
|
35
|
+
Uses user's home directory to persist across package updates.
|
|
36
|
+
Each cached frontend is version-specific and will be re-downloaded
|
|
37
|
+
when the cognee package version changes.
|
|
38
|
+
"""
|
|
39
|
+
cache_dir = Path.home() / ".cognee" / "ui-cache"
|
|
40
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
return cache_dir
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_frontend_download_info() -> Tuple[str, str]:
|
|
45
|
+
"""
|
|
46
|
+
Get the download URL and version for the actual cognee-frontend source.
|
|
47
|
+
Downloads the real frontend from GitHub releases, matching the installed version.
|
|
48
|
+
"""
|
|
49
|
+
version = get_cognee_version()
|
|
50
|
+
|
|
51
|
+
# Clean up version string (remove -local suffix for development)
|
|
52
|
+
clean_version = version.replace("-local", "")
|
|
53
|
+
|
|
54
|
+
# Download from specific release tag to ensure version compatibility
|
|
55
|
+
download_url = f"https://github.com/topoteretes/cognee/archive/refs/tags/v{clean_version}.zip"
|
|
56
|
+
|
|
57
|
+
return download_url, version
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def download_frontend_assets(force: bool = False) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Download and cache frontend assets.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
force: If True, re-download even if already cached
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
bool: True if successful, False otherwise
|
|
69
|
+
"""
|
|
70
|
+
cache_dir = get_frontend_cache_dir()
|
|
71
|
+
frontend_dir = cache_dir / "frontend"
|
|
72
|
+
version_file = cache_dir / "version.txt"
|
|
73
|
+
|
|
74
|
+
# Check if already downloaded and up to date
|
|
75
|
+
if not force and frontend_dir.exists() and version_file.exists():
|
|
76
|
+
try:
|
|
77
|
+
cached_version = version_file.read_text().strip()
|
|
78
|
+
current_version = get_cognee_version()
|
|
79
|
+
|
|
80
|
+
# Compare normalized versions to handle development versions
|
|
81
|
+
cached_normalized = normalize_version_for_comparison(cached_version)
|
|
82
|
+
current_normalized = normalize_version_for_comparison(current_version)
|
|
83
|
+
|
|
84
|
+
if cached_normalized == current_normalized:
|
|
85
|
+
logger.debug(f"Frontend assets already cached for version {current_version}")
|
|
86
|
+
return True
|
|
87
|
+
else:
|
|
88
|
+
logger.info(
|
|
89
|
+
f"Version mismatch detected: cached={cached_version}, current={current_version}"
|
|
90
|
+
)
|
|
91
|
+
logger.info("Updating frontend cache to match current cognee version...")
|
|
92
|
+
# Clear the old cached version
|
|
93
|
+
if frontend_dir.exists():
|
|
94
|
+
shutil.rmtree(frontend_dir)
|
|
95
|
+
if version_file.exists():
|
|
96
|
+
version_file.unlink()
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.debug(f"Error checking cached version: {e}")
|
|
99
|
+
# Clear potentially corrupted cache
|
|
100
|
+
if frontend_dir.exists():
|
|
101
|
+
shutil.rmtree(frontend_dir)
|
|
102
|
+
if version_file.exists():
|
|
103
|
+
version_file.unlink()
|
|
104
|
+
|
|
105
|
+
download_url, version = get_frontend_download_info()
|
|
106
|
+
|
|
107
|
+
logger.info(f"Downloading cognee frontend assets for version {version}...")
|
|
108
|
+
logger.info("This will be cached and reused until the cognee version changes.")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# Create a temporary directory for download
|
|
112
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
113
|
+
temp_path = Path(temp_dir)
|
|
114
|
+
archive_path = temp_path / "cognee-main.zip"
|
|
115
|
+
|
|
116
|
+
# Download the actual cognee repository from releases
|
|
117
|
+
logger.info(
|
|
118
|
+
f"Downloading cognee v{version.replace('-local', '')} from GitHub releases..."
|
|
119
|
+
)
|
|
120
|
+
logger.info(f"URL: {download_url}")
|
|
121
|
+
response = requests.get(download_url, stream=True, timeout=60)
|
|
122
|
+
response.raise_for_status()
|
|
123
|
+
|
|
124
|
+
with open(archive_path, "wb") as f:
|
|
125
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
126
|
+
f.write(chunk)
|
|
127
|
+
|
|
128
|
+
# Extract the archive and find the cognee-frontend directory
|
|
129
|
+
if frontend_dir.exists():
|
|
130
|
+
shutil.rmtree(frontend_dir)
|
|
131
|
+
|
|
132
|
+
with zipfile.ZipFile(archive_path, "r") as zip_file:
|
|
133
|
+
# Extract to temp directory first
|
|
134
|
+
extract_dir = temp_path / "extracted"
|
|
135
|
+
zip_file.extractall(extract_dir)
|
|
136
|
+
|
|
137
|
+
# Find the cognee-frontend directory in the extracted content
|
|
138
|
+
# The archive structure will be: cognee-{version}/cognee-frontend/
|
|
139
|
+
cognee_frontend_source = None
|
|
140
|
+
for root, dirs, files in os.walk(extract_dir):
|
|
141
|
+
if "cognee-frontend" in dirs:
|
|
142
|
+
cognee_frontend_source = Path(root) / "cognee-frontend"
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
if not cognee_frontend_source or not cognee_frontend_source.exists():
|
|
146
|
+
logger.error(
|
|
147
|
+
"Could not find cognee-frontend directory in downloaded release archive"
|
|
148
|
+
)
|
|
149
|
+
logger.error("This might indicate a version mismatch or missing release.")
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
# Copy the cognee-frontend to our cache
|
|
153
|
+
shutil.copytree(cognee_frontend_source, frontend_dir)
|
|
154
|
+
logger.debug(f"Frontend extracted to: {frontend_dir}")
|
|
155
|
+
|
|
156
|
+
# Write version info for future cache validation
|
|
157
|
+
version_file.write_text(version)
|
|
158
|
+
logger.debug(f"Cached frontend for cognee version: {version}")
|
|
159
|
+
|
|
160
|
+
logger.info(
|
|
161
|
+
f"✓ Cognee frontend v{version.replace('-local', '')} downloaded and cached successfully!"
|
|
162
|
+
)
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
except requests.exceptions.RequestException as e:
|
|
166
|
+
if "404" in str(e):
|
|
167
|
+
logger.error(f"Release v{version.replace('-local', '')} not found on GitHub.")
|
|
168
|
+
logger.error(
|
|
169
|
+
"This version might not have been released yet, or you're using a development version."
|
|
170
|
+
)
|
|
171
|
+
logger.error("Try using a stable release version of cognee.")
|
|
172
|
+
else:
|
|
173
|
+
logger.error(f"Failed to download from GitHub: {str(e)}")
|
|
174
|
+
logger.error("You can still use cognee without the UI functionality.")
|
|
175
|
+
return False
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Failed to download frontend assets: {str(e)}")
|
|
178
|
+
logger.error("You can still use cognee without the UI functionality.")
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def find_frontend_path() -> Optional[Path]:
|
|
183
|
+
"""
|
|
184
|
+
Find the cognee-frontend directory.
|
|
185
|
+
Checks both development location and cached download location.
|
|
186
|
+
"""
|
|
187
|
+
current_file = Path(__file__)
|
|
188
|
+
|
|
189
|
+
# First, try development paths (for contributors/developers)
|
|
190
|
+
dev_search_paths = [
|
|
191
|
+
current_file.parents[4] / "cognee-frontend", # from cognee/api/v1/ui/ui.py to project root
|
|
192
|
+
current_file.parents[3] / "cognee-frontend", # fallback path
|
|
193
|
+
current_file.parents[2] / "cognee-frontend", # another fallback
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
for path in dev_search_paths:
|
|
197
|
+
if path.exists() and (path / "package.json").exists():
|
|
198
|
+
logger.debug(f"Found development frontend at: {path}")
|
|
199
|
+
return path
|
|
200
|
+
|
|
201
|
+
# Then try cached download location (for pip-installed users)
|
|
202
|
+
cache_dir = get_frontend_cache_dir()
|
|
203
|
+
cached_frontend = cache_dir / "frontend"
|
|
204
|
+
|
|
205
|
+
if cached_frontend.exists() and (cached_frontend / "package.json").exists():
|
|
206
|
+
logger.debug(f"Found cached frontend at: {cached_frontend}")
|
|
207
|
+
return cached_frontend
|
|
208
|
+
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def check_node_npm() -> tuple[bool, str]:
|
|
213
|
+
"""
|
|
214
|
+
Check if Node.js and npm are available.
|
|
215
|
+
Returns (is_available, error_message)
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
# Check Node.js
|
|
219
|
+
result = subprocess.run(["node", "--version"], capture_output=True, text=True, timeout=10)
|
|
220
|
+
if result.returncode != 0:
|
|
221
|
+
return False, "Node.js is not installed or not in PATH"
|
|
222
|
+
|
|
223
|
+
node_version = result.stdout.strip()
|
|
224
|
+
logger.debug(f"Found Node.js version: {node_version}")
|
|
225
|
+
|
|
226
|
+
# Check npm
|
|
227
|
+
result = subprocess.run(["npm", "--version"], capture_output=True, text=True, timeout=10)
|
|
228
|
+
if result.returncode != 0:
|
|
229
|
+
return False, "npm is not installed or not in PATH"
|
|
230
|
+
|
|
231
|
+
npm_version = result.stdout.strip()
|
|
232
|
+
logger.debug(f"Found npm version: {npm_version}")
|
|
233
|
+
|
|
234
|
+
return True, f"Node.js {node_version}, npm {npm_version}"
|
|
235
|
+
|
|
236
|
+
except subprocess.TimeoutExpired:
|
|
237
|
+
return False, "Timeout checking Node.js/npm installation"
|
|
238
|
+
except FileNotFoundError:
|
|
239
|
+
return False, "Node.js/npm not found. Please install Node.js from https://nodejs.org/"
|
|
240
|
+
except Exception as e:
|
|
241
|
+
return False, f"Error checking Node.js/npm: {str(e)}"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def install_frontend_dependencies(frontend_path: Path) -> bool:
|
|
245
|
+
"""
|
|
246
|
+
Install frontend dependencies if node_modules doesn't exist.
|
|
247
|
+
This is needed for both development and downloaded frontends since both use npm run dev.
|
|
248
|
+
"""
|
|
249
|
+
node_modules = frontend_path / "node_modules"
|
|
250
|
+
if node_modules.exists():
|
|
251
|
+
logger.debug("Frontend dependencies already installed")
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
logger.info("Installing frontend dependencies (this may take a few minutes)...")
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
result = subprocess.run(
|
|
258
|
+
["npm", "install"],
|
|
259
|
+
cwd=frontend_path,
|
|
260
|
+
capture_output=True,
|
|
261
|
+
text=True,
|
|
262
|
+
timeout=300, # 5 minutes timeout
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if result.returncode == 0:
|
|
266
|
+
logger.info("Frontend dependencies installed successfully")
|
|
267
|
+
return True
|
|
268
|
+
else:
|
|
269
|
+
logger.error(f"Failed to install dependencies: {result.stderr}")
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
except subprocess.TimeoutExpired:
|
|
273
|
+
logger.error("Timeout installing frontend dependencies")
|
|
274
|
+
return False
|
|
275
|
+
except Exception as e:
|
|
276
|
+
logger.error(f"Error installing frontend dependencies: {str(e)}")
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def is_development_frontend(frontend_path: Path) -> bool:
|
|
281
|
+
"""
|
|
282
|
+
Check if this is a development frontend (has Next.js) vs downloaded assets.
|
|
283
|
+
"""
|
|
284
|
+
package_json_path = frontend_path / "package.json"
|
|
285
|
+
if not package_json_path.exists():
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
import json
|
|
290
|
+
|
|
291
|
+
with open(package_json_path) as f:
|
|
292
|
+
package_data = json.load(f)
|
|
293
|
+
|
|
294
|
+
# Development frontend has Next.js as dependency
|
|
295
|
+
dependencies = package_data.get("dependencies", {})
|
|
296
|
+
dev_dependencies = package_data.get("devDependencies", {})
|
|
297
|
+
|
|
298
|
+
return "next" in dependencies or "next" in dev_dependencies
|
|
299
|
+
except Exception:
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def prompt_user_for_download() -> bool:
|
|
304
|
+
"""
|
|
305
|
+
Ask user if they want to download the frontend assets.
|
|
306
|
+
Returns True if user consents, False otherwise.
|
|
307
|
+
"""
|
|
308
|
+
try:
|
|
309
|
+
print("\n" + "=" * 60)
|
|
310
|
+
print("🎨 Cognee UI Setup Required")
|
|
311
|
+
print("=" * 60)
|
|
312
|
+
print("The cognee frontend is not available on your system.")
|
|
313
|
+
print("This is required to use the web interface.")
|
|
314
|
+
print("\nWhat will happen:")
|
|
315
|
+
print("• Download the actual cognee-frontend from GitHub")
|
|
316
|
+
print("• Cache it in your home directory (~/.cognee/ui-cache/)")
|
|
317
|
+
print("• Install dependencies with npm (requires Node.js)")
|
|
318
|
+
print("• This is a one-time setup per cognee version")
|
|
319
|
+
print("\nThe frontend will then be available offline for future use.")
|
|
320
|
+
|
|
321
|
+
response = input("\nWould you like to download the frontend now? (y/N): ").strip().lower()
|
|
322
|
+
return response in ["y", "yes"]
|
|
323
|
+
except (KeyboardInterrupt, EOFError):
|
|
324
|
+
print("\nOperation cancelled by user.")
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def start_ui(
|
|
329
|
+
pid_callback: Callable[[int], None],
|
|
330
|
+
host: str = "localhost",
|
|
331
|
+
port: int = 3000,
|
|
332
|
+
open_browser: bool = True,
|
|
333
|
+
auto_download: bool = False,
|
|
334
|
+
start_backend: bool = False,
|
|
335
|
+
backend_host: str = "localhost",
|
|
336
|
+
backend_port: int = 8000,
|
|
337
|
+
) -> Optional[subprocess.Popen]:
|
|
338
|
+
"""
|
|
339
|
+
Start the cognee frontend UI server, optionally with the backend API server.
|
|
340
|
+
|
|
341
|
+
This function will:
|
|
342
|
+
1. Optionally start the cognee backend API server
|
|
343
|
+
2. Find the cognee-frontend directory (development) or download it (pip install)
|
|
344
|
+
3. Check if Node.js and npm are available (for development mode)
|
|
345
|
+
4. Install dependencies if needed (development mode)
|
|
346
|
+
5. Start the frontend server
|
|
347
|
+
6. Optionally open the browser
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
pid_callback: Callback to notify with PID of each spawned process
|
|
351
|
+
host: Host to bind the frontend server to (default: localhost)
|
|
352
|
+
port: Port to run the frontend server on (default: 3000)
|
|
353
|
+
open_browser: Whether to open the browser automatically (default: True)
|
|
354
|
+
auto_download: If True, download frontend without prompting (default: False)
|
|
355
|
+
start_backend: If True, also start the cognee API backend server (default: False)
|
|
356
|
+
backend_host: Host to bind the backend server to (default: localhost)
|
|
357
|
+
backend_port: Port to run the backend server on (default: 8000)
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
subprocess.Popen object representing the running frontend server, or None if failed
|
|
361
|
+
Note: If backend is started, it runs in a separate process that will be cleaned up
|
|
362
|
+
when the frontend process is terminated.
|
|
363
|
+
|
|
364
|
+
Example:
|
|
365
|
+
>>> import cognee
|
|
366
|
+
>>> # Start just the frontend
|
|
367
|
+
>>> server = cognee.start_ui()
|
|
368
|
+
>>>
|
|
369
|
+
>>> # Start both frontend and backend
|
|
370
|
+
>>> server = cognee.start_ui(start_backend=True)
|
|
371
|
+
>>> # UI will be available at http://localhost:3000
|
|
372
|
+
>>> # API will be available at http://localhost:8000
|
|
373
|
+
>>> # To stop both servers later:
|
|
374
|
+
>>> server.terminate()
|
|
375
|
+
"""
|
|
376
|
+
logger.info("Starting cognee UI...")
|
|
377
|
+
backend_process = None
|
|
378
|
+
|
|
379
|
+
# Start backend server if requested
|
|
380
|
+
if start_backend:
|
|
381
|
+
logger.info("Starting cognee backend API server...")
|
|
382
|
+
try:
|
|
383
|
+
import sys
|
|
384
|
+
|
|
385
|
+
backend_process = subprocess.Popen(
|
|
386
|
+
[
|
|
387
|
+
sys.executable,
|
|
388
|
+
"-m",
|
|
389
|
+
"uvicorn",
|
|
390
|
+
"cognee.api.client:app",
|
|
391
|
+
"--host",
|
|
392
|
+
backend_host,
|
|
393
|
+
"--port",
|
|
394
|
+
str(backend_port),
|
|
395
|
+
],
|
|
396
|
+
# Inherit stdout/stderr from parent process to show logs
|
|
397
|
+
stdout=None,
|
|
398
|
+
stderr=None,
|
|
399
|
+
preexec_fn=os.setsid if hasattr(os, "setsid") else None,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
pid_callback(backend_process.pid)
|
|
403
|
+
|
|
404
|
+
# Give the backend a moment to start
|
|
405
|
+
time.sleep(2)
|
|
406
|
+
|
|
407
|
+
if backend_process.poll() is not None:
|
|
408
|
+
logger.error("Backend server failed to start - process exited early")
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
logger.info(f"✓ Backend API started at http://{backend_host}:{backend_port}")
|
|
412
|
+
|
|
413
|
+
except Exception as e:
|
|
414
|
+
logger.error(f"Failed to start backend server: {str(e)}")
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
# Find frontend directory
|
|
418
|
+
frontend_path = find_frontend_path()
|
|
419
|
+
|
|
420
|
+
if not frontend_path:
|
|
421
|
+
logger.info("Frontend not found locally. This is normal for pip-installed cognee.")
|
|
422
|
+
|
|
423
|
+
# Offer to download the frontend
|
|
424
|
+
if auto_download or prompt_user_for_download():
|
|
425
|
+
if download_frontend_assets():
|
|
426
|
+
frontend_path = find_frontend_path()
|
|
427
|
+
if not frontend_path:
|
|
428
|
+
logger.error(
|
|
429
|
+
"Download succeeded but frontend still not found. This is unexpected."
|
|
430
|
+
)
|
|
431
|
+
return None
|
|
432
|
+
else:
|
|
433
|
+
logger.error("Failed to download frontend assets.")
|
|
434
|
+
return None
|
|
435
|
+
else:
|
|
436
|
+
logger.info("Frontend download declined. UI functionality not available.")
|
|
437
|
+
logger.info("You can still use all other cognee features without the web interface.")
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
# Check Node.js and npm
|
|
441
|
+
node_available, node_message = check_node_npm()
|
|
442
|
+
if not node_available:
|
|
443
|
+
logger.error(f"Cannot start UI: {node_message}")
|
|
444
|
+
logger.error("Please install Node.js from https://nodejs.org/ to use the UI functionality")
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
logger.debug(f"Environment check passed: {node_message}")
|
|
448
|
+
|
|
449
|
+
# Install dependencies if needed
|
|
450
|
+
if not install_frontend_dependencies(frontend_path):
|
|
451
|
+
logger.error("Failed to install frontend dependencies")
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
# Prepare environment variables
|
|
455
|
+
env = os.environ.copy()
|
|
456
|
+
env["HOST"] = host
|
|
457
|
+
env["PORT"] = str(port)
|
|
458
|
+
|
|
459
|
+
# Start the development server
|
|
460
|
+
logger.info(f"Starting frontend server at http://{host}:{port}")
|
|
461
|
+
logger.info("This may take a moment to compile and start...")
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
# Create frontend in its own process group for clean termination
|
|
465
|
+
process = subprocess.Popen(
|
|
466
|
+
["npm", "run", "dev"],
|
|
467
|
+
cwd=frontend_path,
|
|
468
|
+
env=env,
|
|
469
|
+
stdout=subprocess.PIPE,
|
|
470
|
+
stderr=subprocess.PIPE,
|
|
471
|
+
text=True,
|
|
472
|
+
preexec_fn=os.setsid if hasattr(os, "setsid") else None,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
pid_callback(process.pid)
|
|
476
|
+
|
|
477
|
+
# Give it a moment to start up
|
|
478
|
+
time.sleep(3)
|
|
479
|
+
|
|
480
|
+
# Check if process is still running
|
|
481
|
+
if process.poll() is not None:
|
|
482
|
+
stdout, stderr = process.communicate()
|
|
483
|
+
logger.error("Frontend server failed to start:")
|
|
484
|
+
logger.error(f"stdout: {stdout}")
|
|
485
|
+
logger.error(f"stderr: {stderr}")
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
# Open browser if requested
|
|
489
|
+
if open_browser:
|
|
490
|
+
|
|
491
|
+
def open_browser_delayed():
|
|
492
|
+
time.sleep(5) # Give Next.js time to fully start
|
|
493
|
+
try:
|
|
494
|
+
webbrowser.open(f"http://{host}:{port}") # TODO: use dashboard url?
|
|
495
|
+
except Exception as e:
|
|
496
|
+
logger.warning(f"Could not open browser automatically: {e}")
|
|
497
|
+
|
|
498
|
+
browser_thread = threading.Thread(target=open_browser_delayed, daemon=True)
|
|
499
|
+
browser_thread.start()
|
|
500
|
+
|
|
501
|
+
logger.info("✓ Cognee UI is starting up...")
|
|
502
|
+
logger.info(f"✓ Open your browser to: http://{host}:{port}")
|
|
503
|
+
logger.info("✓ The UI will be available once Next.js finishes compiling")
|
|
504
|
+
|
|
505
|
+
# Store backend process reference in the frontend process for cleanup
|
|
506
|
+
if backend_process:
|
|
507
|
+
process._cognee_backend_process = backend_process
|
|
508
|
+
|
|
509
|
+
return process
|
|
510
|
+
|
|
511
|
+
except Exception as e:
|
|
512
|
+
logger.error(f"Failed to start frontend server: {str(e)}")
|
|
513
|
+
# Clean up backend process if it was started
|
|
514
|
+
if backend_process:
|
|
515
|
+
logger.info("Cleaning up backend process due to frontend failure...")
|
|
516
|
+
try:
|
|
517
|
+
backend_process.terminate()
|
|
518
|
+
backend_process.wait(timeout=5)
|
|
519
|
+
except (subprocess.TimeoutExpired, OSError, ProcessLookupError):
|
|
520
|
+
try:
|
|
521
|
+
backend_process.kill()
|
|
522
|
+
backend_process.wait()
|
|
523
|
+
except (OSError, ProcessLookupError):
|
|
524
|
+
pass
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def stop_ui(process: subprocess.Popen) -> bool:
|
|
529
|
+
"""
|
|
530
|
+
Stop a running UI server process and backend process (if started), along with all their children.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
process: The subprocess.Popen object returned by start_ui()
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
bool: True if stopped successfully, False otherwise
|
|
537
|
+
"""
|
|
538
|
+
if not process:
|
|
539
|
+
return False
|
|
540
|
+
|
|
541
|
+
success = True
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
# First, stop the backend process if it exists
|
|
545
|
+
backend_process = getattr(process, "_cognee_backend_process", None)
|
|
546
|
+
if backend_process:
|
|
547
|
+
logger.info("Stopping backend server...")
|
|
548
|
+
try:
|
|
549
|
+
backend_process.terminate()
|
|
550
|
+
try:
|
|
551
|
+
backend_process.wait(timeout=5)
|
|
552
|
+
logger.info("Backend server stopped gracefully")
|
|
553
|
+
except subprocess.TimeoutExpired:
|
|
554
|
+
logger.warning("Backend didn't terminate gracefully, forcing kill")
|
|
555
|
+
backend_process.kill()
|
|
556
|
+
backend_process.wait()
|
|
557
|
+
logger.info("Backend server stopped")
|
|
558
|
+
except Exception as e:
|
|
559
|
+
logger.error(f"Error stopping backend server: {str(e)}")
|
|
560
|
+
success = False
|
|
561
|
+
|
|
562
|
+
# Now stop the frontend process
|
|
563
|
+
logger.info("Stopping frontend server...")
|
|
564
|
+
# Try to terminate the process group (includes child processes like Next.js)
|
|
565
|
+
if hasattr(os, "killpg"):
|
|
566
|
+
try:
|
|
567
|
+
# Kill the entire process group
|
|
568
|
+
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
|
569
|
+
logger.debug("Sent SIGTERM to process group")
|
|
570
|
+
except (OSError, ProcessLookupError):
|
|
571
|
+
# Fall back to terminating just the main process
|
|
572
|
+
process.terminate()
|
|
573
|
+
logger.debug("Terminated main process only")
|
|
574
|
+
else:
|
|
575
|
+
process.terminate()
|
|
576
|
+
logger.debug("Terminated main process (Windows)")
|
|
577
|
+
|
|
578
|
+
try:
|
|
579
|
+
process.wait(timeout=10)
|
|
580
|
+
logger.info("Frontend server stopped gracefully")
|
|
581
|
+
except subprocess.TimeoutExpired:
|
|
582
|
+
logger.warning("Frontend didn't terminate gracefully, forcing kill")
|
|
583
|
+
|
|
584
|
+
# Force kill the process group
|
|
585
|
+
if hasattr(os, "killpg"):
|
|
586
|
+
try:
|
|
587
|
+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
588
|
+
logger.debug("Sent SIGKILL to process group")
|
|
589
|
+
except (OSError, ProcessLookupError):
|
|
590
|
+
process.kill()
|
|
591
|
+
logger.debug("Force killed main process only")
|
|
592
|
+
else:
|
|
593
|
+
process.kill()
|
|
594
|
+
logger.debug("Force killed main process (Windows)")
|
|
595
|
+
|
|
596
|
+
process.wait()
|
|
597
|
+
|
|
598
|
+
if success:
|
|
599
|
+
logger.info("UI servers stopped successfully")
|
|
600
|
+
|
|
601
|
+
return success
|
|
602
|
+
|
|
603
|
+
except Exception as e:
|
|
604
|
+
logger.error(f"Error stopping UI servers: {str(e)}")
|
|
605
|
+
return False
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
# Convenience function similar to DuckDB's approach
|
|
609
|
+
def ui() -> Optional[subprocess.Popen]:
|
|
610
|
+
"""
|
|
611
|
+
Convenient alias for start_ui() with default parameters.
|
|
612
|
+
Similar to how DuckDB provides simple ui() function.
|
|
613
|
+
"""
|
|
614
|
+
return start_ui()
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
if __name__ == "__main__":
|
|
618
|
+
# Test the UI startup
|
|
619
|
+
server = start_ui()
|
|
620
|
+
if server:
|
|
621
|
+
try:
|
|
622
|
+
input("Press Enter to stop the server...")
|
|
623
|
+
finally:
|
|
624
|
+
stop_ui(server)
|