Merge 3d2b300789 into 2ef347f8fa
This commit is contained in:
commit
836476a492
18 changed files with 1710 additions and 25 deletions
|
|
@ -92,6 +92,7 @@ def _discover_commands() -> List[Type[SupportsCliCommand]]:
|
|||
("cognee.cli.commands.cognify_command", "CognifyCommand"),
|
||||
("cognee.cli.commands.delete_command", "DeleteCommand"),
|
||||
("cognee.cli.commands.config_command", "ConfigCommand"),
|
||||
("cognee.cli.commands.tui_command", "TuiCommand"),
|
||||
]
|
||||
|
||||
for module_path, class_name in command_modules:
|
||||
|
|
|
|||
56
cognee/cli/commands/tui_command.py
Normal file
56
cognee/cli/commands/tui_command.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import argparse
|
||||
from cognee.cli import SupportsCliCommand
|
||||
from cognee.cli.config import DEFAULT_DOCS_URL
|
||||
import cognee.cli.echo as fmt
|
||||
from cognee.cli.exceptions import CliCommandException
|
||||
|
||||
|
||||
class TuiCommand(SupportsCliCommand):
|
||||
@property
|
||||
def command_string(self) -> str:
|
||||
return "tui"
|
||||
|
||||
@property
|
||||
def help_string(self) -> str:
|
||||
return "Launch the interactive Textual TUI for cognee commands"
|
||||
|
||||
@property
|
||||
def docs_url(self) -> str:
|
||||
return f"{DEFAULT_DOCS_URL}/usage/tui"
|
||||
|
||||
def configure_parser(self, parser: argparse.ArgumentParser) -> None:
|
||||
# No additional arguments for now
|
||||
pass
|
||||
|
||||
def execute(self, args: argparse.Namespace) -> None:
|
||||
try:
|
||||
from textual.app import App
|
||||
from cognee.cli.tui.home_screen import HomeScreen
|
||||
from cognee.shared.logging_utils import setup_logging
|
||||
class CogneeTUI(App):
|
||||
"""Main TUI application for cognee."""
|
||||
|
||||
CSS = """
|
||||
Screen {
|
||||
background: $surface;
|
||||
}
|
||||
"""
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Push the home screen on mount."""
|
||||
self.push_screen(HomeScreen())
|
||||
|
||||
setup_logging(enable_console_logging=False)
|
||||
app = CogneeTUI()
|
||||
app.run()
|
||||
except ImportError:
|
||||
raise CliCommandException(
|
||||
"Textual is not installed. Install with: pip install textual",
|
||||
docs_url=self.docs_url,
|
||||
)
|
||||
except Exception as ex:
|
||||
raise CliCommandException(
|
||||
f"Failed to launch TUI: {str(ex)}",
|
||||
docs_url=self.docs_url,
|
||||
raiseable_exception=ex,
|
||||
)
|
||||
1
cognee/cli/tui/__init__.py
Normal file
1
cognee/cli/tui/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
126
cognee/cli/tui/add_screen.py
Normal file
126
cognee/cli/tui/add_screen.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import asyncio
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import Input, Label, Static, TextArea
|
||||
from textual.containers import Container, Vertical
|
||||
from textual.binding import Binding
|
||||
|
||||
from cognee.cli.tui.base_screen import BaseTUIScreen
|
||||
|
||||
|
||||
class AddTUIScreen(BaseTUIScreen):
|
||||
"""TUI screen for adding data to cognee."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("q", "quit_app", "Quit"),
|
||||
Binding("escape", "back", "Back"),
|
||||
Binding("ctrl+s", "submit", "Submit"),
|
||||
Binding("ctrl+v", "paste", "Paste", show=False),
|
||||
]
|
||||
|
||||
CSS = (
|
||||
BaseTUIScreen.CSS
|
||||
+ """
|
||||
#data-input {
|
||||
height: 8;
|
||||
min-height: 8;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.is_processing = False
|
||||
|
||||
def compose_content(self) -> ComposeResult:
|
||||
with Container(classes="tui-main-container"):
|
||||
with Container(classes="tui-title-wrapper"):
|
||||
yield Static("📥 Add Data to Cognee", classes="tui-title-bordered")
|
||||
with Vertical(classes="tui-form"):
|
||||
yield Label(
|
||||
"Data (text, file path (/path/to/doc), URL, or S3 path (s3://bucket)):",
|
||||
classes="tui-label-spaced",
|
||||
)
|
||||
yield TextArea(
|
||||
"",
|
||||
id="data-input",
|
||||
)
|
||||
|
||||
yield Label("Dataset Name:", classes="tui-label-spaced")
|
||||
yield Input(placeholder="main_dataset", value="main_dataset", id="dataset-input")
|
||||
yield Static("", classes="tui-status")
|
||||
|
||||
def compose_footer(self) -> ComposeResult:
|
||||
yield Static("Ctrl+S: Add • Esc: Back • q: Quit", classes="tui-footer")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Focus the data input on mount."""
|
||||
data_input = self.query_one("#data-input", TextArea)
|
||||
data_input.focus()
|
||||
|
||||
def action_back(self) -> None:
|
||||
"""Go back to home screen."""
|
||||
if not self.is_processing:
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self) -> None:
|
||||
"""Quit the entire application."""
|
||||
self.app.exit()
|
||||
|
||||
def action_paste(self) -> None:
|
||||
"""Handle paste action - Textual handles this automatically."""
|
||||
pass
|
||||
|
||||
def action_submit(self) -> None:
|
||||
"""Submit the form."""
|
||||
if not self.is_processing:
|
||||
self._submit_data()
|
||||
|
||||
def _submit_data(self) -> None:
|
||||
"""Process and submit the data."""
|
||||
data_input = self.query_one("#data-input", TextArea)
|
||||
dataset_input = self.query_one("#dataset-input", Input)
|
||||
status = self.query_one(".tui-status", Static)
|
||||
|
||||
data = data_input.text.strip()
|
||||
dataset_name = dataset_input.value.strip() or "main_dataset"
|
||||
|
||||
if not data:
|
||||
status.update("[red]✗ Please enter data to add[/red]")
|
||||
return
|
||||
|
||||
self.is_processing = True
|
||||
status.update("[yellow]⏳ Processing...[/yellow]")
|
||||
|
||||
# Disable inputs during processing
|
||||
data_input.disabled = True
|
||||
dataset_input.disabled = True
|
||||
|
||||
# Run async add operation
|
||||
asyncio.create_task(self._add_data_async(data, dataset_name))
|
||||
|
||||
async def _add_data_async(self, data: str, dataset_name: str) -> None:
|
||||
"""Async function to add data to cognee."""
|
||||
status = self.query_one(".tui-status", Static)
|
||||
|
||||
try:
|
||||
import cognee
|
||||
|
||||
await cognee.add(data=data, dataset_name=dataset_name)
|
||||
|
||||
status.update(f"[green]✓ Successfully added data to dataset '{dataset_name}'[/green]")
|
||||
|
||||
# Clear the data input after successful add
|
||||
data_input = self.query_one("#data-input", TextArea)
|
||||
data_input.clear()
|
||||
|
||||
except Exception as e:
|
||||
status.update(f"[red]✗ Failed to add data: {str(e)}[/red]")
|
||||
|
||||
finally:
|
||||
# Re-enable inputs
|
||||
self.is_processing = False
|
||||
data_input = self.query_one("#data-input", TextArea)
|
||||
dataset_input = self.query_one("#dataset-input", Input)
|
||||
data_input.disabled = False
|
||||
dataset_input.disabled = False
|
||||
data_input.focus()
|
||||
45
cognee/cli/tui/base_screen.py
Normal file
45
cognee/cli/tui/base_screen.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from textual.screen import Screen
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import Static
|
||||
|
||||
from cognee.version import get_cognee_version
|
||||
from cognee.cli.tui.common_styles import COMMON_STYLES
|
||||
|
||||
|
||||
class BaseTUIScreen(Screen):
|
||||
"""Base screen class with constant header for all TUI screens."""
|
||||
|
||||
# Subclasses should override this CSS and add their own styles
|
||||
CSS = (
|
||||
COMMON_STYLES
|
||||
+ """
|
||||
#header {
|
||||
dock: top;
|
||||
background: $boost;
|
||||
color: $text;
|
||||
content-align: center middle;
|
||||
border: solid $primary;
|
||||
text-style: bold;
|
||||
padding: 1;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def compose_header(self) -> ComposeResult:
|
||||
"""Compose the constant header widget."""
|
||||
version = get_cognee_version()
|
||||
yield Static(f"🧠 cognee v{version}", id="header")
|
||||
|
||||
def compose_content(self) -> ComposeResult:
|
||||
"""Override this method in subclasses to provide screen-specific content."""
|
||||
yield from ()
|
||||
|
||||
def compose_footer(self) -> ComposeResult:
|
||||
"""Override this method in subclasses to provide screen-specific footer."""
|
||||
yield from ()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the screen with header, content, and footer."""
|
||||
yield from self.compose_header()
|
||||
yield from self.compose_content()
|
||||
yield from self.compose_footer()
|
||||
169
cognee/cli/tui/cognify_screen.py
Normal file
169
cognee/cli/tui/cognify_screen.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import asyncio
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import Input, Label, Static, Checkbox, RadioSet, RadioButton
|
||||
from textual.containers import Container, Vertical
|
||||
from textual.binding import Binding
|
||||
from cognee.cli.tui.base_screen import BaseTUIScreen
|
||||
from cognee.cli.config import CHUNKER_CHOICES
|
||||
|
||||
|
||||
class CognifyTUIScreen(BaseTUIScreen):
|
||||
"""TUI screen for cognifying data in cognee."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("q", "quit_app", "Quit"),
|
||||
Binding("escape", "back", "Back"),
|
||||
Binding("ctrl+s", "submit", "Submit"),
|
||||
]
|
||||
|
||||
CSS = (
|
||||
BaseTUIScreen.CSS
|
||||
+ """
|
||||
Checkbox {
|
||||
margin-top: 1;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
RadioSet {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
height: 1;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.is_processing = False
|
||||
|
||||
def compose_content(self) -> ComposeResult:
|
||||
with Container(classes="tui-main-container"):
|
||||
with Container(classes="tui-title-wrapper"):
|
||||
yield Static("⚡ Cognify Data", classes="tui-title-bordered")
|
||||
with Vertical(classes="tui-form"):
|
||||
yield Label(
|
||||
"Dataset Name:", classes="tui-label-spaced"
|
||||
)
|
||||
yield Input(
|
||||
placeholder="Enter the dataset name here.", value="", id="dataset-input"
|
||||
)
|
||||
|
||||
yield Label("Chunker Type:", classes="tui-label-spaced")
|
||||
with RadioSet(id="chunker-radio"):
|
||||
for chunker in CHUNKER_CHOICES:
|
||||
yield RadioButton(chunker, value=(chunker == "TextChunker"))
|
||||
|
||||
yield Checkbox("Run in background", id="background-checkbox")
|
||||
yield Static("", classes="tui-status")
|
||||
|
||||
def compose_footer(self) -> ComposeResult:
|
||||
yield Static("Ctrl+S: Start • Esc: Back • q: Quit", classes="tui-footer")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Focus the dataset input on mount."""
|
||||
dataset_input = self.query_one("#dataset-input", Input)
|
||||
dataset_input.focus()
|
||||
|
||||
def action_back(self) -> None:
|
||||
"""Go back to home screen."""
|
||||
if not self.is_processing:
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self) -> None:
|
||||
"""Quit the entire application."""
|
||||
self.app.exit()
|
||||
|
||||
def action_submit(self) -> None:
|
||||
"""Submit the form."""
|
||||
if not self.is_processing:
|
||||
self._submit_cognify()
|
||||
|
||||
def _submit_cognify(self) -> None:
|
||||
"""Process and submit the cognify request."""
|
||||
dataset_input = self.query_one("#dataset-input", Input)
|
||||
chunker_radio = self.query_one("#chunker-radio", RadioSet)
|
||||
background_checkbox = self.query_one("#background-checkbox", Checkbox)
|
||||
status = self.query_one(".tui-status", Static)
|
||||
|
||||
dataset_name = dataset_input.value.strip() or None
|
||||
chunker_type = (
|
||||
str(chunker_radio.pressed_button.label)
|
||||
if chunker_radio.pressed_button
|
||||
else "TextChunker"
|
||||
)
|
||||
run_background = background_checkbox.value
|
||||
|
||||
self.is_processing = True
|
||||
status.update("[yellow]⏳ Starting cognification...[/yellow]")
|
||||
|
||||
# Disable inputs during processing
|
||||
dataset_input.disabled = True
|
||||
chunker_radio.disabled = True
|
||||
background_checkbox.disabled = True
|
||||
|
||||
# Run async cognify operation
|
||||
asyncio.create_task(self._cognify_async(dataset_name, chunker_type, run_background))
|
||||
|
||||
async def _cognify_async(
|
||||
self, dataset_name: str | None, chunker_type: str, run_background: bool
|
||||
) -> None:
|
||||
"""Async function to cognify data."""
|
||||
status = self.query_one(".tui-status", Static)
|
||||
from cognee.modules.chunking.TextChunker import TextChunker
|
||||
|
||||
try:
|
||||
# Get chunker class
|
||||
chunker_class = TextChunker
|
||||
if chunker_type == "LangchainChunker":
|
||||
try:
|
||||
from cognee.modules.chunking.LangchainChunker import LangchainChunker
|
||||
except ImportError:
|
||||
LangchainChunker = None
|
||||
if LangchainChunker is not None:
|
||||
chunker_class = LangchainChunker
|
||||
else:
|
||||
status.update(
|
||||
"[yellow]⚠ LangchainChunker not available, using TextChunker[/yellow]"
|
||||
)
|
||||
elif chunker_type == "CsvChunker":
|
||||
try:
|
||||
from cognee.modules.chunking.CsvChunker import CsvChunker
|
||||
except ImportError:
|
||||
CsvChunker = None
|
||||
if CsvChunker is not None:
|
||||
chunker_class = CsvChunker
|
||||
else:
|
||||
status.update("[yellow]⚠ CsvChunker not available, using TextChunker[/yellow]")
|
||||
|
||||
# Prepare datasets parameter
|
||||
datasets = [dataset_name] if dataset_name else None
|
||||
import cognee
|
||||
|
||||
await cognee.cognify(
|
||||
datasets=datasets,
|
||||
chunker=chunker_class,
|
||||
run_in_background=run_background,
|
||||
)
|
||||
|
||||
if run_background:
|
||||
status.update("[green]✓ Cognification started in background![/green]")
|
||||
else:
|
||||
status.update("[green]✓ Cognification completed successfully![/green]")
|
||||
|
||||
except Exception as e:
|
||||
status.update(f"[red]✗ Failed to cognify: {str(e)}[/red]")
|
||||
|
||||
finally:
|
||||
# Re-enable inputs
|
||||
self.is_processing = False
|
||||
dataset_input = self.query_one("#dataset-input", Input)
|
||||
chunker_radio = self.query_one("#chunker-radio", RadioSet)
|
||||
background_checkbox = self.query_one("#background-checkbox", Checkbox)
|
||||
dataset_input.disabled = False
|
||||
chunker_radio.disabled = False
|
||||
background_checkbox.disabled = False
|
||||
dataset_input.focus()
|
||||
135
cognee/cli/tui/common_styles.py
Normal file
135
cognee/cli/tui/common_styles.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
"""Common CSS styles for TUI screens to reduce repetition."""
|
||||
|
||||
COMMON_STYLES = """
|
||||
/* Common screen background */
|
||||
Screen {
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
/* Common container styles */
|
||||
.tui-container {
|
||||
height: 100%;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
.tui-bordered-wrapper {
|
||||
border: solid $primary;
|
||||
}
|
||||
|
||||
.tui-content-container {
|
||||
height: auto;
|
||||
padding: 1;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
/* Main container wrapper - used across all screens */
|
||||
.tui-main-container {
|
||||
height: 100%;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
/* Title wrapper - centers title elements */
|
||||
.tui-title-wrapper {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
align: center middle;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
/* Styled title with border */
|
||||
.tui-title-bordered {
|
||||
text-align: center;
|
||||
width: auto;
|
||||
color: $accent;
|
||||
text-style: bold;
|
||||
padding: 0 10;
|
||||
border: solid $accent;
|
||||
}
|
||||
|
||||
.tui-form {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: solid $primary;
|
||||
padding: 2;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
/* Common title styles */
|
||||
.tui-title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $accent;
|
||||
margin-bottom: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Common label styles */
|
||||
.tui-label {
|
||||
color: $text-muted;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.tui-label-spaced {
|
||||
color: $text-muted;
|
||||
margin-top: 1;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
/* Common input styles */
|
||||
Input {
|
||||
width: 100%;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
/* Common button styles */
|
||||
Button {
|
||||
margin: 0 1;
|
||||
}
|
||||
|
||||
/* Common status message styles */
|
||||
.tui-status {
|
||||
text-align: center;
|
||||
margin-top: 2;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Common footer styles */
|
||||
.tui-footer {
|
||||
dock: bottom;
|
||||
padding: 1 0;
|
||||
background: $boost;
|
||||
color: $text-muted;
|
||||
content-align: center middle;
|
||||
border: solid $primary;
|
||||
}
|
||||
|
||||
/* Common dialog/modal styles */
|
||||
.tui-dialog {
|
||||
border: thick $warning;
|
||||
background: $surface;
|
||||
padding: 2;
|
||||
}
|
||||
|
||||
.tui-dialog-title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $warning;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.tui-dialog-message {
|
||||
text-align: center;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.tui-dialog-buttons {
|
||||
align: center middle;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
/* Common input group styles */
|
||||
.tui-input-group {
|
||||
height: auto;
|
||||
margin-bottom: 2;
|
||||
}
|
||||
"""
|
||||
404
cognee/cli/tui/config_screen.py
Normal file
404
cognee/cli/tui/config_screen.py
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
import argparse
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
|
||||
from cognee.cli.reference import SupportsCliCommand
|
||||
from cognee.cli import DEFAULT_DOCS_URL
|
||||
from cognee.cli.exceptions import CliCommandException
|
||||
|
||||
try:
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import DataTable, Input, Label, Button, Static
|
||||
from textual.containers import Container, Horizontal
|
||||
from textual.binding import Binding
|
||||
from cognee.cli.tui.base_screen import BaseTUIScreen
|
||||
except ImportError:
|
||||
# Handle case where textual is not installed to prevent import errors at module level
|
||||
BaseTUIScreen = object
|
||||
|
||||
|
||||
class ConfirmModal(Screen):
|
||||
"""Modal screen for confirming reset action."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
]
|
||||
|
||||
CSS = """
|
||||
ConfirmModal {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#confirm-dialog {
|
||||
width: 60;
|
||||
height: auto;
|
||||
border: thick $warning;
|
||||
background: $surface;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
#confirm-title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#confirm-message {
|
||||
text-align: center;
|
||||
margin-bottom: 2;
|
||||
}
|
||||
|
||||
.tui-dialog-buttons {
|
||||
align: center middle;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
Button {
|
||||
margin: 0 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, key: str, default_value: str):
|
||||
super().__init__()
|
||||
self.key = key
|
||||
self.default_value = default_value
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="confirm-dialog"):
|
||||
yield Label("⚠ Reset Configuration", id="confirm-title")
|
||||
yield Label(f"Are you sure you want to reset '{self.key}'?", id="confirm-message")
|
||||
yield Label(f"It will revert to: {self.default_value}", classes="dim-text")
|
||||
|
||||
with Horizontal(classes="tui-dialog-buttons"):
|
||||
yield Button("Reset", variant="error", id="confirm-btn")
|
||||
yield Button("Cancel", variant="default", id="cancel-btn")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "confirm-btn":
|
||||
self.dismiss(True)
|
||||
else:
|
||||
self.dismiss(False)
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.dismiss(False)
|
||||
|
||||
|
||||
class ConfigTUIScreen(BaseTUIScreen):
|
||||
"""Main config TUI screen with inline editing and live data fetching."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("q", "quit_app", "Quit"),
|
||||
Binding("escape", "cancel_or_back", "Back/Cancel"),
|
||||
Binding("e", "edit", "Edit"),
|
||||
Binding("enter", "confirm_edit", "Confirm", show=False),
|
||||
Binding("r", "reset", "Reset"),
|
||||
Binding("up", "cursor_up", "Up", show=False),
|
||||
Binding("down", "cursor_down", "Down", show=False),
|
||||
]
|
||||
|
||||
CSS = (
|
||||
BaseTUIScreen.CSS
|
||||
+ """
|
||||
DataTable {
|
||||
height: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#inline-edit-container {
|
||||
display: none;
|
||||
height: auto;
|
||||
padding: 0 1;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
#inline-edit-container.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#edit-label {
|
||||
color: $text-muted;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#inline-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dim-text {
|
||||
color: $text-muted;
|
||||
text-align: center;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
# Config key mappings: Key -> (Reset Method Name, Default Value)
|
||||
CONFIG_MAP = {
|
||||
"llm_provider": ("set_llm_provider", "openai"),
|
||||
"llm_model": ("set_llm_model", "gpt-5-mini"),
|
||||
"llm_api_key": ("set_llm_api_key", ""),
|
||||
"llm_endpoint": ("set_llm_endpoint", ""),
|
||||
"graph_database_provider": ("set_graph_database_provider", "kuzu"),
|
||||
"vector_db_provider": ("set_vector_db_provider", "lancedb"),
|
||||
"vector_db_url": ("set_vector_db_url", ""),
|
||||
"vector_db_key": ("set_vector_db_key", ""),
|
||||
"chunk_size": ("set_chunk_size", 1500),
|
||||
"chunk_overlap": ("set_chunk_overlap", 10),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.editing_key = None # Track which key is being edited
|
||||
|
||||
def compose_content(self) -> ComposeResult:
|
||||
with Container(classes="tui-main-container"):
|
||||
with Container(classes="tui-title-wrapper"):
|
||||
yield Static("⚙️ Configuration Manager", classes="tui-title-bordered")
|
||||
|
||||
with Container(classes="tui-bordered-wrapper"):
|
||||
table = DataTable(id="config-table")
|
||||
table.cursor_type = "row"
|
||||
table.zebra_stripes = True
|
||||
yield table
|
||||
|
||||
with Container(id="inline-edit-container"):
|
||||
yield Label("", id="edit-label")
|
||||
yield Input(placeholder="Enter new value", id="inline-input")
|
||||
|
||||
def compose_footer(self) -> ComposeResult:
|
||||
yield Static(
|
||||
"↑↓: Navigate • e: Edit • Enter: Save • r: Reset • Esc: Back • q: Quit",
|
||||
classes="tui-footer",
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Initialize the table with columns and current data."""
|
||||
table = self.query_one(DataTable)
|
||||
table.add_columns("Configuration Key", "Current Value")
|
||||
|
||||
self._load_table_data()
|
||||
table.focus()
|
||||
|
||||
def _load_table_data(self) -> None:
|
||||
"""Fetch real config values and populate the table."""
|
||||
table = self.query_one(DataTable)
|
||||
table.clear()
|
||||
|
||||
try:
|
||||
import cognee
|
||||
|
||||
# Check if get method exists, otherwise warn
|
||||
has_get = hasattr(cognee.config, "get")
|
||||
except ImportError:
|
||||
has_get = False
|
||||
self.notify("Could not import cognee config", severity="error")
|
||||
|
||||
for key, (_, default_val) in self.CONFIG_MAP.items():
|
||||
value_display = "N/A"
|
||||
|
||||
if has_get:
|
||||
try:
|
||||
raw_val = cognee.config.get(key)
|
||||
if raw_val is None:
|
||||
raw_val = default_val
|
||||
value_display = str(raw_val) if raw_val is not None else "(empty)"
|
||||
except Exception:
|
||||
value_display = "Error fetching value"
|
||||
|
||||
table.add_row(key, value_display, key=key)
|
||||
|
||||
def action_cursor_up(self) -> None:
|
||||
if self.editing_key:
|
||||
return
|
||||
self.query_one(DataTable).action_cursor_up()
|
||||
|
||||
def action_cursor_down(self) -> None:
|
||||
if self.editing_key:
|
||||
return
|
||||
self.query_one(DataTable).action_cursor_down()
|
||||
|
||||
def action_cancel_or_back(self) -> None:
|
||||
if self.editing_key:
|
||||
self._cancel_edit()
|
||||
else:
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self) -> None:
|
||||
self.app.exit()
|
||||
|
||||
def action_edit(self) -> None:
|
||||
"""Start inline editing for the selected config value."""
|
||||
if self.editing_key:
|
||||
return
|
||||
|
||||
table = self.query_one(DataTable)
|
||||
if table.cursor_row < 0:
|
||||
return
|
||||
|
||||
# Get row data using the cursor
|
||||
row_key = table.coordinate_to_cell_key(table.cursor_coordinate).row_key
|
||||
current_val = table.get_cell(row_key, list(table.columns.keys())[1]) # Get value column
|
||||
|
||||
self.editing_key = str(row_key.value)
|
||||
|
||||
# Show edit container
|
||||
edit_container = self.query_one("#inline-edit-container")
|
||||
edit_container.add_class("visible")
|
||||
|
||||
# Update UI
|
||||
label = self.query_one("#edit-label", Label)
|
||||
label.update(f"Editing: [bold]{self.editing_key}[/bold]")
|
||||
|
||||
input_widget = self.query_one("#inline-input", Input)
|
||||
input_widget.value = ""
|
||||
# Don't put "empty" or "N/A" into the input box to save user deleting it
|
||||
if current_val not in ["(empty)", "N/A", "Error fetching value"]:
|
||||
input_widget.value = str(current_val)
|
||||
|
||||
input_widget.placeholder = f"Enter new value for {self.editing_key}"
|
||||
input_widget.focus()
|
||||
|
||||
def action_confirm_edit(self) -> None:
|
||||
"""Confirm the inline edit and save the value."""
|
||||
if not self.editing_key:
|
||||
return
|
||||
|
||||
input_widget = self.query_one("#inline-input", Input)
|
||||
value = input_widget.value.strip()
|
||||
|
||||
# Allow saving even if empty (might mean unset/empty string)
|
||||
self._save_config(self.editing_key, value)
|
||||
self._cancel_edit()
|
||||
|
||||
def _cancel_edit(self) -> None:
|
||||
self.editing_key = None
|
||||
edit_container = self.query_one("#inline-edit-container")
|
||||
edit_container.remove_class("visible")
|
||||
self.query_one("#inline-input", Input).value = ""
|
||||
self.query_one(DataTable).focus()
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
if event.input.id == "inline-input" and self.editing_key:
|
||||
self.action_confirm_edit()
|
||||
|
||||
def action_reset(self) -> None:
|
||||
"""Reset the selected config to default."""
|
||||
table = self.query_one(DataTable)
|
||||
if table.cursor_row < 0:
|
||||
return
|
||||
|
||||
row_key_obj = table.coordinate_to_cell_key(table.cursor_coordinate).row_key
|
||||
key = str(row_key_obj.value)
|
||||
|
||||
if key not in self.CONFIG_MAP:
|
||||
self.notify(f"Cannot reset '{key}'", severity="warning")
|
||||
return
|
||||
|
||||
_, default_value = self.CONFIG_MAP[key]
|
||||
display_default = "(empty)" if default_value == "" else str(default_value)
|
||||
|
||||
def handle_confirm_result(confirmed: bool) -> None:
|
||||
if confirmed:
|
||||
self._reset_config(key)
|
||||
|
||||
self.app.push_screen(ConfirmModal(key, display_default), handle_confirm_result)
|
||||
|
||||
def _save_config(self, key: str, value: str) -> None:
|
||||
"""Save config value and update UI."""
|
||||
try:
|
||||
import cognee
|
||||
|
||||
# Parse value types (restore JSON behavior)
|
||||
try:
|
||||
parsed_value = json.loads(value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# If it looks like a boolean but json didn't catch it
|
||||
if value.lower() == "true":
|
||||
parsed_value = True
|
||||
elif value.lower() == "false":
|
||||
parsed_value = False
|
||||
else:
|
||||
parsed_value = value
|
||||
|
||||
cognee.config.set(key, parsed_value)
|
||||
self._update_table_row(key, parsed_value)
|
||||
self.notify(f"✓ Set {key}", severity="information")
|
||||
|
||||
except Exception as e:
|
||||
self.notify(f"✗ Error setting {key}: {str(e)}", severity="error")
|
||||
|
||||
def _reset_config(self, key: str) -> None:
|
||||
"""Reset config to default using mapped method and update UI."""
|
||||
try:
|
||||
import cognee
|
||||
|
||||
method_name, default_value = self.CONFIG_MAP[key]
|
||||
|
||||
if hasattr(cognee.config, method_name):
|
||||
method = getattr(cognee.config, method_name)
|
||||
method(default_value)
|
||||
|
||||
# IMPROVEMENT: Update table immediately
|
||||
self._update_table_row(key, default_value)
|
||||
self.notify(f"✓ Reset {key}", severity="information")
|
||||
else:
|
||||
self.notify(f"✗ Reset method '{method_name}' not found", severity="error")
|
||||
|
||||
except Exception as e:
|
||||
self.notify(f"✗ Failed to reset {key}: {str(e)}", severity="error")
|
||||
|
||||
def _update_table_row(self, key: str, value: Any) -> None:
|
||||
"""Helper to update a specific row's value column visually."""
|
||||
table = self.query_one(DataTable)
|
||||
display_val = str(value) if value != "" else "(empty)"
|
||||
|
||||
# 'key' was used as the row_key in add_row, so we can address it directly
|
||||
# The value column is at index 1 (0 is key, 1 is value)
|
||||
try:
|
||||
col_key = list(table.columns.keys())[1]
|
||||
table.update_cell(key, col_key, display_val)
|
||||
except Exception:
|
||||
# Fallback if key update fails, reload all
|
||||
self._load_table_data()
|
||||
|
||||
|
||||
class ConfigTUICommand(SupportsCliCommand):
|
||||
"""TUI command for config management."""
|
||||
|
||||
command_string = "config-tui"
|
||||
help_string = "Launch interactive TUI for managing cognee configuration"
|
||||
docs_url = f"{DEFAULT_DOCS_URL}/usage/config-tui"
|
||||
|
||||
def configure_parser(self, parser: argparse.ArgumentParser) -> None:
|
||||
pass
|
||||
|
||||
def execute(self, args: argparse.Namespace) -> None:
|
||||
try:
|
||||
# Import here to check if Textual is actually installed
|
||||
from textual.app import App
|
||||
|
||||
class ConfigTUIApp(App):
|
||||
"""Simple app wrapper for config TUI."""
|
||||
|
||||
CSS = """
|
||||
Screen { background: $surface; }
|
||||
"""
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.push_screen(ConfigTUIScreen())
|
||||
|
||||
app = ConfigTUIApp()
|
||||
app.run()
|
||||
|
||||
except ImportError:
|
||||
raise CliCommandException(
|
||||
"Textual is not installed. Install with: pip install textual",
|
||||
docs_url=self.docs_url,
|
||||
)
|
||||
except Exception as ex:
|
||||
raise CliCommandException(
|
||||
f"Failed to launch config TUI: {str(ex)}",
|
||||
docs_url=self.docs_url,
|
||||
raiseable_exception=ex,
|
||||
)
|
||||
247
cognee/cli/tui/delete_screen.py
Normal file
247
cognee/cli/tui/delete_screen.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import asyncio
|
||||
from uuid import UUID
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import Input, Button, Static, Label
|
||||
from textual.containers import Container, Vertical, Horizontal
|
||||
from textual.binding import Binding
|
||||
from cognee.cli.tui.base_screen import BaseTUIScreen
|
||||
from cognee.modules.data.methods.delete_dataset_by_name import delete_dataset_by_name
|
||||
from cognee.modules.data.methods.delete_data_by_user import delete_data_by_user
|
||||
from cognee.modules.users.methods import get_default_user
|
||||
|
||||
|
||||
class DeleteTUIScreen(BaseTUIScreen):
|
||||
"""Simple delete screen with input fields for dataset name, user ID, or delete all."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("q", "quit_app", "Quit"),
|
||||
Binding("escape", "back", "Back"),
|
||||
Binding("ctrl+s", "delete", "Delete"),
|
||||
Binding("ctrl+a", "delete_all", "Delete All", priority=True),
|
||||
]
|
||||
|
||||
CSS = (
|
||||
BaseTUIScreen.CSS
|
||||
+ """
|
||||
#button-group {
|
||||
height: auto;
|
||||
align: center middle;
|
||||
margin-top: 2;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.is_processing = False
|
||||
|
||||
def compose_content(self) -> ComposeResult:
|
||||
with Container(classes="tui-main-container"):
|
||||
with Container(classes="tui-title-wrapper"):
|
||||
yield Static("🗑 Delete Data", classes="tui-title-bordered")
|
||||
with Vertical(id="delete-form", classes="tui-form"):
|
||||
with Vertical(classes="tui-input-group"):
|
||||
yield Label("Dataset Name (optional):", classes="tui-label")
|
||||
yield Input(
|
||||
placeholder="Enter dataset name to delete specific dataset",
|
||||
id="dataset-input",
|
||||
)
|
||||
|
||||
with Vertical(classes="tui-input-group"):
|
||||
yield Label("User ID (optional):", classes="tui-label")
|
||||
yield Input(
|
||||
placeholder="Enter user ID to delete user's data or leave empty for default user.",
|
||||
id="user-input",
|
||||
)
|
||||
|
||||
with Horizontal(id="button-group"):
|
||||
yield Button("Delete", variant="error", id="delete-btn")
|
||||
yield Button("Delete All", variant="error", id="delete-all-btn")
|
||||
|
||||
yield Static("", classes="tui-status")
|
||||
|
||||
def compose_footer(self) -> ComposeResult:
|
||||
yield Static(
|
||||
"Ctrl+s: Delete • Ctrl+a: Delete All • Esc: Back • q: Quit", classes="tui-footer"
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Focus the dataset input on mount."""
|
||||
dataset_input = self.query_one("#dataset-input", Input)
|
||||
dataset_input.focus()
|
||||
|
||||
def action_back(self) -> None:
|
||||
"""Go back to home screen."""
|
||||
if not self.is_processing:
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self) -> None:
|
||||
"""Quit the entire application."""
|
||||
self.app.exit()
|
||||
|
||||
def action_delete(self) -> None:
|
||||
"""Delete the dataset."""
|
||||
if not self.is_processing:
|
||||
self._handle_delete()
|
||||
|
||||
def action_delete_all(self) -> None:
|
||||
"""Delete all data."""
|
||||
if not self.is_processing:
|
||||
self._handle_delete_all()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button presses."""
|
||||
if self.is_processing:
|
||||
return
|
||||
if event.button.id == "delete-btn":
|
||||
self._handle_delete()
|
||||
elif event.button.id == "delete-all-btn":
|
||||
self._handle_delete_all()
|
||||
elif event.button.id == "cancel-btn":
|
||||
self.app.pop_screen()
|
||||
|
||||
def _handle_delete(self) -> None:
|
||||
status = self.query_one(".tui-status", Static)
|
||||
status.update("🔍 Starting the deletion process...")
|
||||
"""Handle delete operation for dataset or user."""
|
||||
if self.is_processing:
|
||||
return
|
||||
|
||||
dataset_input = self.query_one("#dataset-input", Input)
|
||||
user_input = self.query_one("#user-input", Input)
|
||||
|
||||
dataset_name = dataset_input.value.strip() or None
|
||||
user_id = user_input.value.strip() or None
|
||||
|
||||
if not dataset_name and not user_id:
|
||||
status.update("⚠️ Please enter a dataset name or user ID")
|
||||
return
|
||||
|
||||
self.is_processing = True
|
||||
status.update("🔍 Checking data to delete...")
|
||||
# Run async delete operation
|
||||
asyncio.create_task(self._delete_async(dataset_name, user_id))
|
||||
|
||||
async def _delete_async(self, dataset_name: str | None, user_id: str | None) -> None:
|
||||
"""Async function to delete data."""
|
||||
status = self.query_one(".tui-status", Static)
|
||||
try:
|
||||
if user_id is None:
|
||||
user = await get_default_user()
|
||||
resolved_user_id = user.id
|
||||
else:
|
||||
resolved_user_id = UUID(user_id)
|
||||
|
||||
if dataset_name:
|
||||
await delete_dataset_by_name(dataset_name, resolved_user_id)
|
||||
status.update(f"✓ Successfully deleted dataset '{dataset_name}'.")
|
||||
else:
|
||||
await delete_data_by_user(resolved_user_id)
|
||||
status.update(f"✓ Successfully deleted all data for user {resolved_user_id}.")
|
||||
except Exception as e:
|
||||
status.update(f"✗ Error: {str(e)}")
|
||||
finally:
|
||||
self.is_processing = False
|
||||
self.clear_input()
|
||||
|
||||
def _handle_delete_all(self) -> None:
|
||||
"""Handle delete all operation with confirmation."""
|
||||
if self.is_processing:
|
||||
return
|
||||
user_input = self.query_one("#user-input", Input)
|
||||
user_id = user_input.value.strip() or None
|
||||
|
||||
def handle_confirm(confirmed: bool) -> None:
|
||||
if confirmed:
|
||||
asyncio.create_task(self._perform_delete_all(user_id))
|
||||
|
||||
self.app.push_screen(DeleteAllConfirmModal(), handle_confirm)
|
||||
|
||||
async def _perform_delete_all(self, user_id: str | None) -> None:
|
||||
"""Perform the actual delete all operation."""
|
||||
status = self.query_one(".tui-status", Static)
|
||||
self.is_processing = True
|
||||
|
||||
try:
|
||||
status.update("🔍 Deleting all data...")
|
||||
if user_id is None:
|
||||
user = await get_default_user()
|
||||
resolved_user_id = user.id
|
||||
else:
|
||||
resolved_user_id = UUID(user_id)
|
||||
await delete_data_by_user(resolved_user_id)
|
||||
status.update(f"✓ Successfully deleted all data by user {resolved_user_id}")
|
||||
|
||||
# Clear inputs
|
||||
dataset_input = self.query_one("#dataset-input", Input)
|
||||
user_input = self.query_one("#user-input", Input)
|
||||
dataset_input.value = ""
|
||||
user_input.value = ""
|
||||
|
||||
except Exception as e:
|
||||
status.update(f"✗ Error: {str(e)}")
|
||||
finally:
|
||||
self.is_processing = False
|
||||
|
||||
def clear_input(self) -> None:
|
||||
dataset_input = self.query_one("#dataset-input", Input)
|
||||
user_input = self.query_one("#user-input", Input)
|
||||
dataset_input.value = ""
|
||||
user_input.value = ""
|
||||
|
||||
|
||||
class DeleteAllConfirmModal(BaseTUIScreen):
|
||||
"""Modal screen for confirming delete all action."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
]
|
||||
|
||||
CSS = (
|
||||
BaseTUIScreen.CSS
|
||||
+ """
|
||||
DeleteAllConfirmModal {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#confirm-dialog {
|
||||
width: 60;
|
||||
height: 20;
|
||||
border: thick $error;
|
||||
background: $surface;
|
||||
padding: 2;
|
||||
}
|
||||
|
||||
#confirm-title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $error;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#confirm-warning {
|
||||
text-align: center;
|
||||
color: $warning;
|
||||
text-style: bold;
|
||||
margin-bottom: 2;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def compose_content(self) -> ComposeResult:
|
||||
with Container(id="confirm-dialog"):
|
||||
yield Label("⚠️ DELETE ALL DATA", id="confirm-title")
|
||||
yield Label("This will delete ALL data from cognee", classes="tui-dialog-message")
|
||||
yield Label("This operation is IRREVERSIBLE!", id="confirm-warning")
|
||||
with Horizontal(classes="tui-dialog-buttons"):
|
||||
yield Button("Delete All", variant="error", id="confirm-btn")
|
||||
yield Button("Cancel", variant="default", id="cancel-btn")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "confirm-btn":
|
||||
self.dismiss(True)
|
||||
else:
|
||||
self.dismiss(False)
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.dismiss(False)
|
||||
188
cognee/cli/tui/home_screen.py
Normal file
188
cognee/cli/tui/home_screen.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
from textual.app import ComposeResult
|
||||
from textual.widgets import ListView, ListItem, Static
|
||||
from textual.containers import Container, Horizontal
|
||||
from textual.binding import Binding
|
||||
|
||||
from cognee.cli.tui.base_screen import BaseTUIScreen
|
||||
from cognee.cli.tui.config_screen import ConfigTUIScreen
|
||||
from cognee.cli.tui.add_screen import AddTUIScreen
|
||||
from cognee.cli.tui.cognify_screen import CognifyTUIScreen
|
||||
from cognee.cli.tui.search_screen import SearchTUIScreen
|
||||
from cognee.cli.tui.delete_screen import DeleteTUIScreen
|
||||
|
||||
|
||||
def make_item(icon: str, command: str, description: str) -> ListItem:
|
||||
"""Compose a ListItem that contains a Horizontal container with 3 children."""
|
||||
return ListItem(
|
||||
Horizontal(
|
||||
Static(icon, classes="cmd-icon"),
|
||||
Static(command, classes="cmd-name"),
|
||||
Static(description, classes="cmd-desc"),
|
||||
classes="cmd-row",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class HomeScreen(BaseTUIScreen):
|
||||
"""Home screen with command selection menu."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("q", "quit_app", "Quit"),
|
||||
Binding("escape", "quit_app", "Quit"),
|
||||
Binding("enter", "select", "Select"),
|
||||
Binding("up", "nav_up", "Up", priority=True),
|
||||
Binding("down", "nav_down", "Down", priority=True),
|
||||
]
|
||||
|
||||
CSS = (
|
||||
BaseTUIScreen.CSS
|
||||
+ """
|
||||
ListView > ListItem {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-list > ListItem {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
height: auto;
|
||||
background: $surface;
|
||||
border: none;
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
ListView {
|
||||
height: auto;
|
||||
background: $surface;
|
||||
border: none;
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
ListItem {
|
||||
background: $surface;
|
||||
color: $text;
|
||||
width: 100%;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
ListItem:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
ListItem.highlighted {
|
||||
background: $primary-darken-3;
|
||||
color: $text;
|
||||
}
|
||||
ListItem.highlighted .cmd-name {
|
||||
text-style: bold;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
.cmd-row {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
align-horizontal: left;
|
||||
align-vertical: middle;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
.cmd-icon {
|
||||
width: 4;
|
||||
text-align: center;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.cmd-name {
|
||||
width: 14;
|
||||
padding-left: 1;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
.cmd-desc {
|
||||
width: 1fr;
|
||||
overflow: auto;
|
||||
padding-left: 1;
|
||||
color: $text-muted;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.lv = None
|
||||
self.current_index = 0
|
||||
|
||||
def compose_content(self) -> ComposeResult:
|
||||
with Container(classes="tui-main-container"):
|
||||
with Container(classes="tui-title-wrapper"):
|
||||
yield Static("Select Command", classes="tui-title-bordered")
|
||||
with Container(classes="tui-bordered-wrapper"):
|
||||
yield ListView(
|
||||
make_item("📥", "add", "Add data to cognee"),
|
||||
make_item("🔍", "search", "Search data in cognee"),
|
||||
make_item("⚡", "cognify", "Process data in cognee"),
|
||||
make_item("🗑️", "delete", "Delete data from cognee"),
|
||||
make_item("⚙️", "config", "Configure cognee settings"),
|
||||
id="menu-list",
|
||||
classes="menu-list",
|
||||
)
|
||||
|
||||
def compose_footer(self) -> ComposeResult:
|
||||
yield Static("↑↓: Navigate • Enter: Select • q/Esc: Quit", classes="tui-footer")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Focus the list view on mount."""
|
||||
self.lv = self.query_one(ListView)
|
||||
self.current_index = 0
|
||||
self.set_focus(self.lv)
|
||||
self._apply_highlight()
|
||||
|
||||
def _apply_highlight(self) -> None:
|
||||
lv = self.lv
|
||||
children = list(lv.children)
|
||||
self.lv.index = self.current_index
|
||||
for idx, item in enumerate(children):
|
||||
if idx == self.current_index:
|
||||
item.add_class("highlighted")
|
||||
else:
|
||||
item.remove_class("highlighted")
|
||||
|
||||
def action_nav_up(self) -> None:
|
||||
self.current_index = max(0, self.current_index - 1)
|
||||
self._apply_highlight()
|
||||
|
||||
def action_nav_down(self) -> None:
|
||||
children = list(self.lv.children)
|
||||
self.current_index = min(len(children) - 1, self.current_index + 1)
|
||||
self._apply_highlight()
|
||||
|
||||
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
||||
selected_index = event.index
|
||||
self.current_index = selected_index
|
||||
self._apply_highlight()
|
||||
if selected_index == 0: # add
|
||||
self.app.push_screen(AddTUIScreen())
|
||||
elif selected_index == 1: # search
|
||||
self.app.push_screen(SearchTUIScreen())
|
||||
elif selected_index == 2: # cognify
|
||||
self.app.push_screen(CognifyTUIScreen())
|
||||
elif selected_index == 3: # delete
|
||||
self.app.push_screen(DeleteTUIScreen())
|
||||
elif selected_index == 4: # config
|
||||
self.app.push_screen(ConfigTUIScreen())
|
||||
else:
|
||||
self.app.exit()
|
||||
|
||||
def action_select(self) -> None:
|
||||
"""Select the current item."""
|
||||
list_view = self.query_one(ListView)
|
||||
list_view.action_select_cursor()
|
||||
|
||||
def action_quit_app(self) -> None:
|
||||
"""Quit the entire application."""
|
||||
self.app.exit()
|
||||
179
cognee/cli/tui/search_screen.py
Normal file
179
cognee/cli/tui/search_screen.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import asyncio
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import Input, Label, Static, Select
|
||||
from textual.containers import Container, Vertical, ScrollableContainer
|
||||
from textual.binding import Binding
|
||||
from cognee.cli.tui.base_screen import BaseTUIScreen
|
||||
|
||||
|
||||
class SearchTUIScreen(BaseTUIScreen):
|
||||
"""Simple search screen with query input and results display."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("q", "quit_app", "Quit"),
|
||||
Binding("escape", "back", "Back"),
|
||||
Binding("ctrl+s", "search", "Search"),
|
||||
]
|
||||
|
||||
CSS = (
|
||||
BaseTUIScreen.CSS
|
||||
+ """
|
||||
#search-form {
|
||||
height: auto;
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#search-form Label {
|
||||
margin-bottom: 0;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
#search-form Input, #search-form Select {
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#results-container {
|
||||
height: 1fr;
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
#results-title {
|
||||
text-style: bold;
|
||||
color: $accent;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#results-content {
|
||||
height: 1fr;
|
||||
overflow-y: auto;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.is_searching = False
|
||||
|
||||
def compose_content(self) -> ComposeResult:
|
||||
with Container(classes="tui-main-container"):
|
||||
with Container(classes="tui-title-wrapper"):
|
||||
yield Static("🔍 Search Data", classes="tui-title-bordered")
|
||||
with Vertical(id="search-form"):
|
||||
yield Label("Query:", classes="tui-label-spaced")
|
||||
yield Input(placeholder="Enter your search query...", id="query-input")
|
||||
yield Label("Search Type:", classes="tui-label-spaced")
|
||||
yield Select(
|
||||
[
|
||||
("Graph Completion (Recommended)", "GRAPH_COMPLETION"),
|
||||
("RAG Completion", "RAG_COMPLETION"),
|
||||
("Chunks", "CHUNKS"),
|
||||
("Summaries", "SUMMARIES"),
|
||||
("Coding Rules", "CODING_RULES"),
|
||||
],
|
||||
value="GRAPH_COMPLETION",
|
||||
id="query-type-select",
|
||||
)
|
||||
with Container(id="results-container"):
|
||||
yield Static("Results", id="results-title")
|
||||
with ScrollableContainer(id="results-content"):
|
||||
yield Static(
|
||||
"Enter a query and click Search to see results.", id="results-text"
|
||||
)
|
||||
|
||||
def compose_footer(self) -> ComposeResult:
|
||||
yield Static("Ctrl+S: Search • Esc: Back • q: Quit", classes="tui-footer")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Focus the query input on mount."""
|
||||
query_input = self.query_one("#query-input", Input)
|
||||
query_input.focus()
|
||||
|
||||
def action_back(self) -> None:
|
||||
"""Go back to home screen."""
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self) -> None:
|
||||
"""Quit the entire application."""
|
||||
self.app.exit()
|
||||
|
||||
def action_search(self) -> None:
|
||||
"""Trigger search action."""
|
||||
if not self.is_searching:
|
||||
self._perform_search()
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
"""Handle Enter key in query input."""
|
||||
if event.input.id == "query-input":
|
||||
self._perform_search()
|
||||
|
||||
def _perform_search(self) -> None:
|
||||
"""Perform the search operation."""
|
||||
if self.is_searching:
|
||||
return
|
||||
|
||||
query_input = self.query_one("#query-input", Input)
|
||||
query_text = query_input.value.strip()
|
||||
|
||||
if not query_text:
|
||||
self.notify("Please enter a search query", severity="warning")
|
||||
return
|
||||
|
||||
query_type_select = self.query_one("#query-type-select", Select)
|
||||
query_type = str(query_type_select.value)
|
||||
|
||||
self.is_searching = True
|
||||
self.notify(f"Searching for: {query_text}", severity="information")
|
||||
|
||||
# Update results to show loading
|
||||
results_text = self.query_one("#results-text", Static)
|
||||
results_text.update("🔍 Searching...")
|
||||
|
||||
# Run async search
|
||||
asyncio.create_task(self._async_search(query_text, query_type))
|
||||
|
||||
async def _async_search(self, query_text: str, query_type: str) -> None:
|
||||
"""Async search operation."""
|
||||
try:
|
||||
import cognee
|
||||
from cognee.modules.search.types import SearchType
|
||||
|
||||
# Convert string to SearchType enum
|
||||
search_type = SearchType[query_type]
|
||||
# Perform search
|
||||
results = await cognee.search(
|
||||
query_text=query_text,
|
||||
query_type=search_type,
|
||||
system_prompt_path="answer_simple_question.txt",
|
||||
top_k=10,
|
||||
)
|
||||
|
||||
# Update results display
|
||||
results_text = self.query_one("#results-text", Static)
|
||||
|
||||
if not results:
|
||||
results_text.update("No results found for your query.")
|
||||
else:
|
||||
# Format results based on type
|
||||
if query_type in ["GRAPH_COMPLETION", "RAG_COMPLETION"]:
|
||||
formatted = "\n\n".join([f"📝 {result}" for result in results])
|
||||
elif query_type == "CHUNKS":
|
||||
formatted = "\n\n".join(
|
||||
[f"📄 Chunk {i + 1}:\n{result}" for i, result in enumerate(results)]
|
||||
)
|
||||
else:
|
||||
formatted = "\n\n".join([f"• {result}" for result in results])
|
||||
|
||||
results_text.update(formatted)
|
||||
|
||||
self.notify(f"✓ Found {len(results)} result(s)", severity="information")
|
||||
|
||||
except Exception as e:
|
||||
results_text = self.query_one("#results-text", Static)
|
||||
results_text.update(f"❌ Error: {str(e)}")
|
||||
self.notify(f"Search failed: {str(e)}", severity="error")
|
||||
|
||||
finally:
|
||||
self.is_searching = False
|
||||
|
|
@ -3,8 +3,7 @@ import asyncio
|
|||
from os import path
|
||||
import tempfile
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from typing import AsyncGenerator, List
|
||||
from typing import Optional, AsyncGenerator, List, Union
|
||||
from contextlib import asynccontextmanager
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
|
|
@ -257,35 +256,46 @@ class SQLAlchemyAdapter:
|
|||
return [schema[0] for schema in result.fetchall()]
|
||||
return []
|
||||
|
||||
async def delete_entity_by_id(
|
||||
self, table_name: str, data_id: UUID, schema_name: Optional[str] = "public"
|
||||
async def delete_entities_by_id(
|
||||
self,
|
||||
table_name: str,
|
||||
data_id: Union[UUID, List[UUID]], # Supports a single UUID or a List of UUIDs
|
||||
schema_name: Optional[str] = "public",
|
||||
):
|
||||
"""
|
||||
Delete an entity from the specified table based on its unique ID.
|
||||
Delete one or more entities from the specified table based on their ID(s).
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
|
||||
- table_name (str): The name of the table from which to delete the entity.
|
||||
- data_id (UUID): The unique identifier of the entity to be deleted.
|
||||
- schema_name (Optional[str]): The name of the schema where the table resides,
|
||||
defaults to 'public'. (default 'public')
|
||||
- table_name (str): The name of the table from which to delete the entities.
|
||||
- data_id (Union[UUID, List[UUID]]): The unique identifier(s) to be deleted.
|
||||
- schema_name (Optional[str]): The name of the schema where the table resides.
|
||||
"""
|
||||
if self.engine.dialect.name == "sqlite":
|
||||
async with self.get_async_session() as session:
|
||||
TableModel = await self.get_table(table_name, schema_name)
|
||||
|
||||
# Foreign key constraints are disabled by default in SQLite (for backwards compatibility),
|
||||
# so must be enabled for each database connection/session separately.
|
||||
# Ensure data_ids is a list for the WHERE clause logic
|
||||
if isinstance(data_id, list):
|
||||
data_ids_to_delete = data_id
|
||||
else:
|
||||
data_ids_to_delete = [data_id]
|
||||
|
||||
if not data_ids_to_delete:
|
||||
return
|
||||
|
||||
async with self.get_async_session() as session:
|
||||
TableModel = await self.get_table(table_name, schema_name)
|
||||
|
||||
# Handle SQLite's foreign key requirement
|
||||
if self.engine.dialect.name == "sqlite":
|
||||
from sqlalchemy import text
|
||||
|
||||
await session.execute(text("PRAGMA foreign_keys = ON;"))
|
||||
|
||||
await session.execute(TableModel.delete().where(TableModel.c.id == data_id))
|
||||
await session.commit()
|
||||
else:
|
||||
async with self.get_async_session() as session:
|
||||
TableModel = await self.get_table(table_name, schema_name)
|
||||
await session.execute(TableModel.delete().where(TableModel.c.id == data_id))
|
||||
await session.commit()
|
||||
# Construct the DELETE statement using the 'in_()' operator
|
||||
stmt = TableModel.delete().where(TableModel.c.id.in_(data_ids_to_delete))
|
||||
|
||||
# Execute and commit
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
async def delete_data_entity(self, data_id: UUID):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ from .get_dataset_ids import get_dataset_ids
|
|||
|
||||
# Delete
|
||||
from .delete_dataset import delete_dataset
|
||||
from .delete_dataset_by_name import delete_dataset_by_name
|
||||
from .delete_data_by_user import delete_data_by_user
|
||||
from .delete_data import delete_data
|
||||
|
||||
# Create
|
||||
|
|
|
|||
33
cognee/modules/data/methods/delete_data_by_user.py
Normal file
33
cognee/modules/data/methods/delete_data_by_user.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from uuid import UUID
|
||||
from sqlalchemy import select
|
||||
from cognee.infrastructure.databases.relational import get_relational_engine
|
||||
from cognee.modules.data.models import Dataset
|
||||
from cognee.modules.users.methods import get_user
|
||||
from cognee.shared.logging_utils import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
async def delete_data_by_user(user_id: UUID):
|
||||
"""
|
||||
Delete all datasets and their associated data for a specific user.
|
||||
|
||||
This function performs a comprehensive deletion of all data owned by a user,
|
||||
including datasets, data entries, and all related records in the database.
|
||||
|
||||
Args:
|
||||
user_id: UUID of the user whose data should be deleted
|
||||
|
||||
Raises:
|
||||
EntityNotFoundError: If user is not found
|
||||
"""
|
||||
db_engine = get_relational_engine()
|
||||
|
||||
async with db_engine.get_async_session() as session:
|
||||
# Verify user exists
|
||||
await get_user(user_id)
|
||||
# Get all datasets owned by this user
|
||||
datasets_query = select(Dataset.id).where(Dataset.owner_id == user_id)
|
||||
user_datasets_ids = (await session.execute(datasets_query)).scalars().all()
|
||||
if user_datasets_ids:
|
||||
await db_engine.delete_entities_by_id(Dataset.__table__.name, user_datasets_ids)
|
||||
28
cognee/modules/data/methods/delete_dataset_by_name.py
Normal file
28
cognee/modules/data/methods/delete_dataset_by_name.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from uuid import UUID
|
||||
from sqlalchemy import select
|
||||
from cognee.infrastructure.databases.relational import get_relational_engine
|
||||
from ..models import Dataset
|
||||
|
||||
|
||||
async def delete_dataset_by_name(dataset_name: str, user_id: UUID):
|
||||
"""
|
||||
Delete a single dataset by name for a specific user.
|
||||
|
||||
Args:
|
||||
dataset_name: The name of the dataset to delete (must be a single string).
|
||||
user_id: UUID of the dataset owner.
|
||||
|
||||
"""
|
||||
db_engine = get_relational_engine()
|
||||
|
||||
async with db_engine.get_async_session() as session:
|
||||
dataset_id = (
|
||||
await session.scalars(
|
||||
select(Dataset.id)
|
||||
.filter(Dataset.owner_id == user_id)
|
||||
.filter(Dataset.name == dataset_name)
|
||||
)
|
||||
).first()
|
||||
# Keeping this out of the first session, since delete_entities_by_id creates another session.
|
||||
if dataset_id:
|
||||
await db_engine.delete_entities_by_id(Dataset.__table__.name, dataset_id)
|
||||
|
|
@ -285,7 +285,7 @@ def cleanup_old_logs(logs_dir, max_files):
|
|||
return False
|
||||
|
||||
|
||||
def setup_logging(log_level=None, name=None):
|
||||
def setup_logging(log_level=None, name=None, enable_console_logging=True):
|
||||
"""Sets up the logging configuration with structlog integration.
|
||||
|
||||
Args:
|
||||
|
|
@ -465,7 +465,8 @@ def setup_logging(log_level=None, name=None):
|
|||
root_logger = logging.getLogger()
|
||||
if root_logger.hasHandlers():
|
||||
root_logger.handlers.clear()
|
||||
root_logger.addHandler(stream_handler)
|
||||
if enable_console_logging:
|
||||
root_logger.addHandler(stream_handler)
|
||||
|
||||
# Note: root logger needs to be set at NOTSET to allow all messages through and specific stream and file handlers
|
||||
# can define their own levels.
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ redis = ["redis>=5.0.3,<6.0.0"]
|
|||
monitoring = ["sentry-sdk[fastapi]>=2.9.0,<3", "langfuse>=2.32.0,<3"]
|
||||
|
||||
docling = ["docling>=2.54", "transformers>=4.55"]
|
||||
textual = ["textual>=6.6.0"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://www.cognee.ai"
|
||||
|
|
|
|||
61
uv.lock
generated
61
uv.lock
generated
|
|
@ -1093,6 +1093,9 @@ scraping = [
|
|||
{ name = "protego" },
|
||||
{ name = "tavily-python" },
|
||||
]
|
||||
textual = [
|
||||
{ name = "textual" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
|
|
@ -1193,6 +1196,7 @@ requires-dist = [
|
|||
{ name = "structlog", specifier = ">=25.2.0,<26" },
|
||||
{ name = "tavily-python", marker = "extra == 'scraping'", specifier = ">=0.7.12" },
|
||||
{ name = "tenacity", specifier = ">=9.0.0" },
|
||||
{ name = "textual", marker = "extra == 'textual'", specifier = ">=6.6.0" },
|
||||
{ name = "tiktoken", specifier = ">=0.8.0,<1.0.0" },
|
||||
{ name = "transformers", marker = "extra == 'codegraph'", specifier = ">=4.46.3,<5" },
|
||||
{ name = "transformers", marker = "extra == 'docling'", specifier = ">=4.55" },
|
||||
|
|
@ -1207,7 +1211,7 @@ requires-dist = [
|
|||
{ name = "uvicorn", specifier = ">=0.34.0,<1.0.0" },
|
||||
{ name = "websockets", specifier = ">=15.0.1,<16.0.0" },
|
||||
]
|
||||
provides-extras = ["api", "distributed", "scraping", "neo4j", "neptune", "postgres", "postgres-binary", "notebook", "langchain", "llama-index", "huggingface", "ollama", "mistral", "anthropic", "deepeval", "posthog", "groq", "llama-cpp", "chromadb", "docs", "codegraph", "evals", "graphiti", "aws", "dlt", "baml", "dev", "debug", "redis", "monitoring", "docling"]
|
||||
provides-extras = ["api", "distributed", "scraping", "neo4j", "neptune", "postgres", "postgres-binary", "notebook", "langchain", "llama-index", "huggingface", "ollama", "mistral", "anthropic", "deepeval", "posthog", "groq","llama-cpp", "chromadb", "docs", "codegraph", "evals", "graphiti", "aws", "dlt", "baml", "dev", "debug", "redis", "monitoring", "docling", "textual"]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
|
|
@ -3802,6 +3806,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/6d/c9/556846b9d112a3387397850d5560f5ec63464508c6aa068257f0516159d0/limits-4.8.0-py3-none-any.whl", hash = "sha256:de43d24969a0050b859dd29bbd61bd807a5de3ed9255f666aec1ea3dd3fc407e", size = 62028, upload-time = "2025-04-23T21:00:26.017Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkify-it-py"
|
||||
version = "2.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "uc-micro-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.80.0"
|
||||
|
|
@ -4157,6 +4173,11 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
linkify = [
|
||||
{ name = "linkify-it-py" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "marko"
|
||||
version = "2.2.2"
|
||||
|
|
@ -4323,6 +4344,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdit-py-plugins"
|
||||
version = "0.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
|
|
@ -8622,6 +8655,23 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textual"
|
||||
version = "6.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py", extra = ["linkify"] },
|
||||
{ name = "mdit-py-plugins" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "pygments" },
|
||||
{ name = "rich" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/2f/f0b408f227edca21d1996c1cd0b65309f0cbff44264aa40aded3ff9ce2e1/textual-6.6.0.tar.gz", hash = "sha256:53345166d6b0f9fd028ed0217d73b8f47c3a26679a18ba3b67616dcacb470eec", size = 1579327, upload-time = "2025-11-10T17:50:00.038Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/b3/95ab646b0c908823d71e49ab8b5949ec9f33346cee3897d1af6be28a8d91/textual-6.6.0-py3-none-any.whl", hash = "sha256:5a9484bd15ee8a6fd8ac4ed4849fb25ee56bed2cecc7b8a83c4cd7d5f19515e5", size = 712606, upload-time = "2025-11-10T17:49:58.391Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "threadpoolctl"
|
||||
version = "3.6.0"
|
||||
|
|
@ -9149,6 +9199,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uc-micro-py"
|
||||
version = "1.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unstructured"
|
||||
version = "0.18.27"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue